触发器中避免跨表写操作、禁止select...for update、不依赖max(id)等自增推导,推荐应用层异步处理或预分配id,并通过死锁日志和performance_schema定位问题。

触发器里别碰其他表的写操作
死锁在触发器里爆发,八成是因为它偷偷去改了别的表。比如 INSERT 一个订单后,触发器顺手去 UPDATE 库存表,而另一条事务正相反——先锁库存再插订单,两边一卡,就 deadlock 了。
实操建议:
- 触发器逻辑尽量只操作当前表(
NEW和OLD所属的表),避免UPDATE/DELETE其他表 - 真要联动更新,改用应用层异步处理(比如发 MQ 消息),或退一步用延迟写入(如插入到日志表,由定时任务批量处理)
- 查
SHOW ENGINE INNODB STATUS时重点看*** (1) WAITING FOR THIS LOCK TO BE GRANTED:后面是不是跨表锁
避免在触发器中调用存储函数或复杂子查询
看起来只是读个配置值,但万一那个 SELECT 带了 FOR UPDATE 或命中了没索引的字段,就会拉长锁持有时间,提高死锁概率。
实操建议:
- 触发器内禁止出现
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE - 所有读操作必须走覆盖索引;如果要查配置,优先用常量或应用层缓存,而不是实时查
config_table - 用
EXPLAIN确认触发器里每个SELECT的type是const或ref,不是ALL或index
INSERT 触发器慎用 BEFORE INSERT + 自增主键依赖
当多个并发 INSERT 都在 BEFORE INSERT 里试图读取 MAX(id) 或调用 LAST_INSERT_ID(),很容易因为间隙锁(gap lock)互相阻塞,尤其在可重复读隔离级别下。
实操建议:
- 绝对不要在
BEFORE INSERT里做SELECT MAX(id) FROM t这类操作 - 需要生成业务单号?用
UUID_SHORT()或应用层雪花 ID,别依赖数据库自增序列推导 - 如果非得用序号,考虑提前分配段(如预生成 1000 个号存进
seq_pool表,用UPDATE ... LIMIT 1取),避开高竞争点
监控和快速定位:怎么知道是触发器惹的祸
死锁日志里不会直接写“触发器导致”,但能看出线索:事务堆栈里出现多层嵌套 SQL,或者锁等待链里包含明显不属于主 SQL 的表名。
实操建议:
- 开启
innodb_print_all_deadlocks = ON,把死锁日志重定向到文件,定期 grepTRIGGER或触发器名 - 对高频表的触发器加日志:在触发器开头插入一条记录到
trigger_log(注意这个表要用ENGINE=BLACKHOLE或异步写,否则又绕回死锁) - 用
performance_schema查触发器执行耗时:SELECT * FROM performance_schema.events_statements_history_long WHERE sql_text LIKE '%TRIGGER%'
真正难的不是写触发器,而是它藏在事务深处,不报错、不超时,只在峰值时随机卡住几笔交易——这时候你得信日志,别信“应该没问题”。











