典型表现为订单插入成功但库存未扣减,或直接报error 1442;根本原因是after触发器中update同一表被禁止,正确做法是改用before触发器+select ... for update加锁校验并set new.quantity确保原子性。

INSERT 订单时库存扣减失败的典型表现
订单插入成功但库存没变,或者直接报错 ERROR 1442: Can't update table 'inventory' in stored function/trigger because it is already used by statement which invoked this stored function/trigger。这是 MySQL 触发器里最常踩的坑:在 AFTER INSERT 触发器中对同一事务涉及的表做 UPDATE,会触发“表正在被使用”限制。
正确做法是改用 BEFORE INSERT,并在触发器里直接修改 NEW 引用的行数据(不额外 UPDATE inventory),同时用 SELECT ... FOR UPDATE 显式加行锁防并发超卖:
CREATE TRIGGER tr_order_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
DECLARE current_stock INT DEFAULT 0;
SELECT stock INTO current_stock
FROM inventory
WHERE product_id = NEW.product_id
FOR UPDATE;
IF current_stock <h3>UPDATE 订单数量时库存需双向校正</h3><p>用户改订单数量,比如从 2 改成 5,库存得再扣 3;但如果改成 1,就得回补 1。不能只写“减去新数量”,否则会多扣或少扣。</p><p>关键点在于拿到旧值——<code>OLD.quantity</code> 只在 <code>UPDATE</code> 触发器里可用,且必须用 <code>BEFORE UPDATE</code> 才能安全读取原订单行(<code>AFTER</code> 时原行已不可见):</p>
- 先查出原订单对应的
OLD.quantity和当前库存 - 计算差值:
NEW.quantity - OLD.quantity - 若差值 > 0,检查是否足够扣减;若
- 最后统一执行
UPDATE inventory SET stock = stock - diff
注意:这里仍需 SELECT ... FOR UPDATE 锁住 inventory 行,否则并发更新订单可能引发库存错乱。
DELETE 订单时库存回滚要防重复操作
删订单=释放库存,看似简单,但容易漏掉两个边界:
- 订单状态不是“已取消”或“未支付”,而是“已完成”后又被误删——这种不该回库
- 触发器没判断订单是否真影响了库存(比如赠品订单 quantity=0)
所以 BEFORE DELETE 触发器里必须加条件判断:
IF OLD.status IN ('cancelled', 'pending') THEN
UPDATE inventory SET stock = stock + OLD.quantity
WHERE product_id = OLD.product_id;
END IF;
另外,MySQL 不允许在触发器里对被删除的表再做 SELECT(会报错 Table 'orders' is specified twice),所以状态字段必须存在原行中,不能靠子查询去查。
跨库或分表场景下触发器完全失效
如果 orders 和 inventory 不在同一个数据库,或者用了 MyCat、ShardingSphere 这类中间件,MySQL 原生触发器根本不会跨库执行——inventory 表的变更压根不会发生。
此时必须放弃触发器,改用应用层事务兜底:在插入订单前,用一个 SELECT ... FOR UPDATE 查库存并扣减,再插入订单,整个过程包在同一个事务里。或者引入消息队列,由订单服务发消息,库存服务监听并异步更新,但要额外处理消息幂等和补偿。
真正上线前,一定要确认你的部署架构是否支持触发器跨表生效——很多团队直到压测才发现库存对不上,就是卡在这一步。











