以前整理过一篇事务隔离性的文章Innodb事务隔离性实现原理,你了解吗?,但很多同学看完后还是有些迷糊。最近和小团队构建学习计划,选择回答问题的方式,找一个题目,大家一起研究,效果很好。
把这道题拿出来分享,大家也可以检查自己是否了解隔离性。通过这道题目,能够掌握三部分内容:
- Undo log的作用
- 快照读和当前读
- 可见性规则
默认MySQL的隔离级别是可重复读。
题目
假设有表mvcccase,其中有两行记录,分别为(2,2)(3,3)
1 2 3 4 5 | CREATE TABLE `mvcccase` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', `value` bigint unsigned NOT NULL DEFAULT '0' COMMENT '值', PRIMARY KEY (`id`) ) ENGINE=InnoDB |
起两个事务,执行如下操作:
SessionA | SessionB |
---|---|
start transaction; | start transaction; |
select * from mvcccase; (1) | select * from mvcccase; (2) |
update mvcccase set id = 4 where id =2; (3) | |
select * from mvcccase; (4) | |
select * from mvcccase; (5) | |
commit;(6) | |
select * from mvcccase; (7) | |
update mvcccase set id=5 where id = 4; (8) | |
select * from mvcccase; (9) | |
commit;(10) | |
select * from mvcccase; (11) |
共11个操作,每个操作的结果,如果都能答对,则说明掌握的很好。
基础知识
为了回答上面的问题,先回忆一些核心知识点。
Undo log
在InnoDB redo、undo、binlog,是如何合作的中曾提到过Undo Log。对每行数据进行操作之前都会记录Undo Log,目的是能将数据进行回滚。
同一个事务对数据进行多次修改或者多个事务对同一个数据进行修改,这些修改会按照时间顺序连成链,所以通过undo log可以发现数据修改的历史。
其中对于一行数据,有隐藏的DB_TRX_ID字段,记录这行数据由哪个事务更改。
快照读、当前读
通过Undo Log我们知道,一行数据,可能有多个版本。
当前读意味读取的是最新版本,select lock in share mode(共享锁), select for update ; update, insert ,delete都都是当前读。
快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本,select是快照读。
可见性规则
事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
生成快照的算法比较简单,在生成快照的时候,生成三个数据,活跃事务的ID数组、低水位、高水位,低水位由数组最小值推导而出,高水位由最大事务ID+1计算而出。
如活跃id为9,12,14,当前最大事务id为16,则低水位为8,高水位为17,活跃事务数组为{9,12,14},如下图所示:
1.如果落在绿色部分,表示这个版本是已提交的事务,这个数据是可见的;
2.如果事务id是自己的值,表示数据是可见的;
3.如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
4.如果落在黄色部分,那就包括两种情况
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
如果当前的版本不可见,则根据链表寻找上一个版本,再次判断可见性规则,直到找到可见的数据。
解题
假设sessionA和sessionB的事务id分别为9,12,(2,2)(3,3)数值由事务6执行。
SessionA-9 | SessionB-12 |
---|---|
start transaction; | start transaction; |
select * from mvcccase; (1) (2,2)(3,3) | select * from mvcccase; (2) (2,2)(3,3) |
update mvcccase set id = 4 where id =2; (3) Rows matched: 1 Changed: 1 Warnings: 0 | |
select * from mvcccase; (4) (3,3)(4,2) | |
select * from mvcccase; (5) (2,2)(3,3) | |
commit;(6) | |
select * from mvcccase; (7) (2,2)(3,3) | |
update mvcccase set id=5 where id = 4; (8) Rows matched: 1 Changed: 1 Warnings: 0 | |
select * from mvcccase; (9) (2,2)(3,3)(5,2) | |
commit;(10) | |
select * from mvcccase; (11) (3,3)(5,2) |
1、2
(1)(2)是快照读,此时DB中的数据为:
根据可见性规则,事务9和12能够看到事务6的数据,所以显示结果都为(2,2)(3,3)。
3
事务12的更新操作,做了两件事情
- 将ID=2这行置为deleted
- 创建新的ID=4
此时DB中的数据格式为:
4
执行select后,显示(3,3)(4,2)。根据可见性规则,ID= 2、3、4的行对事务12都可见,但ID=2已被删除,所以只显示(3,3)(4,2)。
5
select仍显示(2,2)(3,3),虽然(3)执行完毕后,表中有三条数据,但根据可见性规则
- ID=2这条数据,最新的记录为事务12更改,对事务9不可见,所以进行回溯,找到事务6更改的版本,才可见
- ID=3这条数据,一直是事务6更改的版本,可见
- ID=4这条数据,只有事务12更改的版本,不可见
6
只有事务12提交后,其它事务才能更新ID=2、4,因为事务12对其加了写锁,其它事务想操作,会被阻塞。关于锁的部分,后面会找例子进行讲解。
7
仍显示(2,2)(3,3),虽然事务12进行了提交,但表中的数据没有变更。分析流程和操作5一致。
8
虽然事务7快照中没有显示ID=4这条数据,但update可以成功,这是为什么呢?
-
因为SessionB提交了,在ID=4上面的写锁已经释放。
-
因为update操作是当前读,看的是最新版本,自然能够修改ID=4的值。
事务7的更新操作,做了两件事情:
- 将ID=4这行置为deleted
- 创建新的ID=5
更新后,DB中的数据格式为:
9
显示(2,2)(3,3)(5,2),为什么会产生如此奇怪的结果?
现在表中有四条数据,根据可见性规则:
- ID=2这条数据,最新的记录为事务12更改,对事务9不可见,所以进行回溯,找到事务6更改的版本,才可见,值为(2,2)
- ID=3这条数据,一直是事务6更改的版本,可见
- ID=4这条数据,最新的是事务9更新的版本,虽然可见,但是因为已删除,所以不显示
- ID=5这条数据,最新的是事务9更新的版本,可见
10
sessionA提交之后,Undo Log也会被销毁,最终DB中的数据为:
11
此时进行的select,是一个新的事务,而且该事务ID比高水位还要高,所以表中的4行数据都可见,但因为有两行被删除,所以只显示(3,3)(5,2)。
总结
希望通过这个题目,能够帮助大家真正明白事务的隔离性。对于这类题目,主要靠三板斧:undo log、快照读与当前读、可见性规则,一旦掌握原理,就可以解答题目了。
最后留一个问题,如果更改的不是id,而是表中的value,每一步会显示怎样的结果呢?欢迎大家一起探讨。