为啥并发的“删除...插入”语句会导致死锁?

Posted

技术标签:

【中文标题】为啥并发的“删除...插入”语句会导致死锁?【英文标题】:Why concurrent "Delete...Insert" statements cause a deadlock?为什么并发的“删除...插入”语句会导致死锁? 【发布时间】:2019-10-22 07:47:32 【问题描述】:

mysql 中考虑以下架构:

create table foo(
  id int not null primary key auto_increment,
  name varchar(32) not null,
  unique key(name)
);

并且表中有一条名为“abc”的记录。

我有一笔交易(RC):

start transaction;
delete from foo where name = "abc";
insert into foo(name) values("abc");
commit;

如果有两个并发事务,就会发生死锁。

       |        TX A         |             TX B
---------------------------------------------------------------------
Step 1 | start transaction;  | 
       | delete name="abc";  |
---------------------------------------------------------------------
Step 2 |                     | start transaction;
       |                     | delete name="abc";
       |                     | <wait for lock>
---------------------------------------------------------------------
Step 3 | insert name="abc";  | <deadlock detected, exit>
---------------------------------------------------------------------
Step 4 | commit;             |
---------------------------------------------------------------------

我想知道为什么这个序列会导致死锁。

在 mysql 文档中说 (https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html)

如果发生重复键错误,重复索引上的共享锁 记录被设定。这种共享锁的使用可能会导致死锁 如果另一个会话试图插入同一行,则有多个会话 session 已经有一个独占锁。如果另一个会话可能会发生这种情况 删除行。

我想当事务A运行“delete”语句时,它已经获得了记录“abc”的X锁。当“insert”语句执行时,由于“重复键错误”,它试图获取 S 锁。既然获得了同一条记录的X锁,它不应该获得S锁吗?为什么这里会发生死锁?

【问题讨论】:

对于delete ....,我假设你总是指delete from foo where name = "abc";,对于insert ...,我假设你总是指insert into foo(name) values("abc"); DELETE FROM ... WHERE ... 在搜索遇到的每条记录上设置一个独占的 next-key 锁定。但是,使用唯一索引锁定行以搜索唯一行的语句只需要一个索引记录锁 是的,你的假设是正确的。 TX A 持有因为DELETE FROM ... WHERE ..."abc" 上的唯一索引锁定.. TX B 也尝试使用“abc”删除但事务TX A“无效”@987654335 @ delete transaction state when TX A 用“abc”插入是什么原因TX B死锁,因为TX A拥有“abc”的权利(或多或少,因为它被简化了一点) 【参考方案1】:

我重现了死锁,得到了innoDB状态日志如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-10-18 18:35:14 0x7f1dfc738700
*** (1) TRANSACTION:
TRANSACTION 26547965, ACTIVE 6 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
/* ApplicationName=DataGrip 2019.1.1 */ delete from foo where name='abc'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547965 lock_mode X locks rec but not gap waiting
Record lock, heap no 153 PHYSICAL RECORD: n_fields 2; ....

*** (2) TRANSACTION:
TRANSACTION 26547960, ACTIVE 10 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 2
/* ApplicationName=DataGrip 2019.1.1 */ INSERT INTO foo(id, name)
VALUES (1, 'abc')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547960 lock_mode X locks rec but not gap
Record lock, heap no 153 PHYSICAL RECORD: ...

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3011 page no 4 n bits 224 index IDX_NAME of table `foo` trx id 26547960 lock mode S waiting
Record lock, heap no 153 PHYSICAL RECORD: ....

*** WE ROLL BACK TRANSACTION (1)

日志解释清楚了原因,TX B 等待 TX A 持有的 X 锁,同时 TX A 等待被 TX B 的锁请求阻塞的 S 锁。

根据 Mysql 文档:

如果发生重复键错误,则会在重复索引记录上设置共享锁。如果另一个会话已经拥有排他锁,那么如果有多个会话尝试插入同一行,则使用共享锁可能会导致死锁。 "

Insert语句在某个时刻确实会获得S锁,所以死锁发生的原因就很清楚了。

但问题是:

根据 mysql 文档,如果发生重复键错误,insert 语句将获取 S 锁,这在我们讨论的当前情况下不会发生 为什么当当前事务已经持有X锁时insert语句仍然获取S锁,X锁足以进行当前读取以检查重复键错误。那么它有什么用呢?

【讨论】:

以上是关于为啥并发的“删除...插入”语句会导致死锁?的主要内容,如果未能解决你的问题,请参考以下文章

同一张表上的两个“SELECT FOR UPDATE”语句会导致死锁吗?

为啥在同一个 goroutine 中使用无缓冲通道会导致死锁?

mysql insert into select 语句为啥会造成死锁

MySQL Innodb表导致死锁日志情况分析与归纳

mysql 发生死锁问题请求帮助

SQL 更新导致死锁