为啥并发的“删除...插入”语句会导致死锁?
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 中使用无缓冲通道会导致死锁?