从 MySQL 的事务 到 锁机制 再到 MVCC

MySQL
226
0
0
2024-04-19

前言

转眼又一年~~2023马上就要到尾声了,在最后的几天中,我想给大家分享一下 MySQL 的一些小知识。

一、事务

1.1 含义

通俗理解:在我的理解下,事务可以使 一组操作,要么全部成功,要么全部失败。事务其目的是为了下保证数据最终的一致性。

举个例子:举个例子,我给你发支付宝转了666块红包。那自然我的支付宝余额会扣减666块,你的支付宝余额会增加666块。

1.2 ACID

  • 原子性 (Atomicity)
  • 一致性 (Consistency)
  • 隔离性 (lsolation)
  • 持久性 (Durability)

原子性指的是:当前事务的操作要么同时成功,要么同时失败。原子性由undo log日志来保证,因为undo log记载着数据修改前的信息。

比如我们要 insert 一条数据了,那undo log 会记录的一条对应的 delete 日志。我们要 update 一条记录时,那undo log会记录之前的 旧值 的update记录。 如果执行事务过程中出现异常的情况那执行 [回滚]。InnoDB引擎就是利用undo log记录下的数据,来将数据 恢复 到事务开始之前。

隔离性指的是:在事务并发执行时,他们内部的操作不能互相干扰。

如果多个事务可以在同一时刻操作同一份数据,那么就会可能会产生脏读、重复读、幻读的问题。 于是,事务与事务之间需要存在 一定 的隔离。在InnoDB引擎中,定义了四种隔离级别供我们使用:
  • read uncommit(读未提交)
  • read commit (读已提交)
  • repeatable read(可重复复读)
  • serializable (串行)

不同的隔离级别对事务之间的隔离性是不一样的 (级别越高事务隔离性越好,但性能就越低) ,而隔离性是由MySQL的各种锁来实现的,只是它屏蔽了加锁的细节。

持久性指的就是:一旦提交了事务,它对数据库的改变就应该是永久性的。说白了就是,会将数据持久化在硬盘上。

而持久性由 redo log 日志来保证,当我们要修改数据时,MySQL是先把这条记录所在的页找到,然后把该页加载到内存中,将对应记录进行修改。 为了防止内存修改完了,MySQL就挂掉了(如果内存改完,直接挂掉,那这次的修改相当于就丢失了)。 MySQL引入了 redo log,内存写完了然后会写一份 redo log,这份 redo log 记载着这次在某个页上做了什么修改。 即便 MySQL 在中途挂了,我们还可以根据 redo log来对数据进行恢复。 redo log 是顺序写的,写入速度很快。并且它记录的是物理修改 (xxxx页做了xxx修改),文件的体积很小,恢复速度也很快。

一致性指的就是:我们使用事务的目的 ,而[隔离性][原子性][持久性]均是为了保障 [一致性] 的手段,保证一致性需要由应用程序代码来保证。

比如,如果事务在发生的过程中,出现了异常情况,此时你就得回滚事务,而不是强行提交事务来导致数据不一致。

二、锁机制

2.1 锁分类

在InnoDB引擎下,按锁的粒度分类,可以简单分为:

  • 行锁
  • 表锁

行锁实际上是作用在索引之上的。

当我们的 SQL 命中了索引,那锁住的就是命中条件内的索引节点(这种就是行锁),如果没有命中索引,那我们锁的就是整个索引树 (表锁)。

简单来说就是:锁住的是整棵树还是某几个节点,完全取决于 SQL 条件是否有命中到对应的索引节点。

而行锁又可以简单分为:

  • 读锁 (共享锁、S锁)
  • 写锁 (排它锁、X锁)

读写锁区别:

  • 读锁是共享的,多个事务可以同时读取同一个资源,但不允许其他事务修改。
  • 写锁是排他的,写锁会阻塞其他的写锁和读锁。

2.2 隔离级别

再回到隔离级别上吧,以例子来说明。

首先来说下read uncommit(读未提交)比如说: A向B转账,A执行了转账语句,但A还没有提交事务,B读取数据发现自己账户钱变多了! B跟A说,我已经收到钱了。A回滚事务[rollback]等B再查看账户的钱时,发现钱并没有多。

简单的定义就是:事务B读取到了事务A还没提交的数据,这种用专业术语来说叫做[脏读]。

对于锁的维度而言,其实就是在read uncommit隔离级别下,读不会加任何锁,而写会加排他锁。读什么锁都不加,这就让排他锁无法排它了。

而我们又知道,对于更新操作而言,InnoDB是肯定会加写锁的 (数据库是不可能允许在同一时间,更新同一条记录的)。而读操作,如果不加任何锁,那就会造成上面的脏读。

脏读在生产环境下肯定是无法接受的,,那如果读加锁的话,那意味着:当更新数据的时,就没办法读取了,这会极大地降低数据库性能。

三、MVCC

3.1 介绍

在MySQL InnoDB引擎层面,又有新的解决方案 (解决加锁后读写性能问题),叫做MVCC(Multi-Version Concurrency Control)多版本并发控制。

在MVCC下,就可以做到读写不阻塞且避免了类似脏读这样的问题。那MVCC是怎么做的呢?

MVCC通过生成数据快照 (Snapshot)并用这个快照来提供一定级别 (语句级或事务级)的一致性读取。

3.2 隔离级别

回到事务隔离级别下,针对于 read commit (读已提交) 隔离级别,它生成的就是语句级快照,而针对于repeatable read(可重复读),它生成的就是事务级的快照。

前面提到过read uncommit隔离级别下会产生脏读,而read commit (读已提交)隔离级别解决了脏读。

思想其实很简单:在读取的时候生成一个"版本号",等到其他事务commit了之后,才会读取最新已commit的"版本号"数据。

比如说: 事务A读取了记录(生成版本号),事务B修改了记录(此时加了写锁)事务A再读取的时候,是依据最新的版本号来读取的(当事务B执行commit了之后,会生成一个新的版本号),如果事务B还没有commit,那事务A读取的还是之前版本号的数据。

通过[版本]的概念,这样就解决了脏读的问题,而通过 版本,又可以对应快照的数据。

read commit (读已提交) 解决了脏读,但也会有其他并发的问题。 [不可重复读]:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改。

不可重复读的例子: A查询数据库得到数据,B去修改数据库的数据,导致A多次查询数据库的结果都不一样[危害: A每次查询的结果都是受B的影响的]。

了解MVCC基础之后,就很容易想到repeatable read (可重复复读)隔离级别是怎么避免不可重复读的问题了 (前面也提到了)。

repeatable read (可重复复读)隔离级别是 事务级别]的快照!每次读取的都是厂当前事务的版本],即使当前数据被其他事务修改了(commit),也只会读取当前事务版本的数据。

在InnoDB引擎下的的repeatable read(可重复复读)隔离级别下,在MVCC下快照读,已经解决了幻读的问题 (因为它是读历史版本的数据)。

而如果是当前读 (比如 select * from table for update),则需要配合间隙锁来解决幻读的问题。

剩下的就是serializable (串行)隔离级别了,它的最高的隔离级别,相当于不允许事务的并发,事务与事务之间执行是串行的,它的效率最低,但同时也是最安全的。

3.3 原理

MVCC的主要是通过read view和undo log来实现的。

undo log前面也提到了,它会记录修改数据之前的信息,事务中的原子性就是通过undo log来实现的。所以,有undo log可以帮我们找到 版本]的数据。

而read view 实际上就是在查询时,InnoDB会生成一个read view,read view 有几个重要的字段,看下去就懂了。

  • trx ids (尚未提交commit的事务版本号集合)
  • low limit id (下一次要生成的事务ID值)
  • low limit id (尚未提交版本号的事务ID最小值)
  • creator_trx_id (当前的事务版本号)

在每行数据有两列隐藏的字段,分别是DB TRX ID (记录着当前ID) 以及DB ROLL PTR (指向上一个版本数据在undolog 里的位置指针)。

铺垫到这了,很容易就发现,MVCC其实就是靠[比对版本,来实现读写不阻塞,而版本的数据存在于undo log中。 而针对于不同的隔离级别 (read commit和repeatable read) ,无非就是read commit隔离级别下,每次都获取一个新的1ead view,repeatable read隔离级别则每次事务只获取一个read view

四、总结

事务、锁机制和 MVCC 是数据库管理系统中的三个核心概念,它们协同工作以确保数据的完整性和并发性。

在 MySQL 中,事务提供了一种方法来管理数据库操作的逻辑边界,锁机制用于控制对数据的并发访问,而 MVCC 则通过允许多个事务同时读取数据来提高并发性能。

了解和正确使用这些概念对于数据库管理员和开发人员来说至关重要,以确保数据库应用的性能和可靠性。