触发器执行时会隐式加行锁,非“只读操作”:访问表会加next-key lock,select需显式加锁,避免全表扫描;禁双向跨表更新防死锁;函数调用不隔离锁;推荐应用层替代。

触发器执行时会隐式加行锁,不是你想的“只读操作”
很多人写 BEFORE INSERT 触发器去校验数据、生成默认值,以为只是读几行、算个值,不会影响并发。实际上,MySQL 在触发器内部访问的每一张表(哪怕只是 SELECT),只要涉及索引查找或写入,都会在对应行上加 next-key lock —— 这是间隙锁 + 行锁的组合,会阻塞其他事务对同一范围的插入/更新。
常见错误现象:INSERT ... SELECT 语句卡住,SHOW ENGINE INNODB STATUS 显示 waiting for table metadata lock 或 lock wait timeout exceeded,但主 SQL 看不出明显锁冲突,根源往往在触发器里多查了一张配置表。
- 触发器中所有
SELECT必须带FOR UPDATE或LOCK IN SHARE MODE显式声明意图,否则 MySQL 默认按一致性读(MVCC)处理;但一旦触发器里有UPDATE或INSERT,前面的SELECT就可能被升级为加锁读 - 避免在触发器里查非主键字段(尤其是没索引的),否则容易升级为全表扫描+锁全表
- 如果只是取一个固定配置值(如系统参数),优先用
SELECT ... INTO @var FROM DUAL或直接硬编码,别查业务表
跨表更新触发死锁:两个触发器互相等待对方锁
典型场景:用户表 users 的 AFTER UPDATE 触发器去更新日志表 user_logs;而 user_logs 的 BEFORE INSERT 又反向查 users 做归属校验。此时两个事务 T1、T2 并发执行,就极易形成循环等待:T1 持有 users 行锁、等 user_logs 插入锁;T2 持有 user_logs 插入锁、等 users 行锁。
这种死锁不会报错“duplicate key”,而是直接 Deadlock found when trying to get lock,且 SHOW ENGINE INNODB STATUS 里能清晰看到两个事务各自持有的锁和等待的锁。
- 触发器链路必须单向:A → B,禁止 B → A。如果真需要双向联动,改用应用层事务 + 显式顺序控制
- 所有跨表操作,按固定顺序加锁:比如约定总是先锁
users,再锁user_logs,所有触发器和应用代码都遵守 - 用
SELECT ... FOR UPDATE时,务必加上ORDER BY primary_key,确保加锁顺序一致,避免因扫描顺序不同引发死锁
触发器里调用存储函数也可能锁表
你以为把复杂逻辑封装成函数就安全了?错。只要函数体内有 SELECT、UPDATE 等 DML,它在触发器里被调用时,锁行为完全继承上下文——也就是说,函数不是“隔离执行单元”,它和触发器共享同一事务和锁空间。
常见错误现象:一个看似无害的 get_user_status(user_id) 函数,在高并发 INSERT 触发器里被频繁调用,结果 users 表的热点行被反复加锁,吞吐量断崖下跌。
- 函数内禁止访问除参数外的任何表,除非你明确知道它会被用在触发器里,且已评估过锁影响
- 若函数必须查表,优先走缓存(如应用层预加载到内存),或改用
SELECT ... INTO @var FROM DUAL返回常量 - MySQL 8.0+ 支持将函数标记为
READS SQL DATA或NO SQL,但这只是语法提示,不改变实际锁行为
替代方案比硬扛触发器更可靠
很多需求本质是“数据变更后同步动作”,比如写 binlog、发消息、更新搜索索引。这类场景下,触发器不是唯一解,反而最容易出问题。
真正可控的做法是:把逻辑从数据库层上提到应用层,用事务 + 异步任务兜底。例如,在应用里用 INSERT INTO users 后立即发一条 Kafka 消息,下游消费者负责写日志、更新 ES;失败可重试,不拖慢主流程。
- 触发器无法回滚外部系统操作(比如已经发了 MQ 消息),而应用层事务可以统一管理 DB + MQ 生产者
- 触发器调试困难,日志分散,线上出问题几乎没法实时 trace;应用层有完整链路追踪和监控
- MySQL 主从复制中,触发器在从库是否执行取决于
binlog_format和log_bin_trust_function_creators,稍不注意就导致主从数据不一致
最麻烦的不是写不出来,而是写出来之后,没人敢动、不敢删、出了问题查三天还找不到源头 —— 尤其当多个触发器嵌套调用、又混着自定义函数时,锁行为基本不可预测。











