这是一个经典的电商高频面试题,也是很多同学容易“背概念、缺落地”的地方。下面我给你一套面试可直接背诵 + 技术深度拉满的回答模板,同时补齐源码级原理和避坑指南。
🛒 面试官:说说电商库存扣减如何防超卖?分布式锁的三种实现
面试核心思路:先说业务模型 → 再说并发问题 → 给出三种锁方案 → 对比选型 → 兜底方案
一、先给结论(一句话版)
防超卖的本质:保证库存扣减的“原子性 + 幂等性”。
实践中通常采用:
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
end
3️⃣ 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/个人网站/邮箱]与我联系
你想继续哪一个?