这是一个经典的电商高频面试题,也是很多同学容易“背概念、缺落地”的地方。下面我给你一套面试可直接背诵 + 技术深度拉满的回答模板,同时补齐源码级原理和避坑指南。
🛒 面试官:说说电商库存扣减如何防超卖?分布式锁的三种实现
面试核心思路:先说业务模型 → 再说并发问题 → 给出三种锁方案 → 对比选型 → 兜底方案
一、先给结论(一句话版)
防超卖的本质:保证库存扣减的“原子性 + 幂等性”。实践中通常采用:Redis 原子扣减(Lua) + 数据库乐观锁 + 分布式锁兜底。
二、超卖是怎么发生的?(面试官爱问)
典型并发场景
库存 = 1 线程A:查库存 = 1 ✅ 线程B:查库存 = 1 ✅ 线程A:扣减库存 → 0 ✅ 线程B:扣减库存 → -1 ❌(超卖)
根本原因
问题 | 说明 |
|---|---|
非原子操作 | select → judge → update 被拆分 |
并发竞争 | 多线程同时进入临界区 |
重试机制 | MQ 重试导致重复扣减 |
三、正确的库存扣减模型(重点)
✅ 强烈推荐的“三段式”模型
1️⃣ Redis 预扣(高性能、抗并发) 2️⃣ MQ 异步落库(解耦、削峰) 3️⃣ DB 最终扣减(强一致)
核心原则
✅ 不要直接在高并发下操作 MySQL
✅ 所有写库存必须是原子操作
✅ 任何扣减都必须幂等
四、分布式锁的三种实现(面试正菜)
✅ 方案一:Redis 分布式锁(最常用)
1️⃣ 加锁(SET NX PX)
SET lock:stock:1001 uuid NX PX 30000
2️⃣ 解锁(Lua 保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end3️⃣ Java 示例(简化版)
String uuid = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:stock:1001", uuid, 30, TimeUnit.SECONDS);
if (locked) {
try {
deductStock(); // 核心业务
} finally {
unlock(uuid);
}
}✅ 优点
- 性能极高(内存级)
- 实现简单
- 适合秒杀、高并发
❌ 缺点
- 主从切换可能丢锁(CAP)
- 锁过期 ≠ 业务执行完成
✅ 解决方案
- Redisson Watch Dog(自动续期)
- RedLock(多实例)
✅ 方案二:ZooKeeper 分布式锁(最稳)
原理
/lock ├── /order-000001(临时顺序节点) ├── /order-000002 └── /order-000003
- 谁序号最小,谁拿到锁
- 客户端断连 → 节点自动删除 → 锁释放
Curator 示例
InterProcessMutex lock =
new InterProcessMutex(client, "/lock/stock");
lock.acquire();
try {
deductStock();
} finally {
lock.release();
}✅ 优点
- CP 强一致
- 不会误删锁
- 天然防死锁
❌ 缺点
- 性能不如 Redis
- 运维复杂
✅ 使用场景
- 资金类
- 订单创建
- 核心配置变更
✅ 方案三:数据库锁(最朴素但有效)
方式 1️⃣:悲观锁(不推荐)
SELECT * FROM stock WHERE id=1 FOR UPDATE;
❌ 缺点:
- 连接被长时间占用
- 极易拖垮数据库
✅ 方式 2️⃣:乐观锁(强烈推荐)
UPDATE stock SET quantity = quantity - 1, version = version + 1 WHERE id = 1 AND quantity >= 1 AND version = old_version;
Java 判定
if (rows == 0) {
throw new RuntimeException("库存扣减失败");
}✅ 优点
- 无锁、高并发
- 天然防超卖
- 数据库兜底方案
❌ 缺点
- 重试成本高
- 不适合秒杀级流量
五、三种分布式锁对比(面试必背)
维度 | Redis | ZooKeeper | 数据库 |
|---|---|---|---|
性能 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
一致性 | AP | CP | CP |
实现难度 | 中 | 高 | 低 |
死锁风险 | 有 | 无 | 无 |
典型场景 | 秒杀、缓存 | 核心业务 | 低频扣减 |
六、真正的大厂方案(加分项)
✅ Redis Lua 原子扣减(无锁胜有锁)
local stock = tonumber(redis.call("get", KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call("decrby", KEYS[1], ARGV[1])
else
return -1
end👉 面试金句:
“在高并发库存扣减场景中,我们优先使用 Redis Lua 原子操作,只有在复杂业务或幂等控制时才引入分布式锁。”
七、完整面试标准答案(可直接背)
面试官:说说电商库存扣减如何防超卖?
✅ 标准回答模板:
防超卖的核心是原子性 + 幂等性。我们一般分三层来做:第一层,在 Redis 中用 Lua 脚本做原子扣减,保证高并发下不超卖;第二层,通过 MQ 异步将扣减请求发到下游,由消费者用数据库乐观锁完成最终扣减;第三层,为了防止极端并发和重复请求,我们会使用 分布式锁。分布式锁我们有三种实现:1️⃣ Redis 锁:性能好,适合秒杀,但要注意锁续期和主从切换问题;2️⃣ ZooKeeper 锁:强一致,适合资金、订单等核心业务;3️⃣ 数据库乐观锁:通过 version 或 CAS 更新,作为最终兜底。在实际生产中,我们更倾向 Redis + Lua + 乐观锁 的组合方案。
八、面试官追问准备
Q:Redis 锁过期了,业务还没执行完怎么办?
✅ 答:Redisson 的 Watch Dog 自动续期。
Q:Redis 主从切换锁丢了怎么办?
✅ 答:使用 RedLock 或多实例仲裁。
Q:MQ 重复消费会不会超卖?
✅ 答:订单号 / 流水号 幂等校验。
九、总结一句话
防超卖不是靠一把锁,而是靠“原子操作 + 幂等设计 + 分层兜底”。
以上是我在电商中台领域的一些实践,目前我正在这个方向进行更深入的探索/提供相关咨询与解决方案。如果你的团队有类似的技术挑战或合作需求,欢迎通过[我的GitHub/个人网站/邮箱]与我联系