CATS
138.50M · 2026-02-04
最近几周观测线上服务的业务告警监控群,报出了几次 MySQL Deadlock 错误(Error 1213) ,由于应用服务日志获取不到mysql相关的日志,联系DBA同学获取到了死锁的相关日志,具体如下:
LATEST DETECTED DEADLOCK
...
*** (1) TRANSACTION:
INSERT INTO tb_xxx (...) VALUES (1, 'mp_data', 'app_trip_...', 'answer_cnt')
...
*** (2) TRANSACTION:
INSERT INTO tb_xxx (...) VALUES (1, 'mp_data_dev', 'app_trip_...', 'answer_cnt')
...
*** WE ROLL BACK TRANSACTION (2)
原因是两个几乎同时执行的 INSERT 语句产生死锁导致其中一个事务被回滚,影响业务写入成功率。
涉及表:tb_xxx
CREATE TABLE `tb_xxx` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
...
`engine_type` tinyint(4) NOT NULL DEFAULT '1',
`db_name` varchar(128) NOT NULL DEFAULT '',
`table_name` varchar(256) NOT NULL DEFAULT '',
`column_name` varchar(256) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_tab_col` (`engine_type`,`db_name`,`table_name`,`column_name`)
) ENGINE=InnoDB;
关键点:
uniq_tab_col在 RR 隔离级别下,InnoDB 为保证唯一性约束和防止幻读,会在插入前对目标索引位置加 间隙锁(Gap Lock) 和 插入意向锁(Insert Intention Lock) 。
两个事务并发插入 在唯一索引排序上相邻的记录,例如:
(1, 'mp_data', ..., 'answer_cnt')(1, 'mp_data_dev', ..., 'answer_cnt')虽然字段 db_name 的内容不同,但因字符串排序相邻('mp_data' < 'mp_data_dev'),落在同一索引间隙(gap)内
两个事务互相持有对方所需的间隙锁,形成 循环等待 → 死锁
将原始 INSERT 改为以下任一形式,从根本上避免死锁 + 实现幂等:
INSERT IGNOREINSERT IGNORE INTO tb_xxx
(engine_type, db_name, table_name, column_name)
VALUES (1, 'mp_data', 'xxx', 'yyy');
INSERT ... ON DUPLICATE KEY UPDATEINSERT INTO tb_xxx
(engine_type, db_name, table_name, column_name)
VALUES (1, 'mp_data', 'xxx', 'yyy')
ON DUPLICATE KEY UPDATE
gmt_modified = CURRENT_TIMESTAMP;
即使优化 SQL,高并发下仍可能偶发其他类型死锁,建议统一增加重试逻辑:
for (int i = 0; i < 3; i++) {
try {
mapper.insertIgnore(record);
break;
} catch (DeadlockLoserDataAccessException e) { // Spring 封装
if (i == 2) throw e;
Thread.sleep(10 * (i + 1)); // 指数退避
}
}
| 方案 | 问题 |
|---|---|
降低隔离级别至 READ COMMITTED | 可能引入幻读,破坏业务一致性,不适用于当前指标系统 |
| 删除唯一索引 | 违背数据模型设计,会导致重复关联,不可接受 |
| 应用层先查后插 | 增加 RT,且仍存在竞态条件,无法根治死锁 |
tb_xxx 的插入操作统一改为 INSERT ... ON DUPLICATE KEY UPDATESHOW ENGINE INNODB STATUS 死锁日志解读