mysql触发器不控制事务隔离级别,死锁主因是其隐式加锁与并发事务交织;须避免before中update、after中更新主表、触发器内select for update及跨表非主键查询。

MySQL触发器本身不控制事务隔离级别,死锁也几乎从不源于“隔离级别设置错误”,而是由触发器内隐式加锁行为与并发事务交织导致。真正要防的,是触发器在事务上下文中无意识延长锁持有时间、打乱加锁顺序、或跨表引入锁依赖。
BEFORE触发器里别写UPDATE,改用SET NEW.xxx赋值
常见错误是在BEFORE UPDATE中执行UPDATE orders SET status = 'processed' WHERE id = NEW.id。此时InnoDB已对NEW.id所在行持X锁,触发器再发一条UPDATE,等于在同一行上重复申请锁,极易触发循环等待。
- 正确做法:直接
SET NEW.status = 'processed',不走SQL语句,不额外加锁 - 适用场景:字段补全、状态预设、时间戳生成(如
SET NEW.updated_at = NOW()) - 注意:仅限当前表字段;跨表查值必须基于主键且命中唯一索引,否则可能引入间隙锁
AFTER触发器禁止更新业务主表,尤其别碰同一张表
AFTER INSERT执行时,新记录已插入并持X锁,但事务未提交。此时若触发器再去UPDATE orders其他行(比如刷新统计字段),就是在已有锁基础上叠加新锁请求——锁粒度变厚、等待链拉长,死锁概率陡增。
- 典型高危写法:
UPDATE order_stats SET total = total + NEW.amount WHERE date = CURDATE(),尤其当order_stats.date没建索引时,会锁整张表或大范围间隙 - 替代方案:用
INSERT INTO summary_log (...) VALUES (...)写日志表(引擎选BLACKHOLE或CSV),再由定时任务批量聚合 - 如果非得更新汇总表,确保WHERE条件命中主键或唯一索引,且目标表写入频率极低(如每日归档表)
触发器里查数据必须走覆盖索引,禁用SELECT FOR UPDATE
死锁日志里出现*** (1) WAITING FOR THIS LOCK TO BE GRANTED: TABLE: `inventory`,基本可断定是触发器越界读取了库存表——主事务锁订单,触发器又去锁库存,另一条路径反着来,立刻deadlock。
- 所有
SELECT必须能被EXPLAIN判定为type=const或ref;禁止type=ALL或index - 绝对禁用
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE;配置类数据优先应用层缓存 - 真要查关联数据,只允许单行主键查找,例如
SELECT qty FROM stock WHERE sku = NEW.sku,且sku必须有唯一索引
隔离级别调到READ COMMITTED能减少间隙锁,但不能解决根本问题
在RR隔离级别下,WHERE status IN ('pending', 'processing')这类范围条件会触发间隙锁,而触发器里一有这种写法,多个并发INSERT就容易在主键索引间隙上互相阻塞。切到READ COMMITTED后,间隙锁仅保留在唯一索引等极少数场景,普通非唯一条件不再加Gap Lock。
- 执行:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED(或全局配置innodb_default_row_format=dynamic配合) - 代价:可能出现不可重复读,但对日志、统计、通知类业务通常可接受
- 重点:这只是辅助手段,不能掩盖触发器里
UPDATE跨表、SELECT FOR UPDATE、MAX(id)等根本性风险操作
最危险的不是“不知道怎么设隔离级别”,而是把统计更新、余额扣减、状态联动这些本该由应用层统一控制锁顺序的逻辑,硬塞进触发器——它天然缺乏事务边界感,又复用主事务锁,稍有不慎就成了死锁放大器。











