前置知识:
1. MySQL 的 undo log
2. 并发事务产生的问题和隔离级别
ReadView机制是什么?
ReadView 机制是基于 undo log 日志版本链实现的并发事务控制机制。使用这个机制,可以解决数据库事务并发的脏写、脏读、不可重复读和幻读问题。这个机制就是在事务开启的时候,生成一个 ReadView 数据结构,然后基于这个数据结构的值在并发读写的时候,进行分析,读取正确的数据的值。
生成的 ReadView 主要包含4个比较关键的数据:
- m_ids : 正在运行的,还没有提交的事务的ID的集合
- min_trx_id : 没有提交的事务中(m_ids里面),最小的事务的ID
- max_trx_id : 下一个要生成的事务的id。
- creator_id : 创建当前 ReadView 的事务的ID
来举一个例子来说明 ReadView 是怎么运作的。
- 首先假设有一行初始的数据,值是初始值,修改这条数据的最后一个事务id(trx_id)=20。

- 现在有事务A(id=50)和事务B(id=55)并发执行,生成一个 ReadView数据结构。
m_ids : [50,55] min_trx_id : 50 max_trx_id : 56 creator_id : 50
- 事务A读取开始读取这行数据,这行数据的 trx_id=20,不在 m_ids 内,并且比当前的 min_trx_id 50 要小,说明这行数据是在 ReadView 开启前就已经提交的值。所以可以直接读取这行数据的原始值。

- 事务B对这行数据进行修改,生成一个 undo log ,并且将值改为 B,并且将 trx_id 改为55。

- 事务A再次读取这行数据,发现这行数据的 trx_id 变成了55,而且比max_trx_id并且 trx_id 在 m_ids 内,所以不能读取值B。然后顺着 undo log 的指针,找到上一个这行数据的数据快照版本。找到 trx_id =20 的版本。trx_id 20 不在m_ids内,并且小于 mi_trx_id ,所以可以读取,读取到原始值。

到这里,两个事务并发执行,事务A只会读取原始值,而不会读取事务B修改过的值。
- 事务A将数据的值改为A,trx_id 改为50,生成 redo log。

- 这时候开启一个新事务C(id=80)开始并发执行。事务C将数据改为C,trx_id 改为80,生成 undo log。

- 事务A再次读取这行数据的值,发现数据的 trx_id 变成了80,大于 max_trx_id ,所以不能读取。然后读取上一个版本,发现 trx_id=50。这个事务就是自己。自己修改过的值,所以可以读取到。

到这里,事务 A,B,C 并发执行,事务A只会读取到原始值或者是事务A自己修改的值。不会读取到事务B或者事务C修改的值。
ReadView机制如何实现事务隔离级别?
ReadView 机制实现RC(Read Commited)隔离级别
RC隔离级别下,不会产生脏读脏写,但是会存在不可重复读和幻读。
实现RC隔离级别的核心是:事务处于RC隔离级别的时候,每次查询都会重新生成一个新的 ReadView 。整体的实现过程如下:
- 首先假设有一行初始的数据,值是初始值,修改这条数据的最后一个事务id(trx_id)=100。

- 现在有事务C(id=120)和事务D(id=140)都正在执行没有提交。

- 事务D对数据行进行修改,将值改成了D,trx_id 改成140,生成undo log。但是事务D没有提交。

- 事务C对数据行发起查询,生成 ReadView。读取数据时发现数据行的 trx_id=140,在m_ids内,所以往上一个版本找;找到trx_id=100的数据版本,读取到原始值。所以事务C是不会读取到未提交的事务D改的数据值的。

- 事务D进行提交。因为事务D已经提交了,在RC隔离级别下,事务C是可以读取到数据值D的。

- 事务C进行查询,再次生成 ReadView。然后读取数据行,发现数据行的事务id=140,小于 max_trx_id ,但是不存在于 m_ids 里面。所以是可以读取到数据值D的。

这个过程,就体现出来事务处于RC隔离级别下,事务是读取不到没有提交的其他事务的数据的,会读取到已经提交的事务的修改值。但是会出现不可重复读和幻读现象。
ReadView 机制实现RR(Repeatable Read)隔离级别
在MySQL的RR隔离级别下, 并发带来的问题都可以得到解决。不会出现脏读脏写,也不会出现不可重复读和幻读。
ReadView 机制配合 undo log 版本链机制,默认情况下就实现了可重复读。只会读取到事务自己修改过的值,或者 ReadView 生成之前,其他事务已经提交的值。
对于幻读问题的处理,核心是判断数据行的 trx_id 是否大于 ReadView 的max_trx_id ,如果大于,则是当前事务开启之后插入的数据,则不能读取。 对于幻读解决过程,如下:
- 事务E(id=200)和事务F(id=220),在同时开启; 事务E执行了查询”SELECT * FROM table WHERE id > 10”; 生成 ReadView, 获取到1条数据。

- 这时候,新执行了一个事务G(id=300)。往 table 插入了一条id=11的数据。

- 事务E再次执行刚才的SQL,获取到了2条数据,包括id=11的数据。发现id=11的数据的 trx_id=300 大于 max_trx_id 221。表明,这个数据是本事务E开启之后才发生的。所以不会读取出来。所以还是读取到原先的1条数据。

