必须建在订单表上。库存变化是订单操作的结果,触发器应在orders表用BEFORE INSERT/UPDATE确保库存充足,避免超卖和循环更新导致的死锁或ERROR 1442。

触发器该建在订单表还是库存表上
必须建在订单表上。库存变化是订单操作的结果,不是原因。如果建在库存表上,反而会因循环更新导致死锁或数据错乱。典型场景是用户下单、取消、退货,这些动作都发生在订单生命周期里,orders 表才是源头。
常见错误:有人把触发器建在 inventory 表上,然后在应用层手动 UPDATE inventory,结果触发器又去改订单状态,形成双向依赖。数据库报错 ERROR 1442: Can't update table 'inventory' in stored function/trigger because it is already used by statement which invoked this stored function/trigger 就是这个原因。
- 下单(
INSERT INTO orders)→ 减库存 - 取消订单(
UPDATE orders SET status = 'cancelled')→ 加回库存 - 发货完成(
UPDATE orders SET status = 'shipped')→ 不再影响库存(已扣减过)
用 BEFORE 还是 AFTER 触发器
一律用 BEFORE INSERT 和 BEFORE UPDATE。因为要确保库存足够才允许下单,而不是先插入订单再回滚——后者会留下脏数据痕迹,且并发时容易超卖。
例如:用户 A 和 B 同时下单 10 件,库存只剩 12。若用 AFTER,两个订单都插入成功,再触发减库存,最终库存变成 -8;而用 BEFORE,第二个事务会在检查时发现 stock ,直接拒绝插入,返回错误。
-
BEFORE INSERT:查inventory表当前quantity,不够就SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock' -
BEFORE UPDATE:只响应status从'pending'变为'cancelled'的情况,执行加库存 - 避免在触发器里调用存储函数或访问其他大表,否则拖慢主 DML 语句
并发场景下怎么防超卖
单靠触发器不够,必须配合行级锁。在触发器里执行 SELECT quantity FROM inventory WHERE id = ? FOR UPDATE,而不是 SELECT ... LOCK IN SHARE MODE 或无锁查询。否则高并发下多个事务同时读到“还有 5 件”,都会通过校验,然后一起减,实际扣成负数。
MySQL 默认可重复读隔离级别下,FOR UPDATE 会阻塞其他事务对同一行的 SELECT ... FOR UPDATE 或写操作,保证原子性。但要注意:不能在触发器里用子查询间接引用本表(比如 SELECT * FROM orders WHERE product_id = NEW.product_id),会触发 ERROR 1442。
- 触发器内只操作
inventory表,且用FOR UPDATE显式加锁 - 订单表
product_id字段必须有索引,否则FOR UPDATE可能锁整张表 - 不要在触发器里做 HTTP 请求、写日志文件等外部操作——事务回滚时这些无法撤回
触发器没法处理的部分得靠应用层兜底
触发器管不了跨库操作、异步任务(如推送消息)、或者需要复杂业务规则判断的情况。比如“买三送一”要算赠品库存,“预售商品”要区分锁定库存和可用库存,这些逻辑放在触发器里会让它臃肿难维护,也违背单一职责。
真正健壮的做法是:触发器只做最基础的原子扣减与校验;复杂规则由应用在事务内调用库存服务完成;最终一致性靠定时任务或 binlog 监听补偿。别指望一个触发器解决所有库存问题。
最容易被忽略的是:触发器不生效时往往没报错,只是静默跳过。上线前务必用 SHOW TRIGGERS LIKE 'orders' 确认存在,再用 INSERT INTO orders (...) VALUES (...) 手动测试并查 inventory 表是否变更。生产环境曾有 DBA 误删触发器,连续三天超卖都没人发现。

















