MySQL 触发器中禁止事务控制语句,不可使用 START TRANSACTION、COMMIT 或 ROLLBACK;NEW/OLD 有读写限制与 NULL 赋值陷阱;禁用耗时操作、远程调用及跨表查询;调试困难,须严格测试验证。

触发器里不能写事务控制语句
MySQL 触发器运行在主 SQL 语句的事务上下文中,BEGIN……END 块内不允许出现 START TRANSACTION、COMMIT 或 ROLLBACK。一旦写了,会直接报错 ERROR 1305 (42000): SAVEPOINT does not exist 或更常见的 ERROR 1305 (42000): FUNCTION does not exist(因解析失败误报函数不存在)。
实际业务中若需“失败回滚整个操作”,必须依赖外层事务;若需“局部补偿”,得改用存储过程 + 应用层协调,或把逻辑拆到应用代码里。
- 触发器中调用的存储过程也不能含
COMMIT -
SELECT …… FOR UPDATE在触发器里可用,但要小心死锁——尤其多表更新时 - 想记录日志又怕影响性能?别在触发器里写
INSERT INTO audit_log,改用异步消息或延迟写入
NEW 和 OLD 的只读性与 NULL 边界问题
NEW 在 BEFORE INSERT 和 BEFORE UPDATE 中可赋值,用于修改即将插入 / 更新的行;OLD 在 UPDATE 和 DELETE 中只读。但很多人忽略:对 NEW.col 赋值时,如果该列是 NOT NULL 且你赋了 NULL,MySQL 不会报错,而是静默转为空字符串或默认值——取决于 SQL mode。
例如:SET NEW.status = NULL; 对 status VARCHAR(20) NOT NULL DEFAULT 'active' 列,结果是 'active',不是预期的空值。
- 检查
sql_mode,确认是否含STRICT_TRANS_TABLES,否则赋值失效不易察觉 -
NEW.col赋值后,后续触发器或主语句仍以该值为准,但不会触发二次校验(如 CHECK 约束) - 在
AFTER触发器中访问NEW没问题,但不能修改——会报ERROR 1362 (HY000): Updating of NEW row is not allowed in after trigger
避免在触发器中调用耗时函数或远程服务
触发器执行是同步阻塞的,哪怕只是调用一个 SLEEP(0.1),也会让整条 INSERT 延迟 100ms。更常见的是误用 UUID()、RAND()、SYSDATE(),或在触发器里拼接 HTTP 请求(通过 sys_exec 或 UDF),导致连接池打满、主从延迟飙升。
真实案例:某订单表 BEFORE INSERT 触发器调用 GET_LOCK('order_seq', 3) 生成单号,高并发下锁争用严重,TPS 从 1200 掉到 80。
- 时间类函数优先用
NOW()(事务开始时间),而非SYSDATE()(执行时刻),避免主从不一致 - 需要唯一编号?用自增 ID + 业务前缀,或提前在应用层生成,别在触发器里查表 + 加锁
- 绝对不要在触发器里调用
SELECT …… FROM remote_db.table—— 即使是同实例,跨库查询也可能引发元数据锁等待
触发器调试难,上线前必须做三件事
MySQL 没有触发器断点调试能力,错误信息也常模糊(比如 ERROR 1442 (HY000): Can't update table't' in stored function/trigger because it is already used by statement which invoked this stored function/trigger)。靠日志和复现成本极高。
上线前务必验证:
- 在测试库开
general_log = ON,手动执行触发语句,确认日志里没有意外的嵌套INSERT/UPDATE - 用
SHOW TRIGGERS LIKE 'table_name';核对触发时机(BEFORE/AFTER)、事件(INSERT/UPDATE/DELETE)和定义体是否和预期一致 - 压测时监控
Threads_running和Innodb_row_lock_waits,若二者同步上涨,大概率是触发器引发锁竞争
复杂逻辑真没必要硬塞进触发器——它适合轻量、确定、无副作用的操作。业务越重,越容易变成线上故障的隐藏开关。






























