【问题标题】:MySQL: A deadlock occurred when deleting the same rowMySQL:删除同一行时发生死锁
【发布时间】:2020-10-31 22:33:54
【问题描述】:

最近在删除记录的时候遇到了死锁(注意隔离级别是REPEATABLE READ,MySQL 5.7)

这里是复制步骤

1 创建一个新表

CREATE TABLE `t` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `p_name` (`name`)
) ENGINE=InnoDB CHARSET=utf8;

2 准备 3 条记录

insert into t (name) value ('A'), ('C'), ('D');

3

+====================================+============================================================+
|             Session A              |                         Session B                          |
+====================================+============================================================+
| begin;                             |                                                            |
+------------------------------------+------------------------------------------------------------+
|                                    | begin;                                                     |
+------------------------------------+------------------------------------------------------------+
| delete from t where name = 'C';    |                                                            |
+------------------------------------+------------------------------------------------------------+
|                                    | delete from t where name = 'C';  --Blocked!                |
+------------------------------------+------------------------------------------------------------+
| insert into t (name) values ('B'); |                                                            |
+------------------------------------+------------------------------------------------------------+
|                                    | ERROR 1213 (40001): Deadlock found when trying to get lock |
+------------------------------------+------------------------------------------------------------+

show engine innodb status的结果如下图(LATEST DETECTED DEADLOCK部分)

LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 3631, ACTIVE 21 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 13, OS thread handle 123145439432704, query id 306 localhost root updating
delete from t where name = 'C'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3631 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 1; hex 43; asc C;;
 1: len 8; hex 8000000000000018; asc         ;;

*** (2) TRANSACTION:
TRANSACTION 3630, ACTIVE 29 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 14, OS thread handle 123145439711232, query id 307 localhost root update
insert into t (name) values ('B')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 1; hex 43; asc C;;
 1: len 8; hex 8000000000000018; asc         ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 1; hex 43; asc C;;
 1: len 8; hex 8000000000000018; asc         ;;

如 Innodb 状态所示,会话 B 正在等待下一个键锁 C,会话 A 持有记录锁 C 并在 C 上等待间隙锁;


众所周知

DELETE FROM ... WHERE ... 在搜索遇到的每条记录上设置一个排他的下一个键锁定

next-key 锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。

Q1:我猜想如果会话B首先获得间隙锁(next-key的一部分),然后等待记录锁。因此,会话 A 中的后者插入被会话 B 阻塞(由于间隙锁),并最终导致死锁。对吧?

Q2:当 C 从索引中清除时,会话 B 持有的间隙锁应该是 ('A', 'D') 吗?如果是这样,为什么会话 A 正在等待范围 (, 'C') 上的插入内涵锁定?

Q3:为什么会话B有1 row lock(s),而会话A有4 row lock(s)


Q4:当将索引p_name 更改为唯一索引时,我们仍然会因为间隙锁而出现死锁,这很奇怪。它的行为与官方doc 不同,后者声明只需要记录锁。

DELETE FROM ... WHERE ... 在搜索遇到的每条记录上设置一个排他的下一个键锁。但是,使用唯一索引锁定行以搜索唯一行的语句只需要一个索引记录锁


但是,使用主键id执行删除是可以的(步骤如下图)。这是 MySQL 的错误吗?

1 准备数据

delete from t;
insert into t (id, name) value (1, 'A'), (3, 'C'), (5, 'D');

2

+-------------------------------------------+--------------------------------------+
|                 Session A                 |              Session B               |
+-------------------------------------------+--------------------------------------+
| begin;                                    |                                      |
|                                           | begin;                               |
| delete from t where id = 3;               |                                      |
|                                           | delete from t where id = 3; Blocked! |
| insert into t (id, name) values (2, 'B'); |                                      |
|                                           |                                      |
| commit;                                   |                                      |
+-------------------------------------------+--------------------------------------+

【问题讨论】:

  • 我不知道细节,但似乎在“间隙”中搞乱会导致比看起来最佳的锁更糟糕。在这种情况下,“间隙”可能是“A”和“D”之间的所有范围。忍受它。编写代码以重新启动事务。第二次尝试可能不会出现死锁。

标签: mysql database transactions innodb database-deadlocks


【解决方案1】:

从事务 3631 的“等待授予此锁”部分,我们可以看到:

RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3631 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
  1. 3631 正在等待记录锁。对应的索引内容为{"name":"C", "id": 24}。
  2. 索引名称是表 t 中的 p_name。
  3. 锁的模式是“lock_mode X”

从事务 3630 的“等待授予此锁”部分,我们可以看到:

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 1; hex 43; asc C;;
 1: len 8; hex 8000000000000018; asc         ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 69 page no 4 n bits 72 index p_name of table `jacky`.`t` trx id 3630 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 1; hex 43; asc C;;
 1: len 8; hex 8000000000000018; asc         ;;
  1. 3630 正在等待记录锁。对应的索引内容为{"name":"C", "id":24}。等待锁的模式是“lock_mode X locks gap”
  2. 3630 持有记录锁。对应的索引内容为{"name":"C", "id":24}。持有锁的模式是“lock_mode X locks”
  3. 索引名称是表 t 中的 p_name。
  4. 此死锁是由执行“插入 t (name) values ('B')”引起的

根据您的重现步骤,会话 A 将首先发送delete from t where name = 'C';,这将锁定:

  1. ('A', 'C'] and ('C', 'D'):下一个键锁'C'和'D'之前的间隙锁;

DELETE FROM ... WHERE ... 为每个 记录搜索遭遇。但是,只有索引记录锁是 对于使用唯一索引锁定行进行搜索的语句来说是必需的 为唯一的行。

  1. 为'C'对应的主索引id添加一个记录锁。这里的 id 值应该是“26”。

然后会话 B 将启动,delete from t where name = 'C'; 将再次执行。然而。对于会话 B,由于会话 A 尚未提交,'C' 已被会话 A 锁定。但是,如果执行删除 sql,会话 B 将尝试按以下顺序添加锁定:

  1. “C”之前的间隙锁:成功,因为 innodb 可以在同一位置添加多个间隙锁。
  2. 记录锁“C”:阻塞,因为会话 A 持有该锁。会话 B 必须等待会话 A 释放它。
  3. “D”之前的间隙锁定:

最后,会话 A 发送insert into t (name) values ('B');。对于表t,有2个索引,分别是idnameid 是一个自增主整数键,对于name,这个sql会尝试添加一个插入意向锁。但是,会话 B 持有一个间隙锁,因此会话 A 必须等待会话 B 释放该间隙锁。现在我们可以看到这个死锁是如何发生的。 Innodb 将根据成本选择一个会话进行回滚。这里会话 B 将被回滚。

对于第一季度,答案是肯定的。 实际上,对于第二季度,已删除的记录在会话提交之前不会从索引中清除。 对于Q3,行锁号等于trx_rows_locked,在mysql网站中,其:

TRX_ROWS_LOCKED

此事务锁定的近似数或行数。价值 可能包括实际存在但不存在的带删除标记的行 对事务可见。

从这个article,我们可以知道:

  1. 对于非聚集唯一索引过滤,由于需要返回表,过滤行数锁定为唯一索引加 返回的行数。

  2. 对于非聚集非唯一索引过滤,会涉及到间隙锁,因此会锁定更多记录。

因此,在会话 A 中删除后 trx_rows_locked(间隙锁 + 下一键锁 + 返回表)为 3。尝试插入后,最终 trx_rows_locked 值应为 3 + 1(插入键锁)。


以下是新的更新问题: 我之前没有注意到删除主键和唯一的辅助键。

经过一番调查,我发现:

  1. 当删除一个primary key,该primary key已经被删除但尚未提交,新的删除操作将只需要record lock而不是next-key lock。
  2. 删除secondary unique key(已删除但尚未提交)时,新的删除操作将需要next-key lock

您可以使用set GLOBAL innodb_status_output_locks=ON; show engine innodb status 来查看正在运行的事务的详细锁定状态。

【讨论】:

  • For session B, because of MVCC in mysql, 'C' could be seen at that moment. ==> 我认为可以看到“C”不是因为 MVCC(会话 B 可以看到它,因为会话 A 尚未提交删除。)。在我看来,MVCC 适用于一致性读取,对于 UPDATE/DELETE 语句,InnoDB 执行“半一致性”读取,以便将最新提交的版本返回给 MySQL。
  • @Jacky1205 是的,你是对的。 UPDATE/DELETE 语句将读取最新提交的数据版本。我会更新我的答案。
  • 当我将索引p_name 更改为唯一索引时,我仍然使用相同的步骤陷入僵局。为什么? mysql 文档已经说过 > 使用唯一索引锁定行以搜索唯一行的语句只需要一个索引记录锁
  • 对于mysql doc中的内容,“使用唯一索引锁定行以搜索唯一行的语句只需要索引记录锁”,这是正确的。但是,在您的情况下,会话 B 中的删除操作不仅仅是“搜索唯一行”。它是“搜索标记为已删除的唯一行。”。在这种情况下,mysql会添加gap lock。如果将 Session A 中的 delete 操作更改为 update 操作,会发现 session B 中的 delete 操作仍然会被阻塞,但是在 Session A 中插入后不会出现死锁。
  • 我找到了一条语句如果这样的记录被删除标记,那么它可能会被清除,并且 lock_rec_inheirt_to_gap 将被调用来决定它的每个锁的命运:要么是作为间隙锁继承,或丢弃。Line 2382, lock0lock.cc 源文件中。但是,我不清楚何时将作为间隙锁继承(例如使用辅助唯一键?)或丢弃(使用主键)的确切规则?
【解决方案2】:

Q4,终于找到a comment in MySQL 5.7 source code,解释了为什么要使用next-key locks,仅供参考。

在索引中最多匹配一条记录的搜索中,我们 锁定一个记录时可以使用 LOCK_REC_NOT_GAP 类型的记录锁 非删除标记的匹配记录。

请注意,在唯一的二级索引中可能有不同的 只有主键的记录的删除标记版本 值不同:因此在二级索引中我们必须使用 next-key 锁定删除标记的记录时锁定

请注意,唯一的二级索引可以包含许多 如果其中一列是 SQL,则具有相同键值的行 空值。 MySQL下的聚集索引永远不能包含null 列,因为我们要求主键中的所有列 是非空的。

【讨论】:

  • 干得好!这里解释一切! a UNIQUE secondary index can contain many rows with the same key value if one of the columns is the SQL null
猜你喜欢
  • 1970-01-01
  • 2017-02-27
  • 2020-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-07
  • 1970-01-01
相关资源
最近更新 更多