mysql触发器高并发下易致死锁,核心原因是隐式延长锁持有时间、打乱加锁顺序或跨表引入额外锁依赖;正确做法是避免触发器内dml操作、剥离多表联动逻辑、禁用select for update、杜绝max(id)等间隙锁风险操作,并通过日志与性能分析快速定位问题。

MySQL触发器在高并发下极易引发死锁,核心原因不是“用了触发器”,而是它在事务上下文中隐式延长了锁持有时间、打乱了加锁顺序、或跨表引入额外锁依赖——只要触发器里出现UPDATE、SELECT FOR UPDATE、INSERT INTO ... SELECT这类语句,死锁风险就直线上升。
BEFORE/AFTER触发器里别碰当前表的DML操作
在BEFORE UPDATE或AFTER INSERT中执行UPDATE t1 SET status = 'done' WHERE id = NEW.id,看似只改自己,实则InnoDB已对NEW.id所在行持锁,触发器再发一条UPDATE就是二次申请同一行锁,极易触发循环等待。
- 正确做法:用
SET NEW.status = 'done'在BEFORE中直接赋值,不走额外SQL - 涉及多字段联动计算(如更新订单状态同时刷新子表计数),必须剥离出触发器,改由应用层异步完成
- 如果非得在数据库侧维护统计值,用
INSERT INTO summary_log (table_name, pk, delta) VALUES ('orders', NEW.id, 1)写日志表(引擎用BLACKHOLE或CSV),再由定时任务批量聚合
触发器内禁止跨表写 + 严格限制读操作范围
死锁日志里出现*** (1) WAITING FOR THIS LOCK TO BE GRANTED: ... TABLE: `inventory`,基本可断定是触发器越界操作了——主事务锁订单表,触发器又去锁库存表,另一条路径反着来,立刻 deadlock。
- 所有读操作必须命中覆盖索引:
EXPLAIN SELECT config_value FROM config_table WHERE key_name = 'order_timeout'的type必须是const或ref,不能是ALL或index - 彻底禁用
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE;配置类数据优先用应用层缓存,或预加载到触发器变量中 - 真要查关联数据,只允许基于主键或唯一索引的单行查找,例如
SELECT qty FROM stock WHERE sku = NEW.sku(且sku必须有唯一索引)
别在BEFORE INSERT里依赖MAX(id)或LAST_INSERT_ID()
并发INSERT时,多个BEFORE INSERT触发器同时执行SELECT MAX(id) FROM orders,会在主键索引间隙上产生冲突的gap lock,尤其在RR隔离级别下,互相阻塞概率极高。
- 绝对不要用
SELECT MAX(id)生成业务单号;改用UUID_SHORT()、雪花ID,或应用层预分配段(如从seq_pool表UPDATE ... LIMIT 1取号) - 自增主键本身已由InnoDB保证唯一性,触发器无需参与ID生成逻辑
- 若业务强依赖连续序号(如发票号),用单独的号段服务+乐观锁更新,避开数据库行锁竞争点
怎么快速确认是不是触发器惹的祸
死锁日志不会明说“触发器导致”,但线索很典型:LATEST DETECTED DEADLOCK里事务堆栈深、SQL语句多层嵌套、锁等待链中出现主SQL根本没涉及的表名。
- 开启
innodb_print_all_deadlocks = ON,定期grep -i 'trigger\|TRIGGER' /var/lib/mysql/hostname.err - 对高频表触发器加轻量日志:在触发器开头插入
INSERT INTO trigger_log (tname, op, ts) VALUES ('orders', 'AFTER_INSERT', NOW()),但该表必须用ENGINE=BLACKHOLE,否则又绕回死锁 - 查
performance_schema.events_statements_history_long,过滤sql_text LIKE '%orders%TRIGGER%',看执行耗时是否异常偏高
最易被忽略的一点:触发器执行时间越长,锁持有窗口就越宽,哪怕它只操作一张表——所以哪怕逻辑合规,也要压测验证TPS拐点。一旦发现QPS上升后死锁率非线性增长,优先砍掉触发器,把逻辑提到应用层做幂等控制和重试,比在数据库里缝缝补补可靠得多。











