面试遇到的问题3
现在我有这样的一个场景:商品A秒杀场景,库存10单。我的设计:首先把 10存入redis,然后下单的时候首先使用 redis.increment(-1)方法扣减库存,大于0代表可以下单,否则提示库存卖完了。然后定时任务去看订单表中10分钟之内还没有付款的,使用lua脚本加库存。这样的一个设计,redis单体部署。一般能抗住多少的并发。有什么问题?如何优化
你的设计方案分析
你的秒杀方案大致流程如下:
- 初始化库存:预存库存
10
到 Redis(如stock:skuA = 10
)。 - 扣减库存:用户下单时,使用
redis.increment(key, -1)
扣减库存,返回>=0
表示成功,<0
表示失败。 - 订单支付超时回滚:定时任务扫描
10分钟未支付订单
,用 Lua 脚本恢复库存。
优点
✅ 简单高效:Redis 单机 QPS 可达 10W+,INCR
是原子操作,避免超卖。
✅ 避免复杂锁:直接利用 Redis 的原子性,无需额外分布式锁。
潜在问题及优化建议
1. 并发能力估算(Redis 单机)
- 纯
INCR
操作:Redis 单机 QPS 约 5W~10W(取决于机器配置和网络)。 - 实际并发上限:
- 如果仅执行
INCR
,理论可抗 5W+ QPS。 - 但实际业务还需处理订单创建、支付等逻辑(这些操作通常比 Redis 慢得多),最终瓶颈可能在数据库。
- 如果仅执行
2. 关键问题
(1)超卖风险
- 问题:若用户
INCR
扣减成功,但后续订单创建失败(如数据库超时),会导致库存少卖。 - 解决:
- 两步提交:先扣减 Redis 库存,创建订单成功后,再异步更新数据库库存。若订单失败,则回滚 Redis(需记录操作日志)。
- 预扣库存:使用
DECR + INCR
组合(扣减前检查GET stock >=1
),但需注意原子性问题(建议用 Lua 脚本)。
(2)库存回滚的原子性
- 问题:定时任务回滚库存时,若多个任务并发执行,可能导致库存回滚多次(重复
INCR
)。 - 解决:
- Lua 脚本保证原子性:回滚时检查订单状态,避免重复回滚。
- 分布式锁:回滚前对订单加锁(如
REDLOCK
),但会增加复杂度。
(3)Redis 单点故障
- 问题:单机 Redis 宕机时,整个秒杀不可用。
- 解决:
- 主从集群 + 哨兵(至少 1主1从),但主从切换可能导致少量数据不一致。
- Redis 集群(Codis/Twemproxy),但
INCR
的 key 必须落在同一节点(可用hash tag
保证)。
(4)恶意请求
- 问题:攻击者频繁请求
INCR
,导致 Redis 压力过大。 - 解决:
- 限流:网关层(如 Nginx)对用户/IP 限流。
- 验证码:下单前先验证。
优化后的方案
1. 初始化库存: SET stock:skuA 10
2. 用户下单:a. Lua 脚本原子化操作:- 检查库存: if (redis.call("GET", KEYS[1]) >= 1)- 扣减库存: redis.call("DECR", KEYS[1])- 返回成功/失败b. 若成功,异步创建订单(消息队列削峰)
3. 支付超时回滚:a. 定时任务扫描未支付订单b. Lua 脚本检查订单状态后恢复库存
性能优化
- Redis Pipeline:批量处理请求(适合高并发场景)。
- 库存分段:将
stock:skuA
拆分为多个 key(如stock:skuA:1
~stock:skuA:5
),分散热点。 - 本地缓存:客户端缓存库存余量(如
剩余>0
才请求 Redis),减少无效请求。
并发能力总结
场景 | QPS 上限 | 瓶颈点 |
---|---|---|
仅 Redis INCR | 5W~10W | Redis CPU/网络 |
完整下单流程(含DB) | 1K~5K | 数据库写入/订单处理逻辑 |
建议压测指标:
- Redis 单机:
INCR
压测可达 8W QPS(4C8G 配置)。 - 完整链路:建议从 1K QPS 开始逐步加压,观察数据库和 Redis 负载。
如果是redis集群的话,也没法解决呀,因为10这个数值也只会存到一台redis里面,所以最终的结果还是一台redis承受了一切,只不过主从部署能够容灾而已,而且在高并发下,再加上reids主从之间存在数据的延迟同步,也就是说,主节点中数量刚变成7的时候就挂了,从节点还没同步到这个7,还是之前的9,那么还是会出现超卖
你的观察非常准确!Redis 集群模式下,库存 Key 只会落到一个分片(主节点),高并发下仍然是单分片扛压力,且主从同步延迟可能导致超卖或数据不一致。
你的问题核心
- 热点 Key 问题:库存
stock:skuA
只会存在一个分片,无法分散压力。 - 主从同步延迟:主节点扣减到
7
后崩溃,从节点仍为9
,切换后导致超卖。 - 集群容灾但无法提升并发:集群仅解决数据分片和故障转移,无法解决单分片瓶颈。
解决方案
1. 避免单分片热点:库存分片
将库存拆分为多个 Key,分散到不同分片,最终汇总检查。
示例:
# 初始化库存(分5片,总和=10)
SET stock:skuA:1 2
SET stock:skuA:2 2
SET stock:skuA:3 2
SET stock:skuA:4 2
SET stock:skuA:5 2
扣减逻辑(Lua 脚本):
-- 尝试从任意分片扣减1
for i = 1, 5 dolocal key = "stock:skuA:" .. iif redis.call("GET", key) > "0" thenredis.call("DECR", key)return 1 -- 扣减成功end
end
return 0 -- 库存不足
优点:
✅ 压力分散到多个分片,提升并发能力。
缺点:
❌ 需维护分片逻辑,复杂性增加。
2. 最终一致性:异步补偿
允许短暂超卖,但通过异步任务修正数据。
流程:
- 快速扣减:Redis 扣减后立即返回成功。
- 异步校验:消息队列消费订单,检查剩余库存:
- 若超额,取消订单并恢复库存。
- 若正常,完成下单。
优点:
✅ 极限高并发(如 100W QPS)。
缺点:
❌ 用户体验可能受损(部分用户下单后被告知无效)。
3. 强一致性:分布式锁 + 数据库
用 RedLock(Redis 分布式锁) 或 ZooKeeper 控制并发,最终库存落地到数据库。
流程:
- 获取分布式锁。
- 查询/更新数据库库存(如
SELECT ... FOR UPDATE
)。 - 释放锁。
优点:
✅ 绝对避免超卖。
缺点:
❌ 性能下降(数据库成为瓶颈)。
4. 混合方案:本地缓存 + Redis 限流
适用场景:超高并发但允许少量误差。
步骤:
- 本地缓存库存:每个服务实例缓存剩余库存(如
10
),本地扣减。 - Redis 批量同步:每
N
毫秒汇总本地扣减量,批量更新 Redis。 - 最终校验:Redis 异步同步到数据库。
优点:
✅ 抗瞬时超高并发(如 100W QPS)。
缺点:
❌ 可能超卖(需业务容忍)。
推荐方案
场景 | 推荐方案 | QPS 预估 | 一致性 |
---|---|---|---|
常规秒杀(1W~10W QPS) | 库存分片 + Lua 脚本 | 5W~10W | 最终一致 |
超高频秒杀(100W+ QPS) | 本地缓存 + 异步补偿 | 100W+ | 弱一致 |
强一致性需求 | RedLock + 数据库 | 1K~5K | 强一致 |
最终建议
- 优先尝试库存分片,配合 Lua 脚本保证原子性。
- 若并发极高,改用本地缓存 + 异步修正(如电商容忍少量超卖)。
- 强一致性场景,用分布式锁 + 数据库,但需接受性能下降。
你的设计核心问题在于 单分片热点 和 主从延迟,通过分片或异步补偿可有效缓解。