事务开启,尝试获取插入意向锁。例如,事务一执行了select * from test where id>8 for update,事务二要插入(9,9),此时先要获取插入意向锁,由于事务一已经在对应的记录和间隙上加了X锁,因此事务二被阻塞,并且阻塞的原因是获取插入意向锁时被事务一的X锁阻塞。
获取意向锁之后,插入之前进行重复索引检查。重复索引检查为当前读,需要添加S锁。
如果是已经存在唯一索引,且索引未加锁。直接抛出Duplicate key的错误。如果存在唯一索引,且索引加锁,等待锁释放。
重复检查通过之后,加入X锁,插入记录
6.3 GAP与Insert Intention冲突引发死锁 update-insert死锁仍然是表test,当前表中的记录如下:
mysql> select * from test; +----+------+ | id | code | +----+------+ | 1 | 1 | | 5 | 5 | | 10 | 10 | +----+------+ 3 rows in set (0.01 sec) 事务一 事务二begin; begin;
select * from test where id=5 for update; select * from test where id=10 for update;
insert into test values(7,7);
insert into test values(7,7);
Query OK, 1 row affected (5.03 sec)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
使用show engine innodb status查看死锁状态。先后出现lock_mode X locks gap before rec insert intention waiting和lock_mode X locks gap before rec字眼,是gap锁和插入意向锁的冲突导致的死锁。
回顾select...for update的加锁范围首先回顾一下两个事务中的select ... for update做了哪些加锁操作。
code=5时,首先会获取code=5的索引记录锁(Record锁),根据之前gap锁的介绍,会在前一个索引和当前索引之间的间隙加锁,于是区间(1,5)之间被加上了X模式的gap锁。除此之外RR模式下,还会加next-key锁,于是区间(5,10]被加了next-key锁;
因此,code=5的加锁范围是,区间(1,5)的gap锁,{5}索引Record锁,(5,10]的next-key锁。即区间(1,10)上都被加上了X模式的锁。
同理,code=10的加锁范围是,区间(5,10)的gap锁,{10}索引Record锁,(10,+∞)的next-key锁。
由gap锁的特性,兼容矩阵中冲突的锁也可以被不同的事务同时加在一个间隙上。上述两个select ... for update语句出现了间隙锁的交集,code=5的next-key锁和code=10的gap锁有重叠的区域——(5,10)。
死锁的成因当事务一执行插入语句时,会先加X模式的插入意向锁,即兼容矩阵中的IX锁。
但是由于插入意向锁要锁定的位置存在X模式的gap锁。兼容矩阵中IX和X锁是不兼容的,因此事务一的IX锁会等待事务二的gap锁释放。
事务二也执行插入语句,与事务一同样,事务二的插入意向锁IX锁会等待事务一的gap锁释放。
两个事务互相等待对方先释放锁,因此出现死锁。
7 总结除了以上给出的几种死锁模式,还有很多其他死锁的场景。
无论是哪种场景,万变不离其宗,都是由于某个区间上或者某一个记录上可以同时持有锁,例如不同事务在同一个间隙gap上的锁不冲突;不同事务中,S锁可以阻塞X锁的获取,但是不会阻塞另一个事务获取该S锁。这样才会出现两个事务同时持有锁,并互相等待,最终导致死锁。
其中需要注意的点是,增、删、改的操作都会进行一次当前读操作,以此获取最新版本的数据,并检测是否有重复的索引。
这个过程除了会导致RR隔离级别下出现死锁之外还会导致其他两个问题:
第一个是可重复读可能会因为这次的当前读操作而中断,(同样,幻读可能也会因此产生);
第二个是其他事务的更新可能会丢失(解决方式:悲观锁、乐观锁)。