这个问题是电商面试中的高频题,涉及业务一致性与分布式系统核心技术。下面我将为你撰写一篇结构清晰、技术深入的文章,既适合学习,也适合面试准备。
🛒 《面试官:说说电商库存扣减如何防超卖?分布式锁的三种实现》
“秒杀不到,库存没了;秒杀到了,库存却超了。” —— 这是电商最怕的事故之一。
在电商系统中,库存扣减是核心业务,一旦出错,轻则用户投诉,重则资损赔偿。而高并发场景下,如何防止“超卖”(Over-Selling)?答案离不开:分布式锁 + 原子操作 + 幂等性设计。
本文将深入讲解:
- 超卖是如何发生的?
- 如何设计安全的库存扣减流程?
- 三种主流分布式锁实现方式对比
- 面试加分项:Redisson、ZooKeeper、数据库锁
一、什么是“超卖”?
超卖:实际卖出的商品数量 > 库存数量。
示例场景:
- 商品库存:10 件
- 同时 15 人下单成功 → 超卖 5 件
- 结果:无法发货,引发客诉、退款、平台信誉受损
二、超卖的根本原因
原因 | 说明 |
|---|---|
并发读写未加锁 | 多个线程同时读取库存为 10,各自扣减,最终扣成负数 |
非原子操作 | 查询库存 + 判断 + 扣减 三步分离,中间被插队 |
缓存与DB不一致 | Redis 预扣库存,但 DB 更新失败,未回滚 |
重试机制不当 | 消息重试导致重复扣减 |
三、正确库存扣减流程设计
✅ 核心原则:原子性 + 幂等性 + 最终一致性
推荐流程(以秒杀为例):
1. 用户点击购买 → 进入排队队列 2. 检查库存(Redis 预扣) 3. 扣减 Redis 库存(Lua 脚本保证原子性) 4. 发送 MQ 消息 → 异步扣减数据库 5. 返回“抢购中”状态 6. 消费者消费消息 → 扣减 DB 库存 7. 扣减成功 → 通知用户付款 8. 超时未付款 → 回补库存(Redis + DB)
⚠️ 关键点:所有“读-改-写”操作必须原子化,不能拆开!
四、分布式锁的三种实现方式
问:如何保证同一时间只有一个请求能扣减某商品的库存?答:用分布式锁,把“扣减”变成串行操作。
方式一:基于 Redis 的分布式锁(最常用)
实现方式:SET key value NX PX 30000
// 加锁
String lockKey = "stock:lock:" + productId;
String requestId = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (locked) {
try {
// 执行业务:扣减库存
int remain = stockService.deductStock(productId, quantity);
if (remain < 0) throw new RuntimeException("库存不足");
} finally {
// 释放锁:Lua 保证只有自己能删
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockKey), requestId);
}
} else {
throw new RuntimeException("系统繁忙,请重试");
}✅ 优点:
- 高性能(内存操作)
- 支持过期自动释放
- 广泛使用(Redisson 封装)
❌ 缺点:
- 主从切换可能导致锁失效(需用 RedLock)
- 时间漂移问题(锁过期但任务未完成)
方式二:基于 ZooKeeper 的分布式锁(高可靠)
原理:临时顺序节点 + Watch 机制
// Curator 框架示例
InterProcessMutex lock = new InterProcessMutex(client, "/locks/stock_" + productId);
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 扣减库存
stockMapper.decrease(productId, quantity);
}
} finally {
lock.release();
}✅ 优点:
- 强一致性(ZAB 协议)
- 锁自动释放(会话断开即删除节点)
- 公平锁支持好
❌ 缺点:
- 性能低于 Redis
- 运维成本高(需部署 ZK 集群)
方式三:基于数据库的分布式锁(最简单,但不推荐高并发)
方式 A:唯一索引 + INSERT 抢占
CREATE TABLE distributed_lock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, lock_key VARCHAR(64) NOT NULL UNIQUE, owner_id VARCHAR(64), expire_time DATETIME );
抢占锁:
INSERT INTO distributed_lock (lock_key, owner_id, expire_time)
VALUES ('stock_123', 'user_456', NOW() + INTERVAL 30 SECOND);
-- 主键冲突则抢占失败释放锁:
DELETE FROM distributed_lock WHERE lock_key='stock_123' AND owner_id='user_456'方式 B:UPDATE 乐观锁(更推荐)
UPDATE stock SET quantity = quantity - #{quantity}, version = version + 1
WHERE product_id = #{productId} AND quantity >= #{quantity};配合版本号或 CAS 操作,天然防超卖。
✅ 优点:
- 无需额外中间件
- 简单易懂,适合中小项目
❌ 缺点:
- 性能差(DB 瓶颈)
- 锁持有时间长会阻塞连接池
- 不支持非阻塞获取
五、三种锁对比表(面试必备)
特性 | Redis 锁 | ZooKeeper 锁 | 数据库锁 |
|---|---|---|---|
一致性 | 最终一致 | 强一致 | 强一致 |
性能 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
可靠性 | 中(需 RedLock) | 高 | 低 |
实现复杂度 | 中 | 高 | 低 |
适用场景 | 秒杀、缓存击穿防护 | 金融、分布式协调 | 低频扣减、简单业务 |
推荐框架 | Redisson | Curator | MyBatis + SQL |
六、进阶:如何用 Lua + Redis 彻底避免超卖?(无锁胜有锁)
原子脚本示例(Redis 中执行):
-- KEYS[1]: stock:{productId}
-- ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
else
return -1 -- 库存不足
endJava 调用:
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long remain = redisTemplate.execute(script, Collections.singletonList(key), String.valueOf(quantity));
if (remain != null && remain >= 0) {
// 扣减成功,继续下单流程
}👉 优势:无需分布式锁,单条命令完成判断+扣减,天然原子!
七、面试加分回答模板
面试官:说说电商库存扣减如何防超卖?
你可以这样回答:
“我们采用‘三级防护’机制:
前端限流 + 按钮置灰,减少无效请求; Redis 预扣库存,通过 Lua 脚本保证原子性,避免并发超卖; 数据库最终扣减,使用乐观锁(version 或 CAS)确保数据一致; 为防止重复扣减,我们引入幂等性设计,通过订单号 + 状态机控制; 在高并发场景下,我们使用 Redisson 分布式锁 对关键资源加锁,确保同一时间只有一个请求能操作某商品库存。此外,我们还设计了库存回补机制:用户超时未付款,通过定时任务将 Redis 和 DB 库存同步回补。”
八、避坑指南
坑 | 解决方案 |
|---|---|
锁忘记释放 | 用 try-finally或看门狗机制(Redisson) |
锁过期但任务没完 | 设置合理的过期时间 + 看门狗续期 |
主从切换丢锁 | 使用 RedLock 或 ZooKeeper |
缓存与 DB 不一致 | 采用“Cache-Aside + 消息补偿”模式 |
重复扣减 | 订单号幂等校验 + 状态机(待支付→已支付) |
九、总结
技术点 | 作用 |
|---|---|
分布式锁 | 控制并发,避免同时操作 |
原子操作(Lua) | 替代锁,提升性能 |
乐观锁(CAS) | 保证 DB 层不超卖 |
幂等性 | 防止重复扣减 |
最终一致性 | 通过 MQ 异步同步数据 |
十、推荐学习资源
- Redisson 官方文档:https://redisson.org/
- Curator 锁实现:https://curator.apache.org/
- 阿里《Java 开发手册》:关于锁的使用规范
- 极客时间《分布式技术原理与实战》
需要我为你画一个“库存防超卖全链路流程图”,用 Mermaid 或文字描述,方便你在面试中手绘展示吗?这样可以让回答更直观,提升印象分。