两个INSERT发生死锁原因剖析

xiaoxiao2021-02-28  14

导读

两个INSERT也能发生死锁?貌似不可思议,实际上是正常的。

本文整理过程中,先后向高鹏、王少华、苏斌等几位朋友请教确认,感谢。

开始之前,关于锁、死锁,我们要先统一下几点认知:

死锁是由于多个事务相互持有其他事务所需要的锁,结果导致事务都无法继续,进而触发死锁检测,其中某个事务会被回滚,释放相应的锁,其他事务得以正常继续;简言之,就是多个事务之间的锁等待产生了回路,死循环了;

死锁发生时,会立刻被检测到,并且回滚其中某个事务,而不会长时间阻塞、等待;

从MySQL 5.7.15开始,新增选项 innodb_deadlock_detect,没记错的话应该是阿里团队率先实现的。当它设置为 OFF 时(默认值是 ON),InnoDB会不检测死锁,在高并发场景(例如“秒杀”)业务中特别有用,可以有效提高事务并发性能;

在启用死锁检测时,InnoDB默认的最大检测深度为200,在上面提到的高并发高竞争场景下,在热点数据上的锁等待队列可能很长,死锁检测代价很大。或者当等待队列中所有的行锁总数超过 100万 时,也会被认为认为发生死锁了,直接触发死锁检测处理机制;

InnoDB行锁等待超时默认为50秒,一般建议设置5-10秒就够了;

有时候,可能会口误把 长时间的行锁等待 说成是 死锁,其实二者完全不一样,不要犯这种常识性口误。

好了,正式开始今天的案例。

先看测试表DDL:

yejr@imysql.com [yejr]>show create table d\G ********************** 1. row **********************       Table: d Create Table: CREATE TABLE `d` (  `i` int(11) NOT NULL DEFAULT '0',  PRIMARY KEY (`i`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; yejr@imysql.com [yejr]>select * from d; +---+ | i | +---+ | 1 | +---+

然后我们执行下面的测试:

session1session2session3begin;delete from d where i=1;begin;insert into d select 1;begin;insert into d select 1;commit;Query OK, 1 row affected (11.82 sec)Records: 1 Duplicates: 0 Warnings: 0ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

这时候我们看下InnoDB STATUS的输出:

------------------------ LATEST DETECTED DEADLOCK ------------------------ 2017-09-02 14:59:08 0x700004208000 *** (1) TRANSACTION: TRANSACTION 274616, ACTIVE 12 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 16, OS thread handle 123145373167616, query id 398 localhost root executing insert into d select 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 510 page no 3 n bits 72 index PRIMARY of table `yejr`.`d` trx id 274616 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 4; hex 80000001; asc     ;; 1: len 6; hex 0000000430b3; asc     0 ;; 2: len 7; hex 3b0000018027a4; asc ;    ' ;; *** (2) TRANSACTION: TRANSACTION 274617, ACTIVE 4 sec inserting mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 18, OS thread handle 123145371549696, query id 400 localhost root executing insert into d select 1 *** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 510 page no 3 n bits 72 index PRIMARY of table `yejr`.`d` trx id 274617 lock mode S(想想,哪里冒出来的S锁?)Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 4; hex 80000001; asc     ;; 1: len 6; hex 0000000430b3; asc     0 ;; 2: len 7; hex 3b0000018027a4; asc ;    ' ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 510 page no 3 n bits 72 index PRIMARY of table `yejr`.`d` trx id 274617 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 4; hex 80000001; asc     ;; 1: len 6; hex 0000000430b3; asc     0 ;; 2: len 7; hex 3b0000018027a4; asc ;    ' ;; *** WE ROLL BACK TRANSACTION (2)

从上面这个输出来看,我们看到的现场是两个 insert 请求发生了死锁。单纯看这2个SQL的话,应该是产生锁等待才对,而不是死锁。

按照我们常规理解,session1 未 commit 前,应该是持有 i=1 上的record lock(X),而session2 和 session3 则都在等待这个锁的释放。而实际上呢,肯定不是这样的,否则也不至于发生死锁了。

关于InnoDB行锁更详细的知识点我们以后找时间再说。这次的案例其实在MySQL官方文档上已经解释过了,而且也给了演示案例(如本例)。文档中是这么说的:

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row. Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6 each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting. 【敲黑板、划重点】If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock. This can occur if another session deletes the row.

划重点的核心内容是:当需要进行唯一性冲突检测时,需要先加一个 S 锁。

这样的话,上面案例的加锁过程就不是之前推测的那样,而是像下面这样了:

session1session2session3begin;delete from d where i=1;持有i=1的record lock(X)begin;insert into d select 1;需要判断唯一性,检测到冲突,请求i=1的next-key lock(S)被阻塞,等待ingbegin;insert into d select 1;需要判断唯一性,检测到冲突,请求i=1的next-key lock(S)被阻塞,等待ingcommit;提交,释放i=1上的锁

成功获取i=1的next-key lock(S);

请求i=1的record lock(X)锁;

但session3上已持有i=1的next-key(S),被阻塞、等待中;

后面session3检测到死锁冲突后,session2才insert成功;

Query OK, 1 row affected (11.82 sec)Records: 1 Duplicates: 0 Warnings: 0

成功获取i=1的next-key lock(S);

请求i=1的record lock(X)锁;

触发死锁检测,失败、回滚;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

下面是另一个类似的案例:

session1session2begin;

select * from d where i = 1 lock in share mode;

持有i=1上的record lock(S)

begin;

select * from d where i = 1 lock in share mode;

持有i=1上的record lock(S)

delete from d where i = 1;请求i=1上的record lock(X),被session2阻塞了,等待中delete from d where i = 1;请求i=1上的record lock(X),检测到死锁,失败,回滚

通过上面这两个案例,其实想要告诉大家的是:发生死锁时,不能只看现场,还得分析过程,才能知道真正的原因,死锁发生的原因也并不复杂,但是得能想办法还原过程。

顺手安利下,业界最好的知数堂MySQL DBA课程最新一期已发车,想在MySQL方面深入学习的同学请快上车咯,叶师傅亲自授课哟~

参考

14.5.3 Locks Set by Different SQL Statements in InnoDB

MySQL发生死锁肿么办?

和叶师傅**的正确姿势

知识无界限,不再加原创

喜欢就转走,铁粉加密圈

好铁观音尽在

「老叶茶馆」

http://yejinrong.com

转载请注明原文地址: https://www.6miu.com/read-1250146.html

最新回复(0)