直接看show engine innodb status\g中latest detected deadlock区块,重点查含new./old.的dml、跨表反向加锁链、非主表锁持有及无索引where触发的锁升级。

怎么看死锁日志里是不是触发器惹的祸
直接看 SHOW ENGINE INNODB STATUS\G 输出中 LATEST DETECTED DEADLOCK 区块,别扫一眼 SQL 就跳过——触发器引发的死锁藏在细节里:
- 找语句里有没有带
NEW.或OLD.的 UPDATE/INSERT,比如UPDATE user_points SET balance = balance + NEW.amount WHERE user_id = NEW.user_id,这种基本就是 AFTER 触发器执行的 - 检查两个事务的锁持有和等待关系是否跨了不同表,且顺序相反:事务 A 持
orders行锁、等user_points行锁;事务 B 持user_points行锁、等orders行锁 → 典型触发器引入的反向加锁链 - 留意“holds the locks”里是否出现非主表的表名(如主操作是 INSERT
orders,但日志里却显示它持有了points_log的 X 锁),说明触发器已实际执行并加锁 - 若某事务的 SQL 显示为
INSERT INTO orders ...,但等待锁的语句却是UPDATE stats SET count = count + 1 WHERE type = 'order',而stats.type又没索引 → 这就是触发器内 DML 引发锁升级的铁证
为什么 AFTER 触发器在死锁日志里更显眼
AFTER 触发器的执行时机决定了它一定会出现在死锁链的“后半段”,而且自带“延迟加锁”属性:
- AFTER INSERT 已完成对新行的 X 锁加锁,此时再执行任何 DML(哪怕只是 UPDATE 单行),都是在已有锁基础上新增锁请求,日志里会明确分两层:先锁主表,再锁被触发的表
- BEFORE 触发器只改
NEW字段,不产生额外 DML,所以几乎不会出现在死锁链中(除非你主动写SELECT ... FOR UPDATE) - MySQL 8.0+ 才能在
INNODB STATUS里把嵌套触发器语句展开,低版本只能看到外层语句,但锁对象和等待关系仍暴露了触发路径
哪些触发器行为会让死锁日志变得难读
不是所有触发器都一样危险,以下三类会让日志线索断裂、定位困难:
- 多级嵌套:AFTER INSERT
orders→ UPDATEusers→ 触发 BEFORE UPDATEusers→ INSERTaudit_log。日志里只显示第一层和最后一层锁,中间环节丢失 - 动态拼接 SQL:用
CONCAT构造表名或条件,导致日志中语句不可读,比如UPDATECONCAT('log_', @month)SET ...,你根本没法反查是哪张物理表 - 调用存储过程:日志里只记
CALL update_summary(...),不展开内部 SQL,锁对象也只显示过程名,无法判断具体锁了哪几行
排查时必须同步核对的三个配置点
光看日志不够,得对照数据库当前状态确认触发器是否真在起作用:
- 查触发器是否存在且启用:
SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING FROM information_schema.TRIGGERS WHERE EVENT_OBJECT_TABLE = 'orders' - 确认隔离级别:RR(默认)下触发器内范围查询会带上 Gap Lock,而 RC 下不会,
SELECT @@transaction_isolation必须查 - 检查关键字段是否有索引:触发器里
WHERE user_id = NEW.user_id如果user_points.user_id没索引,日志里就会出现全表扫描 + Next-Key Lock,锁范围爆炸,但语句本身看起来毫无异常
真正卡住人的,往往不是日志里那几行 SQL,而是触发器把加锁动作藏在了“理所当然”的业务逻辑背后——它不报错,只悄悄延长锁持有时间、打乱加锁顺序、放大锁范围。盯住 NEW.、跨表锁反向、无索引 WHERE,这三点漏掉一个,就可能把根因归给“偶发网络抖动”。











