触发器死锁主因是隐式延长锁持有时间、打乱加锁顺序及索引缺失引发锁升级;须通过索引优化、降隔离级别至READ COMMITTED、禁跨表与自更新、砍嵌套触发器,并捕获1213错误重试。

触发器不是“自动省事”的黑盒,它是主事务的延伸——高频更新下死锁频发,根本原因在于它隐式延长锁持有时间、打乱加锁顺序、且极易因索引缺失引发锁升级。修复不能只靠重试,得从锁路径上切断。
查触发器内 DML 是否走索引
没索引的 WHERE 条件会让触发器里的 UPDATE 或 SELECT FOR UPDATE 升级为全表扫描,锁住远超预期的行或间隙。这不是慢,是锁爆炸。
- 对触发器里每条 DML,用真实参数模拟执行
EXPLAIN FORMAT=TRADITIONAL,例如:EXPLAIN SELECT * FROM product_stock WHERE sku_code = 'ABC123' FOR UPDATE - 重点看
type字段:出现ALL或index(全索引扫描)必须建索引 - 检查
key是否为空、rows估算值是否远大于实际影响行数——这是优化器误判、间隙锁乱加的信号
把隔离级别从 REPEATABLE READ 降到 READ COMMITTED
REPEATABLE READ 下的间隙锁(Gap Lock)是触发器死锁主力推手。比如触发器里有 WHERE status IN ('pending', 'processing'),InnoDB 会锁住整个区间;而 READ COMMITTED 模式下,这类条件只锁匹配行,不锁间隙。
- 单会话测试:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 全局生效:在
my.cnf加transaction_isolation = READ-COMMITTED,重启 MySQL - 代价是可能产生不可重复读,但库存更新、日志写入、统计计数等场景通常可接受
禁止触发器内跨表 DML 和同一表自更新
触发器里直接 UPDATE 同一张表,InnoDB 已持锁又申请新锁,形成循环等待;跨表操作(如订单触发器去改用户余额表)则极易构成 ABBA 锁序。
- 别写
UPDATE orders SET status = 'done' WHERE id = NEW.id这类语句——哪怕看起来“只是改自己” - 必须同步更新其他字段时,优先用
SET NEW.xxx = ...在BEFORE触发器中赋值 - 涉及多表更新,确认所有业务路径中表操作顺序严格一致(例如总是先
product_stock再warehouse_log) - 砍掉嵌套触发器(A → B → C),锁路径会彻底失控
应用层必须捕获错误码 1213 并重试
即使做了以上优化,高并发下仍可能偶发死锁。MySQL 返回 1213 是明确信号,不重试等于把问题甩给用户。
- Python 示例中
execute_with_retry函数必须集成到 DAO 层,且重试间隔要递增(如0.1 * (attempt + 1)秒) - 重试只适用于幂等操作(如扣减库存、更新状态),非幂等操作(如插入流水)需配合唯一索引或业务去重
- 别依赖连接池自动重试——多数连接池(如 HikariCP)默认不处理
1213,需显式捕获并重放事务
真正难的不是加索引或调隔离级别,而是意识到触发器每一行 SQL 都在主事务的锁上下文中执行——它不开启新事务,但会继承锁生命周期和隔离级别。任何看似无害的 SELECT FOR UPDATE 或未命中索引的 UPDATE,都可能成为死锁链上的致命一环。


















