触发器不解决并发反而放大问题,因其在dml事务中同步执行、共享事务id、无法开启新事务,导致锁持有时间延长、易引发死锁或超时;空触发器仍有解析等固定开销;高并发下应优先用应用层事务控制、redis原子操作或显式存储过程替代。

触发器本身不解决并发,反而可能放大并发问题
MySQL 触发器默认在触发它的 DML 语句所属事务中同步执行,**共享同一事务 ID,不可开启新事务**。这意味着:它不是“并行处理单元”,而是事务的延伸——你写一个 AFTER INSERT 触发器,插入没完成,触发逻辑没跑完,那行(甚至相关索引范围)就一直被锁着。
常见错误现象:
高并发下单时,orders 表插入变慢,innodb_row_lock_time_avg 暴涨;两个事务几乎同时插入同款商品订单,库存扣减错乱或报 SQLSTATE '45000' 却不是因为真缺货,而是锁等待超时后逻辑判断失效。
- 触发器内任何
SELECT ... FOR UPDATE或写操作,都会申请 InnoDB 行级 X 锁,阻塞其他事务对该行的修改 - 哪怕触发器只做日志记录(如
INSERT INTO audit_log),只要跨表、没走主键、或扫描范围大,就可能升级为间隙锁或表级锁 - MySQL 8.0 和 5.7 在触发器执行顺序、DDL 原子性上存在差异,但**事务内串行执行这一行为完全一致**
如何让触发器在并发下“勉强可用”
不是“怎么用好”,而是“怎么少出事”。核心思路是:缩小锁粒度、缩短持有时间、避开隐式锁升级路径。
- 所有数据读取必须带
WHERE条件且命中主键或唯一索引,禁用无条件SELECT ... FOR UPDATE - 避免在触发器里调用存储过程、远程服务、或执行
SLEEP()类延迟操作——这些会把行锁拖成秒级 - 更新库存类场景,优先用原子减法:
UPDATE products SET stock = stock - NEW.quantity WHERE id = NEW.product_id AND stock >= NEW.quantity,再用ROW_COUNT()判断是否更新成功,比先查后更新更安全 - 若必须校验后再操作,务必用
SELECT ... FOR UPDATE显式加锁,并确保该SELECT和后续UPDATE在同一事务上下文(即都在触发器内),否则锁会提前释放
为什么空触发器也有性能代价
即使触发器体为空(BEGIN END),MySQL 仍需完成解析、权限检查、事务上下文绑定等固定开销,实测约 0.1–0.3ms。在 TPS 过千的写入场景中,这点开销会线性放大成锁队列积压。
- 每增加一个触发器(哪怕未启用逻辑),都会增加语句执行路径长度,影响 query cache(如启用)和 plan cache 命中率
- 触发器定义在表元数据中,
ALTER TABLE会隐式重建触发器,期间可能阻塞 DML - binlog 格式为
ROW时,触发器产生的变更也会写入 binlog;若主从触发器定义不一致,会导致复制中断
替代方案比硬扛触发器更现实
真正需要强一致性+高并发的场景,比如扣库存、发券、计数更新,靠触发器兜底已经过时了。
- 应用层用
SELECT ... FOR UPDATE+ 事务控制,配合重试机制(如幂等 token),比触发器更可控 - 高频计数类字段(如
view_count),改用 Redis 原子操作 + 定时落库,避免数据库锁竞争 - 审计/日志类需求,改用 MySQL 的
general_log、binlog解析,或业务代码中统一埋点,解耦更干净 - 若必须用数据库端逻辑,优先考虑存储过程显式调用,而非依赖隐式触发——至少你能决定什么时候执行、失败怎么退
最常被忽略的一点:触发器无法响应 LOAD DATA INFILE 或 INSERT ... SELECT 的批量操作中的单行语义,它只对逐行 DML 生效。你以为的“全量同步更新”,其实根本没进触发器。











