MySQL触发器内执行UPDATE会加剧锁竞争,导致高频更新场景下大量事务等待和CPU飙升;应避免二次写入,改用ON DUPLICATE KEY UPDATE或异步聚合。

触发器里写UPDATE会直接放大锁竞争
MySQL触发器本身不“执行慢”,但一旦在 AFTER UPDATE 里再发一条 UPDATE,就等于把单次更新变成两次串行写 + 两次行锁申请。尤其当原表有高频更新(比如秒杀库存、订单状态流转),触发器里的二次更新会迅速把同一行锁住又释放,引发大量 waiting for trx id 等待,CPU全耗在锁调度和事务状态同步上。
常见错误模式:
- 在
goods表的AFTER UPDATE里去更新goods_stat统计表,而goods_stat是单行汇总(如id=1记总销量) - 用
SELECT ... FOR UPDATE在触发器里查再改,形成隐式锁升级 - 触发器中调用自定义函数,而该函数内部含查询或循环逻辑
用 INSERT ... ON DUPLICATE KEY UPDATE 替代触发器UPDATE
只要目标表有唯一键(比如 (goods_id)),就能绕过触发器二次写入。把原本放在触发器里的统计逻辑,改成应用层或定时任务批量写入,或者用原子写法落地:
INSERT INTO goods_stat (goods_id, sale_count, updated_at) VALUES (NEW.id, 1, NOW()) ON DUPLICATE KEY UPDATE sale_count = sale_count + 1, updated_at = NOW();
这样做的好处:
- 避免触发器上下文开销(无额外解析、无嵌套事务状态管理)
- 冲突时走索引更新,不阻塞原表主更新流
- 天然支持批量(一次插多行),比逐行触发高效一个数量级
必须用触发器时,加条件过滤+延迟合并
如果业务强依赖实时性且无法改架构,至少要做两层收敛:
- 在触发器开头加判断:
IF OLD.status != NEW.status AND NEW.status = 'paid' THEN ...,避免所有字段变更都触发 - 不直接UPDATE统计表,而是往一张轻量日志表(如
stat_log)写变更记录,再由后台任务每秒聚合一次写入目标表 - 禁用
innodb_lock_wait_timeout在触发器内生效(它会导致等待超时后自动ROLLBACK,反而加剧CPU重试压力)
注意:stat_log 表必须是 ENGINE=BLACKHOLE 或极简结构(仅 goods_id, delta, created_at),否则日志表自己也会成为新热点。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
检查触发器是否被隐式递归激活
MySQL默认允许触发器内DML操作,但若目标表恰好也挂了同类型触发器,就会形成链式调用。例如:
UPDATE goods → AFTER UPDATE 触发器 → UPDATE goods_stat → goods_stat 上也有 AFTER UPDATE → 再触发…
快速确认方法:
- 查触发器定义:
SHOW CREATE TRIGGER trigger_name; - 看是否对本库其他表做了写操作,尤其是那些也带触发器的表
- 临时禁用可疑触发器:
DROP TRIGGER IF EXISTS xxx;,观察CPU是否回落
真正难排查的是跨库触发器调用——比如 db1.goods 的触发器去改 db2.log,而 db2.log 又有触发器回写 db1.goods_stat。这种环路不会报错,但会让 SHOW PROCESSLIST 里大量线程卡在 updating 状态,CPU持续90%以上。










