触发器本身不直接死锁,但会隐式延长锁持有时间、打乱加锁顺序、引发锁升级,导致死锁更频繁难排查;确认方法是查死锁日志中是否出现跨表反向等待、含new.的update语句或触发器目标表出现在holds/waiting两侧。

触发器本身不直接死锁,但会让死锁更频繁、更难排查——核心问题在于它隐式延长锁持有时间、打乱锁顺序、且容易因索引缺失引发锁升级。
怎么从死锁日志确认是触发器惹的祸
别只扫一眼报错 SQL,重点看 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 区块里的两个事务:
- 找
UPDATE或SELECT FOR UPDATE语句是否出现在触发器目标表上(比如主表是orders,死锁链里却有UPDATE user_points SET ... WHERE user_id = ?) - 检查
holds the locks和waiting for this lock是否跨了不同表,且顺序相反(如事务 A 持有orders锁等待user_points,事务 B 持有user_points锁等待orders) - 若日志里出现类似
UPDATE order_log SET status = 'processed' WHERE order_id = NEW.id这种带NEW.的语句,基本可断定是AFTER INSERT触发器执行的
AFTER vs BEFORE:为什么AFTER触发器更危险
AFTER INSERT 触发器执行时,新行已插入并加了行级 X 锁,事务尚未提交;此时再对其他行或表做 DML,等于在已有锁基础上叠加新锁请求:
-
AFTER INSERT中执行UPDATE stats SET count = count + 1 WHERE type = 'order'—— 若type无索引,InnoDB 全表扫描 + 加 Next-Key Lock,瞬间锁住整张统计表 -
BEFORE INSERT只能改NEW字段值,不额外加锁(除非你主动写SELECT FOR UPDATE) - 多级触发器嵌套(如
AFTER INSERT→ 更新user_points→ 触发BEFORE UPDATE再写points_audit)会让锁路径不可控,MySQL 8.0+ 才能在INNODB STATUS里准确定位到内层语句
触发器里哪些操作必须立刻砍掉
以下行为在高并发下几乎等于主动制造死锁热点:
- 跨业务主表的
UPDATE(例如从orders触发器去改users余额表),一律移出触发器,改用应用层统一锁序 - WHERE 条件未命中主键或唯一索引的任何 DML,比如
UPDATE config SET value = 'on' WHERE module = 'payment'—— 若module无索引,就是全表 X 锁 - 带范围条件的
SELECT FOR UPDATE,如SELECT * FROM logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 DAY) FOR UPDATE,RR 隔离下会锁整个时间区间 - 调用存储过程,除非该过程明确声明只读且无任何 DML
真正管用的优化动作清单
别只调隔离级别或加重试,优先做这几件事:
- 统计类逻辑尽量移出触发器,改用应用层异步更新或定时任务
- 非得用触发器维护汇总字段时,优先选
BEFORE INSERT/UPDATE,直接计算并赋值给NEW.count_field - 如果必须用
AFTER,确保被更新的目标表只有极低并发写入(例如日志归档表),且更新语句带WHERE id = ?主键条件 - 所有触发器内的 DML 必须走主键或唯一索引 —— 没索引就建,别犹豫
最常被忽略的是:触发器里的 SELECT 不一定安全。表面只是查询,但如果 WHERE 条件没索引,就会触发全表扫描 + Next-Key Lock,比显式 UPDATE 更隐蔽、更伤并发。











