在之前的博客中,我写了一系列的文章,比较系统的学习了 MySQL 的事务、隔离级别、加锁流程以及死锁,我自认为对常见 SQL 语句的加锁原理已经掌握的足够了,但看到热心网友在评论中提出的一个问题,我还是彻底被问蒙了。
他的问题是这样的:
加了插入意向锁后,插入数据之前,此时执行了 select...lock in share mode 语句(没有取到待插入的值),然后插入了数据,下一次再执行 select...lock in share mode(不会跟插入意向锁冲突),发现多了一条数据,于是又产生了幻读。会出现这种情况吗?
这个问题初看上去很简单,在 RR 隔离级别下,假设要插入的记录不存在,如果先执行 select...lock in share mode
语句,很显然会在记录间隙之间加上 GAP 锁,而 insert
语句首先会对记录加插入意向锁,插入意向锁和 GAP 锁冲突,所以不存在幻读;如果先执行 insert
语句后执行 select...lock in share mode
语句,由于 insert
语句在插入记录之后,会对记录加 X 锁,它会阻止 select...lock in share mode
对记录加 S 锁,所以也不存在幻读。两种情况如下所示:
先执行 INSERT 后执行 SELECT:
先执行 SELECT 后执行 INSERT:
但是我们仔细想一想就会发现哪里有点不对劲,我们知道 insert
语句会先在插入间隙上加上插入意向锁,然后开始写数据,写完数据之后再对记录加上 X 记录锁。
那么问题就来了,如果在 insert
语句加插入意向锁之后,写数据之前,执行了 select...lock in share mode
语句,这个时候 GAP 锁和插入意向锁是不冲突的,查询出来的记录数为 0,然后 insert
语句写数据,加 X 记录锁,因为记录锁和 GAP 锁也是不冲突的,所以 insert
成功插入了一条数据,这个时候如果事务提交,select...lock in share mode
语句再次执行查询出来的记录数就是 1,岂不是就出现了幻读?
最新面试题整理好了,点击Java面试库小程序在线刷题。
整个流程如下所示(我们把 insert
语句的执行分成两个阶段,INSERT 1 加插入意向锁,还没写数据,INSERT 2 写数据,加记录锁):
一、INSERT 加锁的困惑
在得出上面的结论时,我也感到很惊讶。按理是不可能出现这种情况的,只可能是我对这两个语句的加锁过程还没有想明白。