大家好,我是悟空。
MVCC 引入undo日志、roll_pointer、trx_id 三个概念:
undo日志:事务回滚时恢复数据到未变更状态,每次执行增、删、改操作时都会记录变更前的原始数据到undo日志;
roll_pointer属性:表记录的隐藏字段;
trx_id属性:表记录隐藏字段表示事务ID;
设想一个场景数据库,两个不同用户分别读取和修改同一条数据,为了保证数据的正确性;数据库需要引入读锁、写锁,在读数据加读锁禁止写操作,写数据加写锁禁止读操作,这么做会影响数据性能;
MVCC:引入版本链机制解决读写并发访问问题,由于每次对数据进行增、删、改操作都会备份数据到undo中,因此如果某个记录被多次修改会存在多个版本数据,这些版本信息之间通过roll_pointer属性连成一个链表;在RR级别下,数据记录具有可重复读特性,即在同一个事务执行的过程中对某条数据执行查询(快照读)操作,然后间隔几秒钟继续查询(快照读)数据记录不会发生变化,哪怕在间隔的几秒钟有其它事务对该记录进行了修改操作。
MySQL 中有四种隔离级别,Read Repeatable (RR)级别可以防止脏读、不可重复读、幻读问题。Read Committed (RC)级别解决了脏读问题。
那它是怎么做到的呢?就是利用了 MVCC 多版本控制机制。而且可以实现 读-写,写-读不冲突。
本解答尽量通俗易懂:
多版本
就是有多个某行记录更新后的版本,然后将这些版本从上到下串起来。有点像串糖葫芦,这个就是版本链。
比如说银行转账记录,将多次对账户的修改都串起来了。记录里面有是哪个事务做的转账记录,最后值等于多少。
(1)账户 A = 初始值 200元,事务 id = 40 ->
(2)账户 A = 初始值 200元 + 100 元 = 300 元,事务 id = 51 ->
(3)账户 A = 300 元 + 50 元 = 350 元,事务 id = 59 ->
(4)账户 A = 350 元 - 30 元 = 320 元,事务 id = 72
在 MySQL 就是利用 undo log 日志将这些串起来的。
如下图所示,undolog 的版本串起来长这样:

控制
用自身的事务 id 和其他地方存的事务 id 进行比较,看是否符合读取版本链上的条件,如果符合,读取后就返回了。 怎么控制的呢?利用 ReadView。ReadView 其实也不难理解,就是对当前活跃事务的一个统计。然后 MySQL 利用这个数据统计 + 版本链上的事务 id 来进行比较,获得某个可读到的版本。
ReadView
保证你只能读到事务开启之前,别的事务提交的值,或者自己提交的值。其他情况下无法读取到其他事务提交的值,避免了脏读。
ReadView 生成时机?
每个事务执行查询时都会生成自己事务的 ReadView。 RC 级别是每次查询都会重新生成一份,RR 级别是事务中的 ReadView 都不变。
ReadView 里面有四个重要的属性:
m_ids 事务列表:有哪些事务在MySQL里执行还没提交的; min_trx_id 最小事务 id:m_ids 列表中最小的值 ; max_trx_id 最大事务 id:下一个要生成的事务id,就是最大事务id; creator_trx_id:当前事务的 id。
这四个属性怎么用的呢?
比如说事务 A 用来查询,事务 B 用来更新,它俩都开启了事务,也都还没有提交,对应的事务 id 分别为 51 和 59,那么 ReadView 就长这样:

活跃事务列表就是 [51,59]。不是一个区间,只有两个值 51 和 59。另外图中的 B 事务 id 应该改为 59。
最小事务 id = 51。
最大事务 id = 59+1 = 60。
当前事务 id = 51。
事务 A 首先拿着这几个属性值,到版本链上一个一个比较版本上的事务 id,符合条件就返回。比较又分三种情况:
-
1、如果版本上的事务 id < 最小值 51,说明这个行记录在这些活跃的事务创建前就已经提交了,这个行记录的版本对于当前事务 A 是可见的,就返回了。
-
2、如果版本上的事务 id >= 最大值 60,则说明提交的事务是 ReadView 生成之后创建的,这个版本也是不可读的,就接着往下找。
-
3、如果版本上的事务 id 在在最小和最大值之间,就进行下一步判断:
- 3.1、如果版本上的事务 id 在这个列表 [51,59] 里面,这个列表其实就两个值,51 和 59,只要等于 51 或者 59,就说明不在列表中。说明提交的事务是和 A 事务差不多时间开启的事务,被 ReadView 记录在列表里面了。这种事务提交的版本也是不可读的,就接着往下找。(避免了脏读)
- 3.2、如果不在这个列表 [51,59] 里面(不等于 51 和 59),说明事务已提交了,是可以读取的,读到了就返回。
RC 的读已提交怎么做到的?
我们说 RC 隔离级别下,事务 A 下次查询时,就可以读到其他事务提交的数据了(读已提交),但是根据上面的3.1 的情况来看,事务 A 是读取不到事务 B 提交的呀?
这就需要在 A 查询时重新生成一个 ReadView 了,来看下重新生成的长啥样:
活跃事务列表就是 [51]。
最小事务 id = 51。
最大事务 id = 60。注意:这是 MySQL 下一个要生成的事务 id,不是指活跃事务中的最大事务 id。
当前事务 id = 51。
看到了吗?
事务 B 的 事务 id 59 不在活跃事务列表啦!但是又是小于最大事务 id 60 的。这就符合 3.2 的情况啦,可以读到这个版本了。
那下次 事务 A 再次查询时,又会生成一个 ReadView,可以读到其他事务提交的数据,这个数据和上次的数据很有可能不一样,也就是说不能保证每次读到的数据一样的,这就是不可重复读。 RR 的可重复读怎么做到的?
它和 RC 不同的地方在于,事务 A 查询时,是不会重新生成 ReadView 的,也就是说 B 提交的事务读取不到的,那就顺着版本链继续找呗。找着找着就只能读事务 A 自己提交的,或者事务开启之前,其他事务提交的,那么事务 A 每次查询都是读到一样的数据啦,但是读取的都不是最新的数据,这就是可重复读,避免了读取数据不一致的情况。
注意:不管其他事务怎么修改数据,事务 A 生成的 ReadView 是不会改变的,基于这个 ReadView 看到的值都是一样的!
RR 的幻读是怎么避免的?
比如 A 执行范围查询:select * from table where age > 10,查到了一条数据 X。然后事务 C 72 插入了一条数据,事务 A 再次查询时,可以查到两条数据 X 和 Y。但是 Y 的版本链上事务 id 等于 72,大于最大事务 id 60,说明是事务 A 发起查询后,当然是不可读到的了,所以事务 A 还是只能读到数据 X。
小结
通过版本链 + ReadView 做到了这些事情: 避免了 RR 隔离级别下的脏读、不可重复度、幻读问题。 避免了 RC 隔离级别下的脏读问题,实现了读取已提交数据的功能。
















