库存扣减核心是保证数据一致性防超卖,关键在表设计(如UNSIGNED stock、version乐观锁)与并发控制(UPDATE+WHERE校验或SELECT FOR UPDATE),并可通过lock_stock预占机制提升秒杀场景准确性。

库存扣减逻辑的核心是保证数据一致性,避免超卖。MySQL 实现的关键不在于“怎么写 SQL”,而在于“怎么设计表 + 怎么控制并发”。下面从表结构设计、扣减逻辑实现、常见陷阱三方面说清楚。
库存表基础字段设计(含关键约束)
一张精简但健壮的库存表至少包含以下字段:
- id:主键,自增或业务 ID
- sku_id:商品唯一标识(建议加唯一索引)
- stock:当前可用库存(red">必须为 UNSIGNED INT,防止负数)
- lock_stock:已预占库存(用于下单锁定,非必需但推荐)
- version:乐观锁版本号(INT,默认 0,每次更新 +1)
- updated_at:最后更新时间(便于排查和监控)
重要约束:stock 字段设为 UNSIGNED,配合 WHERE stock >= #{need} 条件,可天然拦截负库存更新;同时在事务中用 SELECT ... FOR UPDATE 或 UPDATE ... WHERE stock >= #{need} 做原子校验。
安全扣减的两种主流实现方式
不推荐直接 SELECT 后 UPDATE(存在竞态),应采用原子操作:
-
方案一:UPDATE + WHERE 校验(推荐初/中级场景)
UPDATE inventory SET stock = stock - #{num}, updated_at = NOW() WHERE sku_id = #{skuId} AND stock >= #{num};
执行后检查 影响行数是否为 1。为 0 表示库存不足或已被扣完,业务层直接拒绝下单。 -
方案二:SELECT FOR UPDATE + UPDATE(适合高一致性要求)
在事务中先查再锁:SELECT stock FROM inventory WHERE sku_id = #{skuId} FOR UPDATE;
检查 stock ≥ num,再执行 UPDATE。注意:该语句必须走索引(如 sku_id 有索引),否则会锁全表。
防超卖进阶:引入预占(lock_stock)与状态协同
秒杀或分布式下单场景下,仅靠最终扣减易导致“下单成功但支付失败却占着库存”。可增加预占机制:
- 用户下单时,先扣减 lock_stock += num,并校验 stock - lock_stock >= num
- 支付成功:真正扣减 stock -= num,并 lock_stock -= num
- 支付失败或超时:异步任务回滚 lock_stock -= num
此时库存可用量 = stock - lock_stock,前端展示和扣减校验都基于该值,兼顾实时性与准确性。
容易忽略但致命的细节
- 事务隔离级别建议用 REPEATABLE READ(MySQL 默认),避免幻读干扰锁范围
- UPDATE 语句必须命中索引,否则 FOR UPDATE 可能升级为表锁,拖垮性能
- 不要依赖应用层重试来解决扣减失败,应明确返回“库存不足”,由前端引导用户刷新或换货
- 高并发下可考虑分库分表(按 sku_id hash),或引入 Redis 预减缓存 + MySQL 最终一致校验










