可以看到,可重复读级别下 update 的加锁与读提交不太一样,加锁的 lock_data 是 1,说明事务 G 扫描的 id 为 1 的记录之后没有释放锁。
如果把事务G、H 的启动顺序反过来,也就是先执行 H 的语句再执行 G 的语句,结果也是一样的,同样加锁的 lock_data 是 1,这说明可重复读的 update 不是先判断条件是否符合再上锁,而是先上锁再判断条件是否符合。
update 都会被阻塞,最终结论就是:
在可重复读级别下,加锁非索引列导致的全表记录上锁会使得所有插入和修改都会被阻塞。
小结一下:
此时把读者问题列上:
留言的回答语境是在可重复读级别下,现在我再来总结回答下:
在读提交级别下:
如果锁定的列为非索引列,加锁都是加到主键索引上的,select ..for update
的加锁的顺序是从前往后全表扫描的顺序,遍历的记录先上锁,上锁之后发现不满足条件,则释放锁,然后继续往后遍历,直到全表扫描结束。
insert 都不会被阻塞。
而 update 其它字段值,其实也是找记录,如果找到的记录已经被上锁了,那么就会阻塞,如果找到的记录没有被锁则不会被阻塞。
在可重复读级别下:
如果锁定的列为非索引列,加锁都是加到主键索引上的,select ..for update
的加锁的顺序是从前往后全表扫描的顺序,遍历的记录先上锁,上锁之后发现不满足条件,则不会释放锁,然后继续往后遍历,直到全表扫描结束。
所以只要有一个全表扫描的加锁,则 insert 的时候就会被阻塞。
update 加锁和select ..for update
一致。
与之相关的还有一个问题:
图里已经有答案了,包括前面的截图也可以看到所有的 lock_type 都是 RECORD ,也就是行级锁。
实验三:隔离级别为读提交,锁定索引列的实验
此时在 name 列建立索引。
CREATE TABLE `yes` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, `address` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
同样准备数据如下:
可以看到,不会被阻塞,丝滑。
这个结果符合认知,因为此时 name 已经有索引了,在读提交级别下,只会在 name 索引上加相关记录的锁,而不会加全表行锁,因此事务 A、B 之间不会被阻塞。
此时再起一个事务 C,执行如下语句:
可以看到,锁的索引确实变成了 idx_name,lock_data 显示锁的是 yes 这个记录,id 为 1。
从结果看:在可以命中二级索引的情况下,锁的是对应的二级索引。
我们继续做实验。
将上面所有事务提交之后。
启动事务 C 执行以下语句,且未提交事务:
执行 name 一样的插入,也不会阻塞。
所以在读提交级别下,对插入都不会产生阻塞。
关于 update 我就不实验了,和实验一的差别就是加锁索引换成了 name 的索引,其他表现一致。
实验四:隔离级别为可重复读,锁定索引列的实验
同样准备数据如下:
这是预期之内的阻塞,因为按照 name 为索引,yes这条记录是排在最后的(字母序),为了防止幻读,可重读隔离级别下会在对应记录前后加入间隙锁,而新的记录的插入恰巧需要排 yes 这条记录的后面。
但是从截图结果来看此时lock_mode是记录锁,且 lock_data 是 supremum,这又涉及到我的盲区了,难道是最后的记录插入比较特殊?所以不是因为间隙锁被阻塞,而是被最大记录行锁阻塞?
此时把事务A、B都提交了 ,然后我们再执行事务 C:
可以看到,此时被阻塞的锁是记录锁+间隙锁(next-key lock),这符合我们的认知和上面的图,因为要插入的数据在 yes 和公众号:yes的练级攻略之间。
小结
在命中索引列的前提下,只会在索引列上加锁。
如果此时在读已提交级别下:
select..for update和update
的所查找的记录本身会被加上记录锁,因此这个位置的插入会被阻塞,其他位置的插入则没有影响。
如果此时在可重复读级别下:
select..for update和update
的所查找的记录在索引位置前后会被加间隙锁,记录本身加记录锁,因此这些位置的插入会被阻塞,其他位置的插入则没有影响。
最后
分了四个实验大类,一个做了十三个实验。
还是挺有收获的,惊喜就是发现了细节盲区,之后研究一下再出一篇文章。
从实验来看,这里再做个概念性的总结:
- 锁是作用在索引上的,因此如果能命中二级索引就在二级索引上加锁,不然就得被迫在聚簇索引上加锁。
- 被迫在聚簇索引上加锁,会导致全表扫描式的加锁。
- 在可重复读下,不论命中哪个索引,不论是
select..for update
还是update
,只要被扫描到的记录,都会被加锁,不论是否符合条件,在事务提交之后才会释放。 - 在读提交下,select..for update表现出来的结果是扫描到的记录先加锁,再判断条件,不符合就立马释放,不需要等到事务提交,而 update 的扫描是先判断是否符合条件,符合了才上锁。
声明:以上实验是基于 MySQL 5.7.26 版本,存储引擎为 InnoDB 。
这些实验我之前花了三个工作日晚上做的,由于时间是零散的,导致中间实验出错,期间设置事务隔离级别语句有问题,导致我在错误的前提下做实验,实验结果不断地冲击我的认知,我整个人都快搞崩溃了....
然后周六花了一天的时间重新理了一下,实验图很多,可能看了后面就忘了前面,建议结合着结论来回看,这样对结论会有更深刻的认识,但是有些实验结论我是根据实验现象来推断的,我没有去找相关的官网说明,如有错误,恳请指正,如有疑惑还请自行实验,可以在评论区交流一番。