触发器不支持重试,因其运行在主事务中,无法提交、回滚或捕获死锁错误(如1213),重试必须由应用层实现,配合指数退避、数据重生成和轻量触发器设计。

触发器本身不支持重试机制,MySQL 也不会在触发器报死锁时自动重试主事务——Deadlock found when trying to get lock 报错后,整个事务(含触发器逻辑)直接回滚,应用层必须自己捕获并重试。
为什么在触发器里加重试没用
触发器运行在主 DML 所属事务中,共享同一事务上下文,无法开启新事务、不能 COMMIT/ROLLBACK 自身,更无法在内部捕获死锁并重试。任何试图在触发器里写 REPEAT ... UNTIL 或调用存储过程做循环重试的方案,都会失败或引发语法错误。
- 触发器内不允许出现
START TRANSACTION、COMMIT、ROLLBACK -
GET DIAGNOSTICS在触发器中无法捕获本事务的死锁错误码 1213 - 即便用
DECLARE EXIT HANDLER FOR 1213,也只能终止触发器执行,不会重启主事务
真正有效的重试必须放在应用层
死锁是并发系统的正常现象,关键不是消灭它,而是让失败可恢复。所有写操作(尤其是带触发器的 INSERT/UPDATE)都应包裹在具备指数退避能力的重试逻辑中:
- 捕获 MySQL 错误码
1213(Deadlock found when trying to get lock) - 设置最多 3 次重试,间隔为
10ms → 30ms → 100ms(避免雪崩式重试) - 每次重试前重新生成业务数据(如订单号、时间戳),防止重复插入
- 避免在重试逻辑中嵌套外部调用(HTTP、RPC),否则锁持有时间不可控
触发器越轻量,重试成功率越高
重试成本和触发器复杂度强相关。一个耗时 50ms 的触发器 + 3 次重试 = 平均 150ms 延迟,而一个只做 SET NEW.updated_at = NOW() 的触发器,重试几乎无感知。
- 删掉触发器里所有
SELECT ... FOR UPDATE、跨表UPDATE、子查询统计 - 把库存扣减、积分发放等逻辑移出触发器,改用应用层发 MQ 或写日志表+定时任务
- 确认所有触发器读操作都命中主键:例如
SELECT balance FROM users WHERE id = NEW.user_id,而非WHERE name = NEW.username
别忽略死锁日志配置这个前提
没有 innodb_print_all_deadlocks = ON,你根本不知道重试到底在救什么。线上必须打开,否则等于蒙眼开车:
- 在
my.cnf中添加:innodb_print_all_deadlocks = 1 - 确保
log_error指向可读写的日志路径,且采集工具(如 Filebeat)已接入 - 死锁日志里重点看两行:
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:和*** (2) HOLDS THE LOCK(S):,它们直接暴露哪条 SQL 在抢哪一行
触发器不是并发安全的银弹,它只是主事务的延伸;重试也不是加个 while 循环就完事——真正的难点在于识别哪些逻辑该留在触发器里,哪些必须剥离。一个总在死锁边缘反复横跳的触发器,往往说明它本就不该存在。


















