触发器本身不开启新事务,但会继承主事务锁生命周期和隔离级别,是死锁链关键一环;需通过索引优化、降级为read committed、禁跨表dml、统一锁序及应用层重试来切断锁冲突路径。

触发器本身不开启新事务,但它在主事务中隐式执行 DML,会继承主事务的锁生命周期和隔离级别——这意味着它不是“独立单元”,而是死锁链上的关键一环。修复核心不是禁用触发器,而是切断它放大锁冲突的路径。
查触发器内部 SQL 是否走索引
没索引的 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
RR 下的间隙锁(Gap Lock)是触发器死锁主力推手。比如触发器里有 WHERE status IN ('pending', 'processing'),InnoDB 会锁住整个区间;而 RC 模式下,这类条件只锁匹配行,不锁间隙。
- 执行
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED测试效果 - 全局修改需在
my.cnf加transaction_isolation = READ-COMMITTED,重启生效 - 代价是可能产生不可重复读,但库存更新、日志写入、统计计数等场景通常可接受
避免触发器内跨表操作形成 ABBA 锁序
主事务先锁 orders → 触发器再去锁 product_stock;另一并发事务反向操作,立刻死锁。这不是概率问题,是必然路径。
- 禁止触发器里出现
UPDATE/INSERT到其他业务表,尤其是非幂等操作 - 若必须同步状态,改用异步消息(如写 binlog + 消费者处理),或业务层统一锁序(例如所有涉及订单+库存的操作,强制先锁
product_stock行,再锁orders行) - 检查是否有嵌套触发器(A 触发 B,B 再触发 C),这种链式调用会让锁范围和顺序彻底失控
应用层加死锁重试逻辑不能少
即使做了以上优化,高并发下仍可能偶发死锁。MySQL 返回错误码 1213 是明确信号,不重试等于把问题甩给用户。
- Python 示例中
execute_with_retry函数必须集成到 DAO 层,且重试间隔要递增(如0.1 * (attempt + 1)秒) - 注意:重试只适用于幂等操作(如扣减库存、更新状态),非幂等操作(如插入流水)需配合唯一索引或业务去重
- 别依赖连接池自动重试——多数连接池(如 HikariCP)默认不处理
1213,需显式捕获并重放事务
真正难的不是加索引或调隔离级别,而是意识到触发器不是“自动省事”的黑盒,它是主事务的延伸,每一行 SQL 都在共享同一把锁、同一个事务 ID。任何脱离锁视角去谈触发器优化的方案,都会在流量高峰被打回原形。











