触发器自动加入当前事务
MySQL 触发器本身不开启新事务,而是**完全依附于触发它的 SQL 语句所处的事务**。也就是说,
INSERT、
UPDATE或
DELETE如果在显式事务中执行(比如包裹在
BEGIN/
COMMIT里),那么该语句激活的所有触发器逻辑都会被包含在这个事务里;如果语句是自动提交模式下的单条执行,那触发器也就在那个隐式事务中运行。
这意味着:触发器内抛出异常(比如通过
SIGNAL)、或触发器里的语句失败(如违反外键、唯一约束),都会导致整个外部事务回滚——不只是触发器代码,还包括原始 DML 语句本身。
不能在触发器里用 START TRANSACTION / COMMIT / ROLLBACK
MySQL 明确禁止在触发器中执行事务控制语句:
START TRANSACTION、
COMMIT、
ROLLBACK、
SAVEPOINT都会直接报错
ERROR 1305 (42000): SAVEPOINT does not exist或类似提示(实际错误码可能是 1305 或 1295)。
这是硬性限制,不是配置问题。原因在于触发器必须保持事务上下文透明,否则会破坏原子性保证。
想“局部回滚”某段逻辑?不行——只能靠提前校验或用IF+
LEAVE控制流程 想记录日志但不影响主事务?得用
INSERT ... ON DUPLICATE KEY UPDATE或写入非事务引擎表(如
MyISAM),但后者已不推荐 依赖
SELECT ... FOR UPDATE加锁?可以,只要锁范围合理,它会和主事务一起提交或回滚
BEFORE 和 AFTER 触发器对事务行为的影响差异
BEFORE触发器能修改即将插入/更新的行(通过
NEW.col = ...),且如果它出错(比如
SIGNAL报错),原始语句根本不会执行;而
AFTER触发器无法修改原数据(
NEW只读),但它执行时原始语句已经成功(至少逻辑上完成),所以它的失败会导致整个事务回滚——包括刚成功的那条 DML。
典型陷阱:
在AFTER INSERT里调用存储过程写审计日志,结果日志表字段长度不够 → 整个
INSERT失败 在
BEFORE UPDATE里做复杂计算并赋值给
NEW.price,但计算过程除零 → 更新直接被拦住,不进表 误以为
AFTER是“事后补救”,其实它和
BEFORE同样具有事务强一致性
跨表操作与死锁风险升高
触发器常用来同步其他表(比如订单变更后更新用户积分)。这类操作会隐式增加锁持有范围和时间,尤其当触发器里有
UPDATE或
SELECT ... FOR UPDATE时,极易引发死锁。
例如:
UPDATE orders SET status = 'shipped' WHERE id = 123激活一个
AFTER UPDATE触发器去
UPDATE users SET points = points + 100 WHERE id = 567。如果另一事务正按相反顺序访问
users再访问
orders,就可能卡住。
应对建议:
尽量避免在触发器中做多表更新,优先考虑应用层异步处理 确保所有跨表操作遵循固定顺序(比如总是先锁users再锁
orders) 监控
SHOW ENGINE INNODB STATUS中的
LATEST DETECTED DEADLOCK区域 测试时用
SELECT SLEEP(1)模拟延迟,更容易复现竞争条件 触发器的事务边界很清晰,但正因为“透明”,反而容易忽略它对整体事务行为的放大效应——一行看似简单的
INSERT,背后可能牵扯四张表加锁、三次校验、两次信号抛出,任何一环出问题都会让整个事务倒退。
