记一次华为云RDS MySQL批量写入死锁(Error 1213):INSERT ON DUPLICATE KEY UPDATE
【摘要】 问题背景最近线上消费队列在往华为云 RDS for MySQL 高并发批量写入数据时,频频触发错误:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction业务表的核心逻辑是采用 INSERT INTO t_order_stat (...) VALUES (...) ON D...
问题背景
最近线上消费队列在往华为云 RDS for MySQL 高并发批量写入数据时,频频触发错误:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
业务表的核心逻辑是采用 INSERT INTO t_order_stat (...) VALUES (...) ON DUPLICATE KEY UPDATE ... 语法来实现“有则更新,无则插入”。表结构中除了主键 id 外,还有一个唯一索引 uk_user_date (user_id, stat_date)。
死锁日志分析
登录 RDS 执行 SHOW ENGINE INNODB STATUS\G 查看最近一次死锁日志:
------------------------
LATEST INNODB DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 2837492, ACTIVE 0 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 1029, OS thread handle 1402, query id 8839210 updating
INSERT INTO t_order_stat (user_id, stat_date, amount) VALUES (1001, '2026-10-27', 100) ON DUPLICATE KEY UPDATE amount = amount + 100
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 128 page no 4 n bits 72 index uk_user_date of table `test_db`.`t_order_stat` trx id 2837492 lock_mode X locks gap before rec insert intention waiting
*** (2) TRANSACTION:
TRANSACTION 2837493, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 3 row lock(s)
INSERT INTO t_order_stat (user_id, stat_date, amount) VALUES (1002, '2026-10-27', 200) ON DUPLICATE KEY UPDATE amount = amount + 200
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 128 page no 4 n bits 72 index uk_user_date of table `test_db`.`t_order_stat` trx id 2837493 lock_mode X locks gap before rec
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 128 page no 4 n bits 72 index uk_user_date of table `test_db`.`t_order_stat` trx id 2837493 lock_mode X locks gap before rec insert intention waiting
从日志可以看出:
- 事务 1 和事务 2 都执行了
INSERT ... ON DUPLICATE KEY UPDATE。 - 在默认的
REPEATABLE-READ(RR)隔离级别下,INSERT ... ON DUPLICATE KEY UPDATE判定唯一键冲突时,会对唯一索引uk_user_date申请 Next-Key Lock(排他锁 + 间隙锁 Gap Lock)。 - 当两个事务并发对邻近记录执行该语句时,事务 1 持有了某段 gap 锁并等待插入意向锁,事务 2 也持有了冲突段的 gap 锁并等待事务 1 的 gap 释放,从而形成循环等待造成死锁 Error 1213。
根本原因
在 InnoDB RR 隔离级别下:
INSERT ... ON DUPLICATE KEY UPDATE会先对唯一索引加 X 锁(包含 Gap 锁)。- 多线程/多节点并发批量写入时,记录顺序是不固定的(比如线程 A 顺序写入
[A, B],线程 B 顺序写入[B, A]),在加锁过程中,交叉申请锁资源必死锁。 - 此外,间隙锁(Gap Lock)之间是互兼容的,多个事务可以同时持有同一区间 Gap 锁,但当它们都需要在该区间申请插入意向锁(Insert Intention Lock)时,就会互相等待对方释放 Gap 锁。
解决方案与代码示例
1. 将隔离级别调整为 READ-COMMITTED (RC)
在 RC 隔离级别下,InnoDB 会取消间隙锁(Gap Lock),显著降低死锁概率。华为云 RDS 可以在参数组中将 transaction_isolation 改为 READ-COMMITTED。
2. 应用层按唯一键排序后分批写入
并发批量写入前,对批次内的记录按照唯一键(如 user_id, stat_date)进行全局严格排序,保证所有并发线程加锁顺序一致。
Python 示例代码如下:
import pymysql
def batch_upsert(cursor, record_list):
if not record_list:
return
# 核心:根据唯一索引字段进行排序,保证加锁顺序一致
sorted_records = sorted(record_list, key=lambda x: (x['user_id'], x['stat_date']))
sql = """
INSERT INTO t_order_stat (user_id, stat_date, amount)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)
"""
values = [(r['user_id'], r['stat_date'], r['amount']) for r in sorted_records]
cursor.executemany(sql, values)
总结:线上高并发批量 Upsert 场景,一定要规范“唯一键排序 + RC 隔离级别”的最佳实践,从根源规避 Gap 锁交叉死锁问题。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)