×

《面试官:说说电商库存扣减如何防超卖?分布式锁的三种实现》

万邦科技Lex 万邦科技Lex 发表于2026-03-27 11:18:39 浏览25 评论0

抢沙发发表评论

这是一个经典的电商高频面试题,也是很多同学容易“背概念、缺落地”的地方。下面我给你一套面试可直接背诵 + 技术深度拉满的回答模板,同时补齐源码级原理避坑指南

🛒 面试官:说说电商库存扣减如何防超卖?分布式锁的三种实现

面试核心思路:先说业务模型 → 再说并发问题 → 给出三种锁方案 → 对比选型 → 兜底方案

一、先给结论(一句话版)

防超卖的本质:保证库存扣减的“原子性 + 幂等性”。
实践中通常采用:
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/个人网站/邮箱]与我联系

群贤毕至

访客