必须在update的where条件中加入stock >= num校验,否则直接update会导致负库存;执行后检查影响行数,为0表示库存不足需拒单。

UPDATE 时用 WHERE 防止超扣
直接 UPDATE 库存字段而不加条件,是负库存的最常见源头。比如 UPDATE goods SET stock = stock - 1 WHERE id = 123,哪怕当前 stock 是 0,它也会变成 -1。
必须把库存是否足够的判断写进 WHERE 条件里:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
执行后检查 ROW_COUNT()(MySQL)或 pg_affected_rows()(PostgreSQL),返回 0 表示没扣成功——不是失败,而是库存不足,业务层该拒单就拒单。
用 SELECT FOR UPDATE 做行级锁
高并发下,即使加了 WHERE stock >= 1,仍可能因“读-改-写”间隙出现超扣。比如两个事务同时查到 stock = 1,都通过 WHERE 判断,然后都执行减 1,结果变成 -1。
解决方法是在 UPDATE 前显式加锁:
BEGIN; SELECT stock FROM goods WHERE id = 123 FOR UPDATE; -- 检查 stock 是否 >= 1,再执行 UPDATE UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1; COMMIT;
注意:FOR UPDATE 必须在事务中,且 WHERE 条件要走索引(如 id 主键),否则会锁表;PostgreSQL 还支持 SKIP LOCKED 避免阻塞。
用 RETURNING 或 OUTPUT 返回实际扣减结果
光靠 ROW_COUNT() 不够细——你不知道扣完后还剩多少,也不知道是不是真扣了。用数据库原生的返回机制更可靠:
- PostgreSQL:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1 RETURNING stock; - SQL Server:
UPDATE goods SET stock = stock - 1 WHERE id = 123 AND stock >= 1 OUTPUT INSERTED.stock; - MySQL 8.0+:不支持 RETURNING,得靠两次查询或触发器兜底
拿到返回值后,业务逻辑可立刻判断是否需要回滚、告警或补偿。
避免在应用层做“先查再更”的乐观判断
这是最容易踩的坑:先 SELECT stock FROM goods WHERE id = 123,再 if stock > 0 就 UPDATE。这个“查”和“更”之间没有原子性,任何并发请求都能插进来。
除非你加了分布式锁或版本号(如 version 字段 + WHERE version = ?),否则这种写法在真实流量下必然出负数。
真正安全的做法只有两种:一是 WHERE stock >= N 原子判断,二是 SELECT ... FOR UPDATE 加锁。前者简单高效,后者适合复杂校验逻辑。
扣减逻辑越靠近数据库,越不容易翻车;把库存状态当普通变量在代码里算,迟早出事。











