示例成品 · 平台演示,按左边这组点选真跑出来的
下面按“面试官原话 → 追问链 → 你该怎么答”来。答案不是标准答案,是你能直接改成自己项目细节的骨架,尽量说人话。
---
## Q1|项目链路与压测数据
**面试官**:先把你这个秒杀系统从用户点击到返回结果的完整链路讲一下,只说关键节点。
**追问**:
- Redis 和 MySQL 分别承担什么?
- 下单是同步还是异步?
- 你实测 QPS 多少?瓶颈在哪?
- 机器配置是什么?压测怎么做的?
**你可以这样答**:
链路:
用户点击 → Nginx/网关限流 → 校验登录/活动时间/用户资格 → Redis 预热库存,用 Lua 原子扣减 → 扣减成功发 MQ → 快速返回“已抢到/排队中” → 订单服务消费 MQ,写 MySQL 订单和库存。
Redis 是库存唯一扣减源,MySQL 只做最终落库,不直接接秒杀流量。
下单走异步,不然同步写 MySQL 会把链路拖垮。
压测数据要提前准备好,别只说“能扛高并发”。可以讲:
“单机 Redis 跑几万 QPS 没问题,我的瓶颈在应用层连接池/序列化,不是 Redis 本身。压测用 wrk/JMeter,机器几核几 G,错误率多少。”
---
## Q2|Redis 库存原子扣减
**面试官**:库存扣减具体怎么做的?为什么能防止超卖?
**追问**:
- Lua 脚本里写了什么?
- 为什么不先 GET 再判断再 SET?
- DECR 变负数怎么办?
- 扣减成功但下单失败,怎么回补?
**你可以这样答**:
我用 Lua 脚本,核心逻辑是:
```lua
local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
local bought = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or 0)
if stock <= 0 then return -1 end
if bought >= tonumber(ARGV[2]) then return -2 end
redis.call('DECRBY', KEYS[1], 1)
redis.call('HINCRBY', KEYS[2], ARGV[1], 1)
return 1
```
`KEYS[1]` 是商品库存 key,`KEYS[2]` 是用户已购 hash,`ARGV[1]` 是用户 ID,`ARGV[2]` 是限购数。
Redis 执行 Lua 是原子的,不会出现“多个请求同时读到库存 1,都扣成 0”的情况。
不能先 GET 再判断再 SET,中间有竞态,Lua 就是解决这个。
如果扣减成功,下单失败,我用 MQ 重试 + 死信 + 定时对账回补,不能只嘴上说“回补”。
---
## Q3|Redis 与 MySQL 一致性
**面试官**:Redis 扣了库存,MySQL 什么时候扣?两边怎么保证一致?
**追问**:
- 为什么不先扣 MySQL?
- 如果 MySQL 更新失败,Redis 已经扣了怎么办?
- 反过来,Redis 扣减失败但 MySQL 还有库存怎么办?
**你可以这样答**:
秒杀场景以 Redis 库存为唯一入口,MySQL 是最终存储。
扣减成功后发 MQ,订单服务消费后写订单、扣 MySQL 库存。一致性是最终一致,不是强一致。
异常处理:
MQ 消费失败重试,重试多次进死信,定时任务对账 Redis 和 MySQL,不一致就补偿或告警。
不要套“延迟双删”那套,那是缓存回填,不是库存扣减。
面试官问为什么不先扣 MySQL,你就说:MySQL 行锁会串行化扣减,扛不住高并发,Redis 单线程原子扣减更快。
---
## Q4|分布式锁到底用没用
**面试官**:你这个系统用到分布式锁了吗?如果没用,为什么?
**追问**:
- 扣库存为什么可以不靠分布式锁?
- Redis 的 SETNX 加锁有什么坑?
- 锁超时业务没执行完怎么办?
- 主从切换锁丢失怎么办?
**你可以这样答**:
扣库存我用 Lua 原子扣减,不需要分布式锁。
分布式锁会降低并发,能不用就不用。
如果一定要用锁,比如防止重复创建订单,我会用:
`SET key value NX PX` + 唯一 requestId,释放时用 Lua 校验后 DEL,不能直接 DEL。
超时问题用 Redisson 看门狗续期。
主从切换锁丢失,Redlock 可以降低概率,但不绝对。更稳的是用数据库唯一索引/幂等兜底,锁只是性能优化。
---
## Q5|秒杀开始 10 万 QPS 怎么限流削峰
**面试官**:秒杀开始瞬间 10 万 QPS,你怎么保护系统?
**追问**:
- 限流在哪一层做?
- 为什么用 MQ?MQ 积压怎么办?
- 前端有什么防刷手段?
- 库存没了怎么快速失败?
**你可以这样答**:
分层处理:
1. 前端:按钮置灰、防抖、验证码/风控。
2. 网关/Nginx:对 IP、用户限流。
3. 业务层:令牌桶/漏桶限制下单接口。
4. Redis 扣库存是最终闸门,库存不足直接返回“已抢光”。
MQ 用来削峰,不阻塞主链路。
如果 MQ 积压,先扩容消费者,批量消费,关闭非核心消息,必要时限流生产者。
核心思路:入口先拒绝,别让流量打到后端。
---
## Q6|缓存击穿、穿透、雪崩,结合秒杀
**面试官**:缓存击穿、穿透、雪崩分别是什么?秒杀里哪个最容易发生?
**追问**:
- 秒杀热点库存 key 过期怎么办?
- 库存为 0 后,这个 key 还是热 key 吗?
- 布隆过滤器在秒杀里有用吗?
**你可以这样答**:
击穿:热点 key 过期,大量请求打到 MySQL。
穿透:请求不存在的商品 ID,Redis 和 MySQL 都没有。
雪崩:大量 key 同时过期,或者 Redis 宕机。
秒杀里最容易发生的是击穿和雪崩。库存 key 是天然热 key,活动期间不能简单设置随机过期。
库存为 0 也是高频访问,要缓存“已抢光”状态,否则请求还会压到后端。
布隆过滤器主要防穿透,对热 key 击穿没帮助。
---
## Q7|幂等与重复下单
**面试官**:用户狂点按钮、网络重试,会不会重复扣库存或者重复下单?你怎么做幂等?
**追问**:
- Redis 扣库存已经原子了,为什么还会重复下单?
- 数据库唯一索引怎么设计?
- TOKEN 幂等和唯一索引区别?
**你可以这样答**:
超卖和重复下单是两个问题。
Lua 原子扣库存只保证库存不超卖,不保证一个用户只下单一次。
我在 Lua 里同时检查用户已购数量,用户限购 1 件时,同一用户重复请求会直接失败。
但订单服务消费 MQ 可能重复投递,所以还要做接口幂等。
最终兜底用数据库唯一索引:`user_id + activity_id` 或者订单号唯一。
TOKEN 幂等是防前端重复提交,唯一索引是最后防线。
---
## Q8|Redis 持久化与宕机恢复
**面试官**:Redis 持久化怎么配的?秒杀场景 Redis 宕机能接受丢多少数据?
**追问**:
- RDB 和 AOF 区别?
- AOF everysec 丢多少?
- 主从同步会丢数据吗?
- Redis 挂了库存怎么恢复?
**你可以这样答**:
配置:AOF everysec + RDB 做备份,主从 + 哨兵。
AOF everysec 最多丢约 1 秒写入,RDB 可能丢几分钟到更久。
主从异步复制,主宕机时从可能没同步到最新数据,所以也可能丢一点。
Redis 宕机后,先熔断秒杀入口,不要直接拿 MySQL 库存覆盖 Redis。
因为可能有“Redis 已经扣了,但订单还没落 MySQL”的情况。
要先用 AOF/用户已购标记/订单流水对账,再重新预热库存。
原则:宁可少卖,不能超卖。
---
## Q9|Redis 单线程是优势还是瓶颈
**面试官**:Redis 单线程为什么快?在秒杀里单线程是优势还是瓶颈?
**追问**:
- Lua 脚本
点左边「开工 · 直接出成品」,出一份你自己的版本(文字免费)