聊一聊 insert on duplicate key update 下的死锁场景

理论上,insert on duplicate key 的执行过程中加锁顺序一致,并且对唯一索引加Record Lock 不存在唯一索引的风险,但是生产中近来出现了多次该场景下的DEADLOCK,在这里借着这个场景聊聊一些知识点以及排查问题的思路,无论是实操还是面试这篇文章都是必读的干货

 

为什么会发生死锁?

所有的死锁都来源于资源的争抢,A、B两个线程 A持有的“甲号”资源等待“乙号”资源,而B持有了“乙号”资源等待“甲号”资源。在程序设计上我们通常使用确保加锁顺序一致的方式来防止发生死锁,但是现实生产中各种权衡下很难用一句话的方案解决多个场景下的复杂问题。

MySQL 5.7.2 为什么单一SQL语句的并发执行会产生死锁?:

由于安全问题,我不在这里贴生产环境出问题的场景了,直接复用MySql官方bug的一个场景,其上下文几乎完全一致:

CREATE TABLE `tt` (
  `a` bigint NOT NULL AUTO_INCREMENT,
  `b` bigint DEFAULT NULL,
  `c` bigint DEFAULT NULL,
  `d` bigint DEFAULT NULL,
  PRIMARY KEY (`a`),
  UNIQUE KEY `blah` (`b`,`c`)
)

T1: INSERT ... (b,c,d) VALUES (1, rand, rand),(1, rand, rand) ON DUPLICATE KEY UPDATE d=VALUES(d)
T2: INSERT ... (b,c,d) VALUES (2, rand, rand),(2, rand, rand) ON DUPLICATE KEY UPDATE d=VALUES(d)

死锁日志:

LATEST DETECTED DEADLOCK
------------------------
2021-05-04 10:53:24 0x7f2e1b4ee700
*** (1) TRANSACTION:
TRANSACTION 2310041, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 291, OS thread handle 139847773697792, query id 768872 localhost local_admin:sys.database update
INSERT INTO S230677 (b, c, d) VALUES (2697564,509247.782987144,1620150804984388.8),(2697564,680856.9544766529,1620150804984389.8) ON DUPLICATE KEY UPDATE d=VALUES(d)

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 34703 n bits 304 index PRIMARY of table `test`.`S230677` trx id 2310041 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 34703 n bits 304 index PRIMARY of table `test`.`S230677` trx id 2310041 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) TRANSACTION:
TRANSACTION 2310042, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 290, OS thread handle 139847812486912, query id 768873 localhost local_admin:sys.database update
INSERT INTO S230677 (b, c, d) VALUES (2697563,36712.30166479333,1620150804984440.0),(2697563,784844.3560763699,1620150804984440.8) ON DUPLICATE KEY UPDATE d=VALUES(d)

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 26 page no 34703 n bits 304 index PRIMARY of table `test`.`S230677` trx id 2310042 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 34703 n bits 304 index PRIMARY of table `test`.`S230677` trx id 2310042 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

先说结论:这种现象是一个innodb在MySQL5.7下的bug,可以通过升级内核解决,只了解到这儿那几乎是没有收获的我们详细看一下为什么会出现这个bug?

生产环境下我的事务隔离级别是RC,RC情况下仍然后存在gap lock这个稍后我在详细只是点讲解的时候讲述为什么RC情况下innodb仍然加gap lock。

读上面的死锁日志你会发现非常迷惑,就是明明已经持有了“RECORD LOCKS space id 26 page no 34703 n bits 304”依然还在申请这个锁,原因是这个死锁日志有问题,RECORD LOCKS在这个日志里有迷惑性,并不仅仅是行锁,我通过构建场景发现Gap Lock,Record Lock,Insert Intension 都被泛泛描述为 Record Lock。

加锁顺序:

  1. 加GapLock,不互斥,均加锁成功。
  2. 加RecordLock,互斥,Transaction 2 加锁成功, Transaction 2 等待加Record Lock。
  3. 加Insert Intension, Transaction 2 执行插入前加锁,与Transaction1 持有GapLock 冲突。

死锁产生。

MySQL官方的bug日志展示在5.7.4的版本上他们修复了这个bug。

Despite the title, Bug#50413 can occur even if MySQL replication or binlog is not used. That bug has was fixed in MySQL 5.7.4. The fix is that when we encounter a duplicate key in the clustered index or in any unique secondary index during an INSERT, we will acquire gap locks in the not-yet-checked secondary indexes as well. In this way, the INSERT will already have acquired some locks for the ON DUPLICATE KEY UPDATE part, thus avoiding some potential deadlocks.

从官方的这个修复日志来看,他们加强了对RC情况下聚簇索引和唯一索引的加锁,即在insert过程中,就对聚簇索引和唯一索引加gap lock,来避免在update进行唯一性检查的时候才加锁。

直接说,当前的加锁机制下,的确存在这个死锁的问题且不可避免,不是仅仅加锁顺序一致就可以避免,我准备在官方论坛跟他们讨论下后续有结论我会继续更新。

Innodb加锁顺序:

这里我只粗略的介绍一个原则,即首先加等级锁如S锁然后加高等级锁如X锁,类似上文中提到的加锁顺序,这种类似锁升级的设计保证了MySQL的高性能,当然,弊端是极端场景下的死锁问题,目前来看我任务这个缺点是需要程序设计的时候避免的,而不能归咎于MySQL的设计缺陷。

Innodb锁分类:

MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking

想了下大家还是直接读官方文档吧,有问题可以给我留言。

RC场景下的间隙锁:

在MySQL5.4之前:Gap locking is only used for foreign-key constraint checking and duplicate-key checking.

翻一下:gap lock 只有在外键或者唯一索引冲突检查是才会使用。

5.4之后,上文中提到了,insert的时候也会加锁。

那么为什么这样设计?

我直接引用一段老外的描述:

Suppose transaction T1, with the RC isolation, updates the value of a column that is part of a unique key (index). Clearly this is only possible if the new key value does not already exist. Suppose also transaction T2, also with the RC isolation, attempts to insert a new record with the key value equal to that just created by T1. Due to the RC isolation level T2 does not see the change made by T1, since it has not been committed yet, so the duplicate key error is not raised. If it weren't for a gap lock on the unique index in question, this would eventually result in two records with the same purportedly unique key.

参考阅读:

MySQL engine features - about InnoDB transaction locks (1) - Alibaba Cloud Developer Forums: Cloud Discussion Forums

MySQL :: MySQL 8.0 Reference Manual :: 13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

MySQL Bugs: #103576: insert ... on duplicate key ... disjoint sets of rows deadlock on supremum

MySQL Bugs: #52020: InnoDB can still deadlock on just INSERT...ON DUPLICATE KEY

https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-locks-set.html

https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-transaction-isolation-levels.html

https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-locking.html#innodb-gap-locks

https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-locks-set.html

### INSERT ON DUPLICATE KEY UPDATE 中插入意向锁和 Gap 锁导致死锁的原因 在 MySQL 的 InnoDB 存储引擎中,`INSERT ... ON DUPLICATE KEY UPDATE` 是种常见的操作模式。然而,在高并发环境下,这种语句可能会引发死锁问题。以下是关于其死锁原因以及解决方案的具体分析。 #### 死锁的根本原因 InnoDB 使用两阶段锁定协议来管理事务中的锁请求。当 `INSERT ... ON DUPLICATE KEY UPDATE` 被执行时,如果目标记录不存在,则会尝试向唯索引的间隙(Gap)加锁以防止其他事务在同位置插入相同的数据[^1]。具体来说: - **Gap Lock**:用于阻止其他事务在指定范围内的间隙插入新记录。 - **Insert Intention Lock**:这是种特殊的 Gap Lock,表示某个事务打算在个特定的位置插入条记录。它与其他 Insert Intention Lock 或者更强的 Gap Lock 不兼容[^3]。 当两个或更多事务试图通过 `INSERT ... ON DUPLICATE KEY UPDATE` 向同间隙插入数据时,它们可能分别持有不同的锁并相互等待对方释放资源,从而形成死锁情况[^2]。 #### 解决方案 针对上述提到的由插入意向锁和 Gap 锁引起的死锁现象,可以采取以下几种措施来进行优化和规避: 1. **调整业务逻辑减少竞争** 尽量设计应用程序使得不同事务不会频繁地争抢相同的行或者区间。可以通过预分配 ID 值等方式降低热点区域的压力。 2. **更改隔离级别** 默认情况下,InnoDB 运行于可重复读 (Repeatable Read, RR) 隔离级别之下。虽然这提供了较高的致性保障但也增加了发生死锁的可能性。考虑将某些不敏感的操作降级至读已提交(Read Committed, RC),这样能有效减少不必要的锁开销。 3. **显式控制事务顺序** 如果能够预测哪些事务之间可能存在潜在冲突,则可以在编码层面强制规定这些事务按照固定次序访问共享资源,进而避免循环依赖关系的发生。 4. **重试机制** 对于不可避免会出现少量随机性死锁的应用场景而言,实现自动化的失败处理流程不失为个简单有效的办法——即捕获异常后适当延迟再重新发起受影响的操作直至成功为止。 5. **升级数据库版本** 新版 MySQL 和 MariaDB 已经对此类问题做了不少改进工作;因此建议尽可能保持软件处于最新稳定状态以便利用官方提供的修复补丁和技术支持。 ```python try: cursor.execute(""" INSERT INTO table_name (column_list) VALUES (%s,%s,...) ON DUPLICATE KEY UPDATE column=value; """, value_tuple) except OperationalError as e: if 'Deadlock' in str(e): time.sleep(random.uniform(0.1, 0.5)) # Randomized backoff strategy retry_logic() # Recursive call or other recovery method else: raise # Re-throw unexpected errors ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值