正确做法是UPDATE时将库存校验纳入WHERE条件,如UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1,并严格校验影响行数;必须使用索引字段、避免函数/类型转换、禁用SELECT+UPDATE组合,扣减量需动态传入而非硬编码。

UPDATE WHERE 条件必须包含库存校验
直接 UPDATE product SET stock = stock - 1 WHERE id = 123 在高并发下必然超卖——多个事务读到同一库存值(比如 1),各自减 1 后都写回 0,实际应扣减失败。
正确做法是把库存是否充足作为更新条件的一部分,让数据库在执行时原子判断:
UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
执行后检查 ROW_COUNT()(MySQL)或 pg_affected_rows()(PostgreSQL)返回值:若为 0,说明条件不满足(库存不足或记录不存在),业务需拒绝下单。
避免幻读导致的重复扣减要加行锁
仅靠 WHERE 条件还不够。如果表没主键/唯一索引,或 WHERE 匹配多行,InnoDB 可能升级为间隙锁甚至临键锁,引发死锁或性能抖动;更危险的是,若用非索引字段查询(如 WHERE sku_code = 'A001' 但该字段无索引),会触发全表扫描+锁表,彻底拖垮并发能力。
确保以下三点:
-
WHERE中的字段必须走聚簇索引(通常是主键)或覆盖索引 - 禁止在 WHERE 中使用函数、表达式或隐式类型转换(如
WHERE id = '123'而 id 是 INT) - 执行前用
EXPLAIN确认type是const或eq_ref,key显示命中索引
UPDATE 返回值必须被业务层严格校验
很多开发者只看 SQL 是否报错,忽略“执行成功但没更新任何行”这种静默失败。这在库存场景等于放行了超卖。
不同数据库获取影响行数的方式不同,必须适配:
- MySQL:调用
mysqli_affected_rows()或 PDO 的rowCount() - PostgreSQL:用
RETURNING子句(更推荐):UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1 RETURNING stock;
这样还能立刻拿到扣减后的剩余库存,避免二次查询 - SQL Server:用
@@ROWCOUNT
不要依赖 SELECT + UPDATE 组合实现“先查后更”
哪怕加了 SELECT ... FOR UPDATE,在应用层做判断再发 UPDATE,中间仍存在时间窗口:另一个事务可能在你 SELECT 之后、UPDATE 之前完成扣减。这不是原子操作,只是“伪原子”。
真正可靠的只有单条 UPDATE 带条件的写法。如果业务逻辑复杂(比如要扣减多个商品、涉及优惠券状态),优先考虑用数据库存储过程封装整个事务,或改用乐观锁(版本号 + 重试),但注意重试次数要设上限,避免雪崩。
最易被忽略的一点:WHERE 中的 stock >= ? 必须传入原始待扣数量(如扣 3 件就写 stock >= 3),而不是硬编码 1——这个数值错一次,整个库存模型就失效了。










