零超卖的核心是确保同一商品库存更新的原子性与一致性,需依业务特征选悲观锁(SELECT FOR UPDATE)、乐观锁(版本号/CAS)或Redis预减+Lua脚本等方案,禁用内存变量或伪原子操作。

在 Java 多线程并发扣减库存场景中,零超卖的核心是**确保同一商品的库存更新具备原子性与一致性**。悲观锁适合高冲突、低并发写场景,乐观锁适合低冲突、高并发读场景。两者不是互斥方案,而是根据业务特征选择或组合使用。
悲观锁:数据库行级锁 + 事务强控制
本质是在操作前加锁,阻塞其他线程对同一行的修改,由数据库保证串行化执行。
-
实现方式:在 SQL 中使用
SELECT ... FOR UPDATE(InnoDB 行锁),必须在事务内执行,且 WHERE 条件需命中索引(如主键或唯一索引),否则会升级为表锁 -
典型流程:
- 开启事务
- 查库存:
SELECT stock FROM t_product WHERE id = ? FOR UPDATE - 判断是否充足(Java 层)
- 充足则执行
UPDATE t_product SET stock = stock - 1 WHERE id = ? AND stock >= 1 - 提交事务;若库存不足,回滚并返回失败
- 注意点:避免长事务(如锁住后做远程调用),防止锁持有时间过长引发性能瓶颈或死锁;FOR UPDATE 仅对已存在记录加锁,插入前需先查再锁,或配合唯一约束+重试
乐观锁:版本号 / CAS 检查 + 更新失败重试
不加锁,靠数据版本或条件判断来检测并发冲突,冲突时主动放弃或重试,适合读多写少、冲突概率低的场景。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
版本号方式(推荐):表中增加
version字段(初始为 0),每次更新都校验并自增- SQL 示例:
UPDATE t_product SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ? AND stock >= 1 - Java 中用
updateCount判断是否更新成功(=1 成功,=0 失败),失败则重查最新 stock & version 后重试(建议限制重试次数,如 3 次)
- SQL 示例:
-
CAS 式条件更新(无版本字段):直接用库存值做条件(如
AND stock = ?),但需注意:该方式在“查-判-更”之间库存可能已被其他线程改过,容易漏判;不如版本号语义清晰、扩展性强
Redis 分布式场景下的补充策略
当库存服务拆分为分布式系统,单机数据库锁失效,需引入 Redis 做前置校验或分布式锁:
立即学习“Java免费学习笔记(深入)”;
-
Redis 预减库存(非最终态):用
DECR或DECRBY原子操作扣减 Redis 中的库存值,成功再走 DB 扣减;失败直接拒绝。注意 Redis 和 DB 库存需异步对账补偿 -
Redis 分布式锁(慎用):如用
SET key value NX PX 5000加锁,再查 DB 库存并扣减;但锁粒度大、性能差、易出现锁误删,一般不推荐用于高频库存扣减,更适合订单生成等低频强一致操作 - Redis + Lua 脚本原子校验扣减:将“判断库存 ≥1 并 DECR”封装进 Lua 脚本,借助 Redis 单线程特性保证原子性,作为高效前置过滤手段
选型与避坑关键点
没有银弹,关键看压测结果和业务容忍度:
- 秒杀类超高并发(QPS 万级+)、库存极少 → 优先 Redis 预减 + DB 最终落库 + 异步补偿,乐观锁兜底
- 普通电商下单(QPS 千级、库存较充足)→ 数据库乐观锁(version)为主,简单可靠,重试成本低
- 后台管理类低频强一致操作(如人工调拨)→ 悲观锁更直观,避免重试逻辑复杂化
- 绝对禁止:纯 Java 内存变量(如 static int stock)做库存计数;或只查不锁、查完再 update 的“伪原子”写法

















