记一次华为云RDS MySQL批量写入死锁(Error 1213):INSERT ON DUPLICATE KEY UPDATE

举报
yd_233909394 发表于 2026/10/28 08:50:25 2026/10/28
【摘要】 问题背景最近线上消费队列在往华为云 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. 事务 1 和事务 2 都执行了 INSERT ... ON DUPLICATE KEY UPDATE。
  2. 在默认的 REPEATABLE-READ(RR)隔离级别下,INSERT ... ON DUPLICATE KEY UPDATE 判定唯一键冲突时,会对唯一索引 uk_user_date 申请 Next-Key Lock(排他锁 + 间隙锁 Gap Lock)。
  3. 当两个事务并发对邻近记录执行该语句时,事务 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)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。