InnoDB的锁机制浅析(五)—死锁场景(Insert死锁)

可能的死锁场景

文章总共分为五个部分: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

1 Duplicate key error引起的死锁

并发条件下,惟一键索引冲突可能会致使死锁,这种死锁通常分为两种,一种是rollback引起,另外一种是commit引起。ui

1.1 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级别下也会出现幻读,幻读的定义应该是连续的读取)。而连续的查询因为都是读取快照,中间没有当前读的操做,因此不会出现幻读。

1.2 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死锁产生的原理一致。

6.2 数据插入的过程

通过以上分析,一条数据在插入时通过如下几个过程:
假设数据表test.test中存在(1,1)、(5,5)和(10,10)三条记录。

  • 事务开启,尝试获取插入意向锁。例如,事务一执行了select * from test where id>8 for update,事务二要插入(9,9),此时先要获取插入意向锁,因为事务一已经在对应的记录和间隙上加了X锁,所以事务二被阻塞,而且阻塞的缘由是获取插入意向锁时被事务一的X锁阻塞。
  • 获取意向锁以后,插入以前进行重复索引检查。重复索引检查为当前读,须要添加S锁。
  • 若是是已经存在惟一索引,且索引未加锁。直接抛出Duplicate key的错误。若是存在惟一索引,且索引加锁,等待锁释放。
  • 重复检查经过以后,加入X锁,插入记录

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 waitinglock_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锁释放。

两个事务互相等待对方先释放锁,所以出现死锁。

2 总结

除了以上给出的几种死锁模式,还有不少其余死锁的场景。
不管是哪一种场景,万变不离其宗,都是因为某个区间上或者某一个记录上能够同时持有锁,例如不一样事务在同一个间隙gap上的锁不冲突;不一样事务中,S锁能够阻塞X锁的获取,可是不会阻塞另外一个事务获取该S锁。这样才会出现两个事务同时持有锁,并互相等待,最终致使死锁。

其中须要注意的点是,增、删、改的操做都会进行一次当前读操做,以此获取最新版本的数据,并检测是否有重复的索引。
这个过程除了会致使RR隔离级别下出现死锁以外还会致使其余两个问题:

  • 第一个是可重复读可能会由于此次的当前读操做而中断,(一样,幻读可能也会所以产生);
  • 第二个是其余事务的更新可能会丢失(解决方式:悲观锁、乐观锁)。
相关文章
相关标签/搜索