触发器在高并发下引发锁竞争的核心是职责错位,应移出查表、跨表更新、聚合计算等操作;其内select会因gap lock放大锁范围,after触发器更易死锁,须用on duplicate key update等替代方案。

触发器在高并发下引发锁竞争,核心问题不是“怎么写得更高效”,而是“它不该干这些事”。查表、跨表更新、聚合计算、调用非确定性函数——全得移出去。
触发器里 SELECT 查其他表为什么放大锁竞争
表面是查一行,实际在 REPEATABLE READ 隔离级别下会加 gap lock:不仅锁住匹配行,还锁住前后间隙。并发插入时多个事务排队等同一段索引范围锁,TPS 断崖下跌。
-
SELECT name FROM staff WHERE id = NEW.operator_id即使staff.id有主键索引,也仍会锁住该索引页;高并发写入会让这个页成为热点 - 若
staff表被其他业务频繁更新,该SELECT还可能触发二级索引回表,延长持锁时间 - MySQL 8.0+ 对
AFTER触发器中SELECT的锁行为更严格,5.7 上没报错的逻辑,在 8.0 可能直接抛Deadlock found when trying to get lock
AFTER vs BEFORE:哪个更容易锁死
AFTER 触发器在高并发下更危险。它在原始语句已持锁的前提下再发起新 SQL,极易形成锁链闭环。
- 比如
AFTER INSERT ON orders中执行UPDATE order_summary SET total = total + NEW.amount,order_summary行锁和orders新行锁在同一事务内竞争,别的事务稍一凑近就死锁 -
BEFORE触发器可直接用SET NEW.total_amount = ...修改即将插入的值,不发额外 SQL,零新增锁 - 真要更新当前行状态,优先用
INSERT ... ON DUPLICATE KEY UPDATE替代先查后更,减少锁持有窗口
哪些写法会把行锁升级成范围锁甚至表锁
触发器里一个看似无害的 UPDATE 或 SELECT,只要条件没命中唯一索引,InnoDB 就可能锁住整个索引范围。
-
UPDATE product SET status = 'sold' WHERE category_id = 123—— 若category_id没索引或只有非唯一索引,就会锁住整个索引范围,所有插入/更新该分类商品的请求全部阻塞 - 禁止在触发器里用
OR、LIKE '%abc'、NOT IN等无法走索引的写法;要用EXISTS替代IN子查询 - 避免
SELECT ... FOR UPDATE—— 触发器里显式加锁等于主动制造瓶颈,除非你明确知道只锁单行且索引完全覆盖
统计类、聚合类逻辑必须移出触发器
这类操作天然不适合放在触发器里:它们需要查多行、跨表、聚合计算,必然拉长事务生命周期,放大锁竞争面。
- 改用应用层异步任务(如 Kafka 消息消费)、定时 job(如 cron + Python 脚本)或写入专用
trigger_queue表由后台消费 - 若必须强一致,可在触发器中仅写入变更事件(如
INSERT INTO trigger_queue (table_name, pk_id, event_type) VALUES ('orders', NEW.id, 'insert')),后续由独立服务处理 - 时间戳统一由应用层传入,作为字段值写入;触发器只做校验或衍生计算,避免
NOW()、UUID()等非确定性函数
真正难的不是写出能跑的触发器,而是判断哪些逻辑“看起来合理”却正在悄悄拖垮整个数据库的并发能力——尤其是那些在低流量下毫无异常、一上生产就卡顿的 SELECT 和 UPDATE。











