必须用before insert触发器结合update ... where stock >= new.quantity原子扣减并校验,若row_count()为0则signal抛异常中断插入;after insert无法回滚,且严禁拆分查库与扣减操作以防竞态。

INSERT 触发器扣减库存前必须检查库存是否充足
订单插入时直接扣减库存,但若不校验余量,超卖就成定局。MySQL 的 BEFORE INSERT 触发器是唯一能阻断非法插入的时机——AFTER 已无法回滚语句本身。
常见错误是把校验和更新拆成两步,在高并发下出现“查到有货→别人抢走→自己还扣”的竞态。必须用单条 UPDATE ... WHERE stock >= new.quantity 原子判断+扣减:
CREATE TRIGGER tr_order_insert BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
UPDATE products SET stock = stock - NEW.quantity
WHERE id = NEW.product_id AND stock >= NEW.quantity;
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock';
END IF;
END;
-
ROW_COUNT()返回实际更新行数,为 0 表示 WHERE 条件不成立(库存不足或商品不存在) - 务必用
SIGNAL抛异常,不能只写SELECT或设变量——那不会中断插入 - 该触发器不处理
product_id不存在的情况,需额外在UPDATE前加EXISTS子查询或靠外键约束兜底
UPDATE 触发器要区分订单状态变更场景
订单可能从「待支付」变「已取消」,这时得把扣掉的库存还回去;也可能从「待发货」变「已完成」,但库存早已扣过——重复操作会出错。
关键不是监听所有 UPDATE,而是聚焦字段变化:
一款AI工具,主要用于管理 OpenClaw 所使用的来自 OpenRouter 的免费 AI 模型。自动按质量对模型进行排序,配置回退机制以应对速率限制,并更新 opencla...,适合需要提升相关任务效率的用户。
CREATE TRIGGER tr_order_update AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status != 'cancelled' AND NEW.status = 'cancelled' THEN
UPDATE products SET stock = stock + OLD.quantity
WHERE id = OLD.product_id;
END IF;
END;
- 只响应
status从非 cancelled 变为 cancelled 的情况,避免误恢复 - 用
OLD.quantity而非NEW.quantity,防止用户改数量再取消导致多退 - 不要在
BEFORE UPDATE里做库存操作:若触发器抛错,订单状态也更新不了,业务逻辑断裂
DELETE 触发器几乎不该存在
真实系统中,订单记录极少物理删除,基本都软删(加 is_deleted 字段)。强行加 DELETE 触发器恢复库存,反而制造风险:
- 管理员误删订单 → 库存自动回补 → 客户侧已发货,账实不符
- 归档脚本批量删历史订单 → 触发器逐行更新,拖垮数据库
- 外键级联删除时,触发器执行顺序不可控,可能读到中间态数据
真正需要的是在业务层明确:「仅允许取消订单,禁止删除订单」。数据库只负责约束,不替业务做决策。
事务隔离级别影响触发器行为
即使触发器逻辑正确,若应用没开启事务或用了 READ COMMITTED,仍可能超卖。原因很简单:两个并发插入请求同时执行 SELECT stock FROM products,都看到足够库存,接着都通过 UPDATE ... WHERE stock >= X 扣减成功。
- MySQL 默认
REPEATABLE READ能避免该问题,因为UPDATE会加临键锁(next-key lock),阻塞其他会修改同一行的事务 - PostgreSQL 需显式用
SELECT ... FOR UPDATE,但触发器里不能直接写——得把库存校验提到应用层或改用存储过程 - 哪怕用了正确隔离级别,也要在应用层捕获
SQLSTATE '45000'异常并重试,不能假设一次必成
触发器不是银弹。它把库存一致性逻辑锁死在数据库层,但并发、分布式、补偿这些事,终究得靠上层设计兜住。










