文章总共分为五个部分:html
大而全版(五合一):InnoDB的锁机制浅析(All in One)mysql
这一章节只列举两种死锁场景,其余的死锁问题大多也万变不离其宗。sql
示例的基础是一个只有两列的数据库表。数据库
mysql> CREATE TABLE test ( id int(11) NOT NULL, code int(11) NOT NULL, PRIMARY KEY(id), KEY (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; mysql> INSERT INTO test(id,code) values(1,1),(10,10);
锁的兼容矩阵以下:并发
--- | 排它锁(X) | 意向排它锁(IX) | 共享锁(S) | 意向共享锁(IS) |
---|---|---|---|---|
排它锁(X) | N | N | N | N |
意向排它锁(IX) | N | OK | N | OK |
共享锁(S) | N | N | OK | OK |
意向共享锁(IS) | N | OK | OK | OK |
并发条件下,惟一键索引冲突可能会致使死锁,这种死锁通常分为两种,一种是rollback
引起,另外一种是commit
引起。ui
rollback
引起的Duplicate key死锁我命名为insert-insert-insert-rollback死锁rest
事务一 | 事务二 | 事务三 |
---|---|---|
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); Query OK, 1 row affected (0.01 sec) |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 执行以后被阻塞,等待事务一 |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 执行以后被阻塞,等待事务一 |
||
mysql>rollback; Query OK, 0 rows affected (0.00 sec) |
||
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
Query OK, 1 row affected (16.13 sec) |
当事务一执行回滚时,事务二和事务三发生了死锁。InnoDB的死锁检测一旦检测到死锁发生,会自动失败其中一个事务,所以看到的结果是一个失败另外一个成功。code
为何会死锁?htm
死锁产生的缘由是事务一插入记录时,对(2,2)记录加X锁,此时事务二和事务三插入数据时检测到了重复键错误,此时事务二和事务三要在这条索引记录上设置S锁,因为X锁的存在,S锁的获取被阻塞。
事务一回滚,因为S锁和S锁是能够兼容的,所以事务二和事务三都得到了这条记录的S锁,此时其中一个事务但愿插入,则该事务指望在这条记录上加上X锁,然而另外一个事务持有S锁,S锁和X锁互相是不兼容的,两个事务就开始互相等待对方的锁释放,形成了死锁。blog
事务二和事务三为何会加S锁,而不是直接等待X锁
事务一的insert语句加的是隐式锁(隐式的Record锁、X锁),可是其余事务插入同一行记录时,出现了惟一键冲突,事务一的隐式锁升级为显示锁。
事务二和事务三在插入以前判断到了惟一键冲突,是由于插入前的重复索引检查,此次检查必须进行一次当前读,因而非惟一索引就会被加上S模式的next-key锁,惟一索引就被加上了S模式的Record锁。
由于插入和更新以前都要进行重复索引检查而执行当前读操做,因此RR隔离级别下,同一个事务内不连续的查询,可能也会出现幻读的效果(但我的并不认为RR级别下也会出现幻读,幻读的定义应该是连续的读取)。而连续的查询因为都是读取快照,中间没有当前读的操做,因此不会出现幻读。
commit
引起的Duplicate key死锁delete-insert-insert-commit死锁
事务一 | 事务二 | 事务三 |
---|---|---|
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from test where id=2; Query OK, 1 row affected (0.01 sec) |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 执行以后被阻塞,等待事务一 |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 执行以后被阻塞,等待事务一 |
||
mysql>commit; Query OK, 0 rows affected (0.00 sec) |
||
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
Query OK, 1 row affected (2.37 sec) |
这种状况下产生的死锁和insert-insert-insert-rollback
死锁产生的原理一致。
通过以上分析,一条数据在插入时通过如下几个过程:
假设数据表test.test
中存在(1,1)、(5,5)和(10,10)三条记录。
select * from test where id>8 for update
,事务二要插入(9,9),此时先要获取插入意向锁,因为事务一已经在对应的记录和间隙上加了X锁,所以事务二被阻塞,而且阻塞的缘由是获取插入意向锁时被事务一的X锁阻塞。Duplicate key
的错误。若是存在惟一索引,且索引加锁,等待锁释放。仍然是表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
作了哪些加锁操做。
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锁释放。
两个事务互相等待对方先释放锁,所以出现死锁。
除了以上给出的几种死锁模式,还有不少其余死锁的场景。
不管是哪一种场景,万变不离其宗,都是因为某个区间上或者某一个记录上能够同时持有锁,例如不一样事务在同一个间隙gap上的锁不冲突;不一样事务中,S锁能够阻塞X锁的获取,可是不会阻塞另外一个事务获取该S锁。这样才会出现两个事务同时持有锁,并互相等待,最终致使死锁。
其中须要注意的点是,增、删、改的操做都会进行一次当前读操做,以此获取最新版本的数据,并检测是否有重复的索引。
这个过程除了会致使RR隔离级别下出现死锁以外还会致使其余两个问题: