库存字段必须用bigint而非int,因int溢出会导致负数库存被误判为有货;扣减需原子化update where stock>=n并检查row_count();高并发下须配合repeatable read与行锁;补货盘点需乐观锁+version字段。

库存字段必须用 BIGINT 而不是 INT
超卖问题往往不是逻辑写错了,而是 INT 溢出导致的负数库存被当成“有货”。比如某商品销量过千万,INT 最大值 2147483647,但扣减时若并发高、没加锁,可能算出 -100 这种值,而业务层又只判 stock > 0,结果继续下单。
实操建议:
- 库存字段类型统一用
BIGINT,预留足够增长空间 - 加
CHECK (stock >= 0)约束(MySQL 8.0.16+ 支持),防止非法负值写入 - 避免用
TINYINT或SMALLINT存“件数”,哪怕初期量小——改类型要锁表,线上不敢动
扣减库存必须走 UPDATE ... WHERE stock >= ?
先 SELECT 再 UPDATE 是经典幻读陷阱。两个请求同时查到 stock = 1,都进判断,然后都执行 UPDATE SET stock = 0,最终变成 -1。
正确做法是把校验和更新合并成一条原子语句:
UPDATE inventory SET stock = stock - 1 WHERE sku_id = 123 AND stock >= 1;
执行后检查 ROW_COUNT():
- 返回 1 → 扣减成功
- 返回 0 → 库存不足或已被抢完,直接拒绝下单
注意:这个 WHERE 条件里不能只写 stock > 0,否则初始为 0 时无法拦截首次扣减;必须是 stock >= N(N 为你本次要扣的数量)
高并发下需要行级锁 + 事务隔离级别配合
光靠 WHERE stock >= 1 不够。如果事务隔离级别是 READ COMMITTED,在两次 UPDATE 之间,另一事务可能已把 stock 改成 0,但当前事务仍基于旧快照判断,导致扣减穿透。
关键控制点:
- 确保事务开启(显式
BEGIN),且结束前不释放锁 - 推荐用
REPEATABLE READ(MySQL 默认),配合UPDATE语句自动加行锁(Next-Key Lock),能挡住并发修改同一sku_id的请求 - 不要在事务里做 HTTP 调用、日志写入等耗时操作——锁持有时间越长,排队越严重
补货和盘点需用乐观锁防覆盖
运营后台批量补货、财务对账盘点,常会直接 UPDATE inventory SET stock = ?。这时如果多个操作同时写,后写的会覆盖先写的,造成数据丢失。
解决方案是加版本号字段 version:
UPDATE inventory SET stock = 1000, version = version + 1 WHERE sku_id = 123 AND version = 5;
如果返回影响行数为 0,说明期间被别人改过,需重读最新 stock 和 version,再计算新值重试。
注意:version 初始值必须为 0,且所有写操作(包括扣减)都要带上它,否则乐观锁形同虚设
真正难的不是建表,而是让每个写路径——扣减、补货、冻结、回滚——都落在同一套锁机制和一致性校验下。漏掉任意一个,库存就可能漂移。











