InnoDB MVCC RR隔离级别下的数据可见性总结
一、背景
熟悉数据库隔离级别的人都知道,在RR(可重复读)隔离级别下,无论何时多次执行相同的SELECT快照读语句,得到的结果集都是完全一样的,即便两次SELECT语句执行期间,其他事务已经改变了该查询结果并已经提交。
对于这一机制的实现原理,网上常见的一种解释如下:
####MVCC在MySQL的InnoDB中的实现 在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下: SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。 INSERT时,保存当前事务版本号为行的创建版本号 DELETE时,保存当前事务版本号为行的删除版本号 UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行
上述解释确实可以让读者简单快速地理解MVCC机制的核心思想,我最开始也以为自己已经完全理解MVCC机制的实现原理了,但是当我试图利用上述原理去解释某些特别的实测结果时,却发现总是难以自圆其说。
二、无法自圆其说的用例
2.1 用例1:为什么看不到尚未提交的记录
如上图所示,事务2中,虽然记录A的创建版本号1小于当前事务版本号2,但是依然无法读取到记录A。
2.2 用例2:为什么看不到先启动事务中插入的记录
如上图所示,事务2中,虽然记录A的创建版本号小于当前事务版本号2,且记录A已经提交,但是第二次查询时,事务2依然无法查询到任何记录。
2.3 用例3:为什么可以看到后启动事务中插入的记录
如上图所示,事务1中,虽然记录A的创建版本号大于当前事务版本号1,但是事务1依然可以查询到记录A。
2.4 结论
可见,常见的对MVCC版本的实现原理的理解似乎遗漏了某些关键逻辑,导致无法解释很多特殊情况。
下面我们来一起看一下InnoDB中,MVCC机制到底是如何控制记录的可见性的。
三、遗漏的关键控制逻辑
InnoDB RR隔离界别下,MVCC对记录可见性控制,还有如下关键判定逻辑:
1. 事务ID并非在事务begin时就分配,而是在事务首次执行非快照读操作(SELECT ... FOR UPDATE/IN SHARE MODE、UPDATE、DELETE)时分配。
注: 如果事务中只有快照读,InnoDB对只有快照读事务有特殊优化,这类事务不会拥有事务ID,因为它们不会在系统中留下任何修改(甚至连锁都不会建),所以也没有留下事务ID的机会。 虽然使用SELECT TRX_ID FROM INFORMATION_SCHEMA.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID(); 查询此类事务ID时,会输出一个很大的事务ID(比如328855902652352),不过这只是MySQL在输出时临时随机分配的一个用于显示的ID而已。
2. 每个事务首次执行快照读操作时,会创建一个read_view对象(可以理解为在当前事务中,为数据表建立了一个逻辑快照,read_view对象就是用来控制此逻辑快照的可见范围的)。事务提交后,其创建的read_view对象将被销毁。
read_view对象中有三个关键字段用于判断记录的可见范围。它们分别是trx_ids、low_limit_id、up_limit_id。 1. read_view->trx_ids:创建该read_view时,记录正活跃的其他事务的ID集合。事务ID在集合中降序排列,便于二分查找。 2. read_view->low_limit_id:当前活跃事务中的最大事务ID+1(即系统中最近一个尚未分配出去的事务号)。 3. read_view->up_limit_id:当前活跃事务中的最小事务ID。
3. 如果记录的版本号比自己事务的read_view->up_limit_id小,则该记录的当前版本一定可见。因为这些版本的内容形成于快照创建之前,且它们的事务也肯定已经commit了。或者如果记录的版本号等于自己事务的事务ID,则该记录的当前版本也一定可见,因为该记录版本就是本事务产生的。
4. 如果记录的版本号与自己事务的read_view->low_limit_id一样或比它更大,则该版本的记录的当前版本一定不可见。因为这些版本的内容形成于快照创建之后。
不可见有如下两层含义: 1. 如果该记录是新增或修改后形成的新版本记录,则对新增和修改行为不可见,即看不到最新的内容; 2. 如果该记录是标记为已删除形成的新版本记录,则对该删除行为不可见,即可以看到删除前的内容。
5. 当无法通过4和5快速判断出记录的可见性时,则查找该记录的版本号是否在自己事务的read_view->trx_ids列表中,如果在则该记录的当前版本不可见,否则该记录的当前版本可见。
6. 当一条记录判断出其当前版本不可见时,通过记录的DB_ROLL_PTR(undo段指针),尝试去当前记录的undo段中提取记录的上一个版本进行4~6中同样的可见性判断,如果可以则该记录的上一个版本可见。
四、用例的正确解释
为了更好的检验上面新增的知识,对部分用例进行了适当的扩展。
4.1 用例1的正确解释
4.2 用例2的正确解释
4.3 用例3的正确解释
五、总结
要正确理解InnoDB RR隔离级别下MVCC的可见性控制逻辑,需注意补充如下关键知识:
1. 事务ID并非事务begin时分配,是延迟到需要分配时才分配的。
2. 事务在首次快照读时创建快照,并将快照版本的可见范围控制信息记录在read_view对象中。