触发器中执行update是死锁高发点,应优先直接赋值new字段;禁止跨表dml、非索引查询及拆步统计更新,所有语句须explain验证索引使用。

触发器里执行 UPDATE 是死锁高发点,不是“能不能做”,而是“做了什么、怎么做的”。高并发下写入吞吐量下降,往往不是因为 SQL 慢,而是锁被意外延长或错序了。
BEFORE 触发器比 AFTER 更安全,但别在里头再发 UPDATE
BEFORE 触发器运行时,新行还没落盘,NEW 可直接赋值,不额外加锁;AFTER 触发器则是在 INSERT/UPDATE 完成后执行,当前行已持 X 锁,此时再 UPDATE 同一表其他行(甚至同一行),等于在已有锁上申请新锁,极易触发循环等待。
- 正确做法:
SET NEW.status = 'processed'或SET NEW.updated_at = NOW()—— 直接改NEW字段,零额外 SQL - 错误写法:
UPDATE orders SET status = 'done' WHERE id = NEW.id—— 看似只改自己,实则二次加锁 - 若需更新关联字段(如订单总金额),优先用
NEW.total = NEW.qty * NEW.price计算,而非查子表再赋值
触发器内禁止跨表 DML,读操作必须命中主键或唯一索引
触发器一旦访问第二张表,就打破了单事务单表的锁边界。MySQL 不会区分“主 SQL”和“触发器 SQL”,所有语句共享同一事务 ID 和锁生命周期 —— 主事务锁 orders,触发器再去锁 inventory,另一条路径反着来,*** (1) WAITING FOR THIS LOCK TO BE GRANTED: TABLE: `inventory` 就出现了。
- 绝对禁用:
UPDATE inventory SET stock = stock - NEW.qty WHERE sku = NEW.sku(除非sku有唯一索引,且确认无并发扣减) - 允许的读操作仅限:
SELECT qty FROM stock WHERE sku = NEW.sku,且必须确保EXPLAIN显示type: const或ref,不能是ALL或index - 配置类数据(如超时时间)别实时查表,改用应用层缓存或预加载到变量中
避免间隙锁爆炸:所有 WHERE 条件必须走索引,RR 隔离级下慎用范围查询
没索引的 WHERE 条件会让 InnoDB 升级为全表扫描 + 临键锁(Next-Key Lock),看似只是 SELECT,实则锁住整段索引间隙 —— 这比显式 UPDATE 更隐蔽、更伤并发。
- 检查方法:对触发器内每条
SELECT/UPDATE/DELETE,用真实参数跑EXPLAIN FORMAT=TRADITIONAL - 危险信号:
type: ALL、rows估算远大于实际、key为NULL - 临时缓解:把隔离级别从默认
REPEATABLE READ降为READ COMMITTED,可让大部分非唯一条件不加间隙锁(代价是可能不可重复读,但统计/日志类业务通常可接受)
统计类逻辑必须剥离出触发器,改用异步或单条原子写
用触发器维护计数器(如 order_stats.total += NEW.amount)是死锁重灾区:高频写入的统计表本身就成了锁热点,而 SELECT + UPDATE 拆成两步,中间窗口期足够并发事务插队。
- 推荐替代方案:
INSERT INTO order_stats_log (order_id, delta) VALUES (NEW.id, NEW.amount)(日志表引擎可用BLACKHOLE或CSV,零存储开销) - 次选方案:
INSERT INTO order_stats (date, total) VALUES (CURDATE(), NEW.amount) ON DUPLICATE KEY UPDATE total = total + NEW.amount—— 单条语句,无 SELECT,锁持有时间最短 - 绝对禁用:
SELECT total FROM order_stats WHERE date = CURDATE()→UPDATE ... SET total = ?拆步写法
真正难处理的不是“要不要用触发器”,而是当它嵌在现有系统里、又没法立刻下线时,如何快速定位哪一行 SQL 在悄悄锁表。每次改触发器,都要拿 EXPLAIN 过一遍内部语句,而不是只看语法是否合法 —— 死锁从来不在报错里,而在锁等待链的缝隙中。











