应在BEFORE UPDATE触发器中用OLD.quantity减去扣减量(如NEW.order_qty)判断是否为负,而非依赖可能被并发修改的当前quantity值。

触发器里怎么判断扣减后库存会变负
核心是拿到更新前后的库存值做对比,不能只看当前 quantity 字段——因为并发写入时,你读到的“当前值”可能已被其他事务改过。必须在 BEFORE UPDATE 触发器中,用 OLD.quantity 和要扣减的数量(比如来自 NEW.order_qty 或计算值)做运算:OLD.quantity - NEW.order_qty 。注意:别直接用 <code>SELECT quantity FROM goods WHERE id = NEW.goods_id,那是快照读,不可靠。
用 SIGNAL 抛出错误来中断事务
MySQL 5.5+ 支持 SIGNAL,这是最干净的拦截方式。不能靠 UPDATE ... SET quantity = GREATEST(0, quantity - x) 这种“静默修正”,那等于放行了负库存逻辑。必须让整个事务失败回滚:
IF OLD.quantity < NEW.order_qty THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足,无法扣减'; END IF;
关键点:
-
SQLSTATE '45000'是用户自定义异常的标准码,应用层容易识别 - 别用
INSERT INTO error_log ...或ROLLBACK——触发器里不能显式提交或回滚事务 - PostgreSQL 用
RAISE EXCEPTION,SQLite 不支持原生 SIGNAL,得靠应用层补救
为什么 AFTER UPDATE 触发器不行
如果放在 AFTER UPDATE,扣减操作已经落地,哪怕你再抛错,数据库里 quantity 已经变成负数。哪怕事务最终回滚,中间状态仍可能被其他查询读到(取决于隔离级别),且日志、缓存、下游同步等都可能基于这个非法中间态做了动作。只有 BEFORE UPDATE 能确保“不合法变更根本不会发生”。
并发场景下仍可能漏检?那得加 SELECT FOR UPDATE
单靠触发器拦不住高并发下的“检查-执行”竞态(check-then-act)。例如两个事务同时读到库存=1,都判断“1−1≥0”,然后都执行扣减,结果库存变−1。解决方法是在业务 SQL 中显式加锁:
START TRANSACTION; SELECT quantity FROM goods WHERE id = 123 FOR UPDATE; -- 再执行 UPDATE ... SET quantity = quantity - 1; COMMIT;
触发器只是最后一道防线,不是唯一防线。漏掉 FOR UPDATE,再严密的触发器也防不住超卖。
真正难的是把锁粒度、事务边界和触发器逻辑对齐——比如订单拆分多商品时,要锁住所有涉及的 goods.id,顺序还得一致,否则容易死锁。

















