MySQL 触发器本质上是一种绑定在数据表上的事件驱动型存储过程:当执行 INSERT、UPDATE、DELETE 等 DML 操作时,它会自动触发执行,既不能传入参数,也不能由开发者手动显式调用。理解 MySQL 触发器时,需要重点把握几个核心特征:第一,触发时机分为 BEFORE 和 AFTER;第二,必须按照 FOR EACH ROW 逐行执行;第三,在事务范围内运行时,通常会随着事务一起提交或回滚。不过也不能误解它的能力边界,像外键级联删除以及 TRUNCATE 这类操作并不会触发触发器;而一旦出现嵌套触发,死锁风险和性能放大问题也往往会随之出现。

MySQL触发器是一种自动执行的存储过程,绑定在数据表上,由 INSERT、UPDATE、DELETE 事件隐式触发;但它绝不是“万能钩子”,使用不当很容易破坏数据一致性,甚至引发严重的性能问题。
触发器本质:不是函数,也不是中间件业务逻辑
MySQL 触发器没有直接调用入口,不支持传参,也不能通过CALL执行,它只会响应行级 DML 事件。其核心特点主要有三点:
BEFORE或AFTER决定触发时机:例如BEFORE INSERT可以修改NEW.column的值,而AFTER UPDATE更适合做操作日志或审计记录- 必须显式指定
FOR EACH ROW:即使一条INSERT ... VALUES (...), (...)语句一次插入100行数据,触发器也会逐行执行100次 - 作用范围通常仅限当前事务:如果触发器内部报错(例如
INSERT INTO nonexistent_table),原始 DML 语句一般会整体回滚——但这种“安全保障”通常只在单条语句上下文内成立
外键级联删除绕过触发器,是 MySQL 中非常常见的坑
当父表设置了ON DELETE CASCADE后,删除父表记录时,子表中被级联删除的行,不会触发子表上的DELETE触发器。常见现象就是:你明明写了删除审计日志触发器,结果日志系统里根本没有这些删除记录。
- 验证方式:如果直接对子表执行
DELETE FROM child WHERE id = ?,日志会正常出现;但执行DELETE FROM parent WHERE id = ?后,子表虽然被删,日志却是空的 - 补救方案:要么把外键约束调整为
ON DELETE RESTRICT,由业务层显式先删子表、再删父表;要么在父表的AFTER DELETE触发器中手动INSERT日志记录 - 还要注意:
TRUNCATE TABLE同样会绕过所有触发器,并且在某些场景下不记录 binlog(ROW 格式下),同时也无法回滚
嵌套触发和死锁风险确实存在,不能低估
如果在一个触发器内部继续修改其他表,就可能进一步激活另一张表上的触发器,从而形成链式调用。MySQL 默认最多允许16层嵌套(max_sp_recursion_depth),但在真实生产环境中,很多时候两层嵌套就已经足以引发问题。
- 典型死锁场景:A表的
AFTER UPDATE去更新B表,而B表的BEFORE UPDATE又反过来查询A表中已被锁住的行 - 性能雪球效应:例如执行
UPDATE t1 SET x=1 WHERE y>1000影响5万行,如果触发器里每一行都执行INSERT INTO log_table,就会瞬间产生5万条日志,导致磁盘 IO 和锁竞争迅速飙升 - 调试难度高:报错堆栈通常不会完整展示触发器调用链,
SHOW ENGINE INNODB STATUS里很多时候也只能看到最后被卡住的 SQL,真正的根因往往很难快速定位
事务边界不清,容易造成“看似成功、实则半生效”
虽然触发器代码和主操作通常运行在同一个事务中,但它带来的“外部副作用”并不一定能被事务完全兜底。比如发送消息、调用外部 API,这类动作一旦发出,即使后续事务回滚,也无法撤回。另一个常被忽略的问题是:在触发器中直接执行START TRANSACTION会报错;而如果INSERT INTO t2执行失败,很多情况下也只是让当前这条记录对应的触发器逻辑失败,不一定会影响主 DML 对其他行的处理,不过这一点通常只适用于AFTER触发器的相关场景。
- 例如:在
AFTER INSERT ON orders中循环插入10条日志,第5条因为唯一键冲突失败 → 前4条已经写入,后5条没有写入,但原始订单插入仍然成功 - 优化思路:所有关键副作用都应尽量和主表操作放在同一个
INSERT/UPDATE/DELETE语句语义内完成,避免拆成多个步骤分别提交 - 真正更危险的是在
BEFORE触发器中执行复杂计算后异常退出:这可能导致NEW字段被部分修改,进而让后续逻辑读取到不完整的中间状态
MySQL 触发器真正复杂的地方并不在语法本身,而在于它会把原本清晰可控的应用层事务边界,悄悄折叠进数据库内核的执行路径中——一旦涉及多表联动、异步操作或者异常分支处理,几乎必然会遇到“看起来执行了,其实没有真正生效”或“表面上没有报错,但数据实际上已经出错”的情况。
