触发器不能直接回滚事务,必须用signal抛异常中止父事务;其运行在父事务上下文中,无权开启/结束事务,signal是mysql 5.5+唯一可靠方式。

触发器里不能直接回滚事务,必须用 SIGNAL 抛出异常
SQL 触发器本身没有 ROLLBACK 权限——它运行在父事务上下文中,无法主动终止事务。真正起作用的是让触发器执行失败,迫使外层事务感知错误并回滚。MySQL 5.5+ 支持 SIGNAL 语句,这是唯一可靠方式;PostgreSQL 则用 RAISE EXCEPTION;SQLite 不支持触发器内抛错,需靠应用层兜底。
常见错误是写 IF stock —— 这在 MySQL 中会直接报错 <code>ERROR 1305 (42000): SAVEPOINT does not exist 或更隐蔽的静默失败。
- MySQL 必须用
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock'; - PostgreSQL 触发器函数中需写
RAISE EXCEPTION 'Insufficient stock'; - SQL Server 的
THROW或RAISERROR同理,但注意RAISERROR需搭配WITH LOG才能中断事务
库存扣减逻辑必须放在 BEFORE UPDATE / BEFORE INSERT 触发器中
如果在 AFTER 触发器里检查库存,数据已变更,回滚代价高且可能引发并发问题(比如两个并发事务都读到“足够”,都进入 AFTER,再各自扣减,超卖就发生了)。BEFORE 是唯一安全时机。
典型场景:订单表插入前检查商品表库存,或更新订单状态为「已支付」时扣减库存。
- MySQL 示例:
CREATE TRIGGER check_stock_before_order BEFORE INSERT ON orders FOR EACH ROW BEGIN ... END; - 务必用
SELECT stock INTO @cur_stock FROM products WHERE id = NEW.product_id FOR UPDATE;加行锁,避免并发读写冲突 - 不要用子查询直接比较:
NEW.quantity > (SELECT stock FROM products ...)—— 这不加锁,有竞态风险
触发器中读取库存必须显式加锁(FOR UPDATE)
单纯 SELECT 不会阻塞其他事务修改同一行,导致「幻读式」超卖:事务 A 读到 stock=10,事务 B 同时扣减 8,A 再扣 5 就变成 -3。只有 SELECT ... FOR UPDATE 能保证当前读+写锁。
注意:FOR UPDATE 在触发器里有效,但只对当前语句生效;若触发器内有多条 SELECT,每条都得加锁,或合并成一条带锁查询。
- 正确:
SELECT stock FROM products WHERE id = NEW.product_id FOR UPDATE; - 错误:
SELECT stock FROM products WHERE id = NEW.product_id;(无锁) - 错误:
SELECT stock FROM products WHERE id = NEW.product_id LOCK IN SHARE MODE;(共享锁,不阻止其他事务扣减)
触发器无法替代应用层的库存预占和分布式一致性
单机数据库触发器只解决本地事务一致性。一旦涉及分库、微服务、缓存穿透或跨库订单(比如订单库和商品库分离),触发器完全失效。此时必须用 TCC、Saga 或库存服务 + Redis 原子操作(DECRBY + GET)来兜底。
另外,触发器不记录日志、难调试、无法做异步通知——比如扣减成功后要发 Kafka 消息,就得在应用层补全。
- 触发器适合:单体架构、强一致性要求、低并发写入的内部系统
- 不适合:高并发秒杀、多数据中心、需要审计溯源、与外部系统联动的场景
- 最容易被忽略的一点:MySQL 触发器不支持事务嵌套,若外层事务已回滚,触发器里的
SIGNAL不会再触发任何清理逻辑










