经典面试题:618大促多渠道同时扣减同一仓库库存,你会怎么做?
这个是我之前面试遇到的一个题,当时回答的很笼统,但是关键点应该都回答了,现在有时间了,我梳理一下这个问题的完善答案,我感觉大家可能也用的上。 经典的问题最能暴漏出来你的设计能力和逻辑思维能力。
一、最大风险是什么?
核心风险就一个:并发写入导致的库存超卖。
具体表现:
▼text复制代码时刻 T:仓库A某SKU库存剩余 1 渠道1(天猫) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 渠道2(京东) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 渠道3(抖音) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 结果:库存只有1件,卖了3件 → 超卖
这是一个典型的 Read-Modify-Write 竞态条件,本质是"先读后写"缺乏原子性。618场景下 QPS 可能达到万级甚至十万级,并发窗口极小但必然被击穿。
二、防超卖的设计方案(分层递进回答)
方案1:数据库行锁(兜底防线,必须要有)
▼sql复制代码-- 核心:用 UPDATE 的行锁保证原子性,而不是先 SELECT 再 UPDATE UPDATE inventory SET stock = stock - #{quantity} WHERE sku_id = #{sku_id} AND warehouse_id = #{warehouse_id} AND stock >= #{quantity};
- 检查
affected rows:返回 1 表示扣减成功,返回 0 表示库存不足 - 优点:简单可靠,数据库 ACID 天然保证正确性
- 缺点:行锁粒度下,高并发时数据库成为瓶颈
方案2:Redis 预扣减(扛并发的核心)
▼text复制代码-- Lua脚本保证原子性 local stock = redis.call('GET', KEYS[1]) if not stock or tonumber(stock) < tonumber(ARGV[1]) then return -1 -- 库存不足 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 -- 扣减成功
关键设计:Redis 做快速预扣,数据库做最终持久化,两者结合。
▼text复制代码用户下单 → Redis预扣(原子Lua) → 创建订单 ↓ 异步MQ → DB扣减(幂等)
- Redis 扛住 99% 的高并发流量
- DB 作为最终一致性的落地存储
- 必须保证:Redis 库存总数 ≤ DB 库存总数(宁可少卖,不可超卖)
方案3:库存分桶(解决热点SKU问题)
618 爆品单品可能集中 10 万+ QPS 打在一个 key 上,单点 Redis 也扛不住。
▼text复制代码SKU_1001 → bucket_0: 500件 → bucket_1: 500件 → bucket_2: 500件 → bucket_3: 500件 渠道1 → hash(渠道1) % 4 → bucket_0 (隔离竞争) 渠道2 → hash(渠道2) % 4 → bucket_1 渠道3 → hash(渠道3) % 4 → bucket_2
- 按渠道/用户 hash 分桶,将热点打散
- 桶内库存独立扣减,无竞争
- 某桶不足时可以合并/重路由
方案4:渠道配额控制(多渠道协同)
▼text复制代码仓库A / SKU_1001 总库存: 10000件 ├── 天猫配额: 4000件 (Redis key: quota:tmall:sku_1001) ├── 京东配额: 3000件 (Redis key: quota:jd:sku_1001) ├── 抖音配额: 2000件 (Redis key: quota:dy:sku_1001) └── 安全余量: 1000件 (兜底池,按需动态分配)
- 每个渠道独立扣自己的配额,天然无跨渠道竞争
- 余量池用于动态调配(某渠道卖超预期可以追加)
- 配额之和 ≤ 总库存,从根源杜绝超卖
三、防少卖的设计要点
少卖的本质是 预扣不释放:用户下单后未支付,库存被锁定但未释放。
▼text复制代码下单 → Redis预扣库存 → 等15分钟支付 ↓ 未支付 定时任务归还库存(回滚Redis + DB)
关键机制:
| 机制 | 说明 |
|---|---|
| 支付超时释放 | 下单后 15~30 分钟未支付,自动归还库存 |
| 订单取消归还 | 用户主动取消订单,同步归还库存 |
| Redis 库存定期对账 | 定时任务扫描预扣记录,清理孤儿锁 |
| DB 库存最终一致性 | 对账线程周期性对比 Redis 可用库存与 DB 可用库存,修正偏差 |
四、整体架构总结
▼text复制代码┌─────────────┐ 天猫 ──┐ │ 网关/限流 │ 京东 ──┼─────────►│ (令牌桶) │ 抖音 ──┘ └──────┬──────┘ │ ┌──────▼──────┐ │ 库存服务 │ │ (无状态) │ └──┬───┬───┬──┘ │ │ │ ┌──────────┘ │ └──────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ │ Redis集群 │ │ MQ队列 │ │ MySQL/DB │ │ (预扣+配额) │ │ (异步落库) │ │ (持久化+对账)│ └─────────────┘ └──────────┘ └─────────────┘ │ │ └─────── 定时对账 ◄───────────┘
五、从购物车到出库 — 库存全链路
前提:库存状态拆分
▼text复制代码总库存 (total_stock) = 10000 ├── 可售库存 (available) = 6000 ← 用户能看到的 ├── 购物车锁定 (cart_locked) = 500 ← 加购占位中 ├── 订单预扣 (order_locked) = 2500 ← 已下单未支付 ├── 已付款待发 (paid_pending) = 800 ← 已支付待发货 └── 安全余量 (safety_buffer) = 200 ← 不对外暴露
每一步流转都是 从一个状态减、往另一个状态加,总量不变,可对账。
第一步:加入购物车
用户点"加入购物车"时,需要校验库存并做一次轻量锁定。
▼text复制代码用户A 加购 SKU_1001 × 2件 │ ▼ GET /cart/add │ ├── 1. 读Redis可用库存,判断是否充足 ├── 2. 充足 → DECRBY 可售库存,INCRBY 购物车锁定库存 └── 3. 返回加购成功
关键设计:
▼lua复制代码-- Redis Lua 原子操作 local available = redis.call('GET', 'stock:available:sku_1001') if tonumber(available) < tonumber(ARGV[1]) then return -1 end redis.call('DECRBY', 'stock:available:sku_1001', ARGV[1]) redis.call('INCRBY', 'stock:cart_locked:sku_1001', ARGV[1]) redis.call('SET', 'cart:lock:{userId}:sku_1001', ARGV[1], 'EX', 1800) -- 30分钟过期,超时自动归还 return 1
核心要点:
- 购物车锁定有时效(一般 30 分钟),超时自动释放回可售库存
- 锁定粒度到用户+SKU,用 key 过期机制做自动归还,不依赖定时任务
- 加购环节不碰数据库,纯 Redis 操作,保证性能
第二步:提交订单(下单)
用户点"去结算"→"提交订单",把购物车锁定正式转为订单预扣。
▼text复制代码用户A 提交订单,SKU_1001 × 2件 │ ▼ POST /order/create │ ├── 1. 校验购物车锁定记录是否存在(防伪造) ├── 2. 购物车锁定 → 订单预扣(状态流转) ├── 3. 写入订单表(状态:待支付) ├── 4. 发MQ消息,异步落DB库存流水 └── 5. 返回订单号 + 支付倒计时(15分钟)
关键设计:
▼lua复制代码-- Redis 状态流转(原子) local cartLock = redis.call('GET', 'cart:lock:{userId}:sku_1001') if not cartLock then return -1 -- 购物车锁已过期,需要重新加购 end -- 购物车锁定 → 订单预扣 redis.call('DECRBY', 'stock:cart_locked:sku_1001', ARGV[1]) redis.call('INCRBY', 'stock:order_locked:sku_1001', ARGV[1]) redis.call('DEL', 'cart:lock:{userId}:sku_1001') redis.call('SET', 'order:lock:{orderId}:sku_1001', ARGV[1], 'EX', 900) -- 15分钟支付超时 return 1
需要处理的异常场景:
| 异常 | 处理方式 |
|---|---|
| 购物车锁已过期 | 提示用户"库存变动,请重新加购",引导回到购物车 |
| 提交订单途中 Redis 超时 | 订单表记录状态为"创建中",定时任务扫描补偿 |
第三步:用户支付
支付回调到达后,订单预扣 → 已付款待发。
▼text复制代码支付回调到达 │ ▼ POST /pay/callback │ ├── 1. 校验支付结果(防伪造回调) ├── 2. 幂等校验(同一笔订单不重复处理) ├── 3. Redis: 订单预扣 → 已付款待发 ├── 4. 更新订单状态 → 已支付 ├── 5. MQ异步落DB库存 + 订单流水 └── 6. 通知仓储系统发货
▼lua复制代码-- Redis 状态流转 local orderLock = redis.call('GET', 'order:lock:{orderId}:sku_1001') if not orderLock then -- 可能已超时释放,但用户实际付了款 -- 进入异常流程:尝试从可用库存补扣 local available = redis.call('GET', 'stock:available:sku_1001') if tonumber(available) >= tonumber(ARGV[1]) then redis.call('DECRBY', 'stock:available:sku_1001', ARGV[1]) else return -1 -- 真的没库存了,触发退款 end else redis.call('DECRBY', 'stock:order_locked:sku_1001', ARGV[1]) redis.call('DEL', 'order:lock:{orderId}:sku_1001') end redis.call('INCRBY', 'stock:paid_pending:sku_1001', ARGV[1]) return 1
关键异常:用户在 15 分钟最后几秒支付,但订单锁刚好过期被归还了。处理策略:
- 优先从可售库存补扣(一般归还后还没被别人抢走)
- 补扣失败 → 触发自动退款 + 补偿券(用户体验兜底)
- 宁可退款不少卖,这是底线
第四步:发货出库
仓储确认发出后,已付款待发 → 扣减总库存(真实物理库存减少)。
▼text复制代码仓储系统回传发货成功 │ ▼ POST /warehouse/ship/callback │ ├── 1. Redis: 已付款待发 → 总库存扣减 ├── 2. DB: 扣减 total_stock(最终落库) ├── 3. 更新订单状态 → 已发货 └── 4. 记录库存流水明细
这一步库存变化落地到 DB,是最终一致性的锚点。
六、异常兜底:定时对账
以上全链路中 Redis 和 DB 存在短暂不一致,必须靠对账兜底:
▼text复制代码┌─────────────────────────────────────────────────────┐ │ 定时对账任务(每分钟) │ │ │ │ 1. Redis各状态求和 vs DB total_stock │ │ 不一致 → 以DB为准,修正Redis │ │ │ │ 2. 扫描过期购物车锁(二次保障,不依赖TTL) │ │ 有残留 → 归还available │ │ │ │ 3. 扫描超时未支付订单 │ │ 有残留 → 归还available + 关闭订单 │ │ │ │ 4. 扫描"创建中"状态的僵尸订单 │ │ 超时未确认 → 回滚所有预留库存 │ └─────────────────────────────────────────────────────┘
七、全链路库存流转总览
▼text复制代码可售库存(available) │ │ ①加购 ▼ 购物车锁定(cart_locked) ──超时30min──→ 归还available │ │ ②下单 ▼ 订单预扣(order_locked) ──超时15min──→ 归还available │ │ ③支付 ▼ 已付款待发(paid_pending) │ │ ④发货 ▼ 总库存扣减(DB total_stock 真实减少)
面试总结一句话:库存不是一步扣完的,而是通过状态拆分 + 逐步流转 + 超时自动归还 + 定时对账,在保证高并发性能的同时,实现不超卖不少卖的最终一致性。
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
内容推荐
找实习从7月22号开始投,现在也总算是有offer了
5
怎么会有笨蛋从一月到现在,背了7个月的面试题,还啥都不会呢,到底背哪里去了,该怎么办
3
Spring Boot 如何处理跨域请求(CORS):深度调研报告
3
【入职求助贴】萌新刚入职某大厂做后端开发,目前还在试用期。最近遇到一个棘手的问题,想向大家求助一下。入职不久,leader 给我派了一个任务。跟我说是0.5天就可以解决,我刚毕业入职,做了一个星期没有做出来。实现一个收集定时成功任务的案例。听起来好像不复杂,但我自己摸索着做了一整个星期,到现在还没达到预期效果。这一周我基本是“边学边做”的状态,遇到卡点也会每天主动找 leader 沟通进度和疑问。
2
#字节内推# 实习、校招、社招均有岗位
4
