Skip to content

【面试题】1. 超卖问题你怎么根治的?Redis Lua + 数据库乐观锁是怎么配合的? ​

【标准答案】超卖是秒杀的一票否决项,100 件库存绝不能卖出 101 件。我用的是双重防线。

第一道防线:Redis Lua 原子扣减。 把"查询库存 + 判断是否足够 + 扣减"三步写进一个 Lua 脚本,Redis 单线程执行,天然原子。核心逻辑就一句:if redis.call('get', KEYS[1]) > 0 then redis.call('decr', KEYS[1]) else return 0 end。库存不足的直接返回 0,链路上报售罄。

第二道防线:数据库乐观锁兜底。 Redis 扣成功了不代表万事大吉——万一 Redis 挂过库存回滚过,数据可能不准。最终写数据库时用 update stock set count=count-1, version=version+1 where id=? and version=? and count>0。version 对不上或者 count 已经为 0,update 返回 affected_rows=0,事务回滚。

两层配合逻辑: Redis 是快闸,拦 99% 无效流量;数据库乐观锁是慢闸,兜住那 1% 的异常。两关都过,才算真正扣减成功。


【面试题】2. 秒杀场景下服务熔断和降级怎么做?Sentinel 怎么配? ​

【标准答案】熔断是"下游挂了我不调你",降级是"扛不住了换个便宜方案"。

熔断配置(Sentinel): 秒杀下单接口对库存服务熔断设"慢调用比例"——1 分钟内 50% 的请求 RT 超过 200ms,触发熔断,熔断窗口 10 秒。熔断期间请求直接走 fallback,返回"系统繁忙"。10 秒后放一个探测请求,成功了关闭熔断,失败了继续熔断。

降级策略分三级: 一级降级——库存扣完直接返回售罄页面,不查任何下游。二级降级——秒杀结束后,关掉商品推荐、浏览记录、优惠券查询这些非核心依赖,只保留下单链路的 DB 和 Redis。三级降级——Redis 挂了就降级到本地缓存,本地缓存没命中直接返回"请稍后再试",保住不雪崩。

核心原则: 熔断是防下游拖死上游,降级是牺牲体验保核心功能。秒杀的核心只有一件——用户能不能抢到,其他都能砍。


【面试题】3. 秒杀系统怎么防黄牛和恶意脚本?风控体系怎么搭? ​

【标准答案】防刷不是加一个接口就解决的事,是一整套纵深防御。

第一层:网关层人机验证。 秒杀开始前弹出滑块验证码或答题(如"5+3=?"),脚本无法自动完成。验证通过后生成一次性 token 写入 Redis,下单接口校验 token 有效性。

第二层:频控与限流。 同一 IP 每秒最多 10 个请求,同一用户 ID 每秒最多 1 次下单请求。超过阈值直接拉黑——IP 加入网关黑名单 10 分钟,用户 ID 加入 Redis 黑名单 30 分钟。

第三层:设备指纹与行为分析。 前端采集浏览器指纹(Canvas、WebGL、字体列表),相同设备指纹的不同账号视为同一台机器。行为上:1 秒内完成"进页面→点按钮→提交"不像人类操作,标记为高风用户。

第四层:下单链路卡口。 即使前面全穿透了,下单时还要校验:收货地址是否集中(同一地址下了 50 单)、支付账号是否异常、用户注册时间是否小于 24 小时。

一句话: 别想着一步拦住,用多道便宜防线层层削弱,最后活到 DB 的恶意请求少到可以忽略。


【面试题】4. 秒杀系统数据库分库分表怎么做?分片键怎么选? ​

【标准答案】秒杀订单量爆炸后,单表撑不住,必须分库分表。

分片策略: 用 ShardingSphere 做中间件,订单表按 user_id 取模分片。为什么不用 order_id?因为秒杀系统最频繁的查询是"查我的订单",按 user_id 分片让用户所有订单落在同一个分片里,避免跨库查询。

分片数量: 16 库 × 16 表 = 256 张逻辑表,按 user_id % 16 定库,user_id / 16 % 16 定表。两百多万用户每张表不到 1 万条,查询性能有保障。

扩容问题: 256 不够了怎么办?两种方式:一是停服迁移,把数据通过一致性哈希重新分布到 512 分片,适合提前规划好的扩容。二是先用分库分表 + 冷热分离,热数据(最近 3 个月)留在 256 分片,冷数据迁到归档库。绝大多数场景都不是分片不够,是没做冷热分离。

关键坑: 分片后 group by、跨分片 join 全部失效,必须在应用层自己聚合。所以分片键的选型最重要——以 80% 的查询场景为基准。


【面试题】5. Redis 高可用在秒杀中怎么选?哨兵 vs Cluster 各有什么适用场景? ​

【标准答案】选型原则很简单:数据量小、写集中用哨兵;数据量大、需要水平扩展用 Cluster。

哨兵模式适合秒杀库存场景。 秒杀的库存 key 就那么几个,数据量极小,但写 QPS 极高。哨兵模式是一主多从:主节点扛写,从节点扛读,哨兵监控主节点故障自动切换。故障转移 10-30 秒,抖动期间依赖本地缓存 + 数据库乐观锁撑住。

Redis Cluster 适合大容量场景。 比如缓存了所有商品的详情、用户信息、资格校验数据,几百 GB 内存单机放不下。Cluster 通过分片把数据分散到多台机器上,水平扩展方便。秒杀场景单独给库存 Key 用 HashTag 锁定节点。

Redisson 的角色: Redisson 不是部署方案,是客户端框架。它封装了分布式锁、读写锁、信号量,在哨兵和 Cluster 模式下都能用。秒杀里主要用它的分布式锁 + 看门狗自动续期。

实战组合: 核心库存用哨兵(保证低延迟),商品和用户缓存用 Cluster(保证容量),中间用 Redisson 统一客户端。不把所有鸡蛋放一个篮子里。


【面试题】6. 让你从零设计一个秒杀系统,你会怎么分阶段落地? ​

【标准答案】我分四个阶段,每个阶段解决一类核心问题。

阶段一:跑通核心链路(1-2 周)。 商品 → 下单 → 扣库存 → 生成订单。库存用 Redis,下单用简单的 Spring Boot + MySQL。不搞缓存、不搞 MQ,先跑通业务流程。这阶段验证的是业务逻辑正确性。

阶段二:扛住并发(2-3 周)。 引入 Redis Lua 原子扣库存、消息队列异步创建订单、动静分离 CDN、前端削峰(按钮置灰+倒计时)。压测找出第一个瓶颈,重点优化。这阶段验证的是并发能力。

阶段三:稳定性建设(1-2 周)。 加上限流(Sentinel+网关)、熔断降级、缓存预热、布隆过滤器防穿透、分布式锁防重、幂等性保证。同时搭好监控大盘(QPS/库存/成功率/RT),配置告警规则。这阶段验证的是系统不会崩。

阶段四:运营与风控(持续)。 防刷风控、压测常态化、大促演练、应急预案文档化、回滚 SOP。每个大促前做一次全链路压测+混沌工程演练。

一句话总结落地节奏: 先跑通,再扛量,再稳定,再运营。很多人死在第二阶段就想完美,结果上线时间一拖再拖。


【面试题】7. 秒杀系统中缓存、数据库、消息三者数据怎么对齐?不一致怎么办? ​

【标准答案】秒杀场景里这三者天然不一致,目标不是"绝对一致",而是"最终一致 + 可感知差异最小"。

正常流程的对齐策略:

  1. Redis 扣减是实时真相。 秒杀窗口内,库存以 Redis 为准。用户看到的库存是 Redis 的值,扣减结果也是 Redis 给的。
  2. 数据库是异步追求的目标。 MQ 收到扣减成功消息后,异步写入数据库。此时 Redis 和 DB 不一致是正常的,DB 晚 0.5~2 秒追上。
  3. 消息队列做缓冲。 MQ 削峰后数据库按自己节奏消费,消费完就对齐了。

异常恢复的对账机制:

  • 实时对账(高频): 每 5 分钟对比 Redis 扣减记录和数据库订单数,差值超过阈值就告警。
  • 每日全量对账(低频): 凌晨低峰期跑全量对账 SQL,查出"有扣减无订单"的记录,自动回滚库存或补建订单。
  • 用户侧感知处理: 用户下单后如果订单没及时生成,前端显示"订单处理中,请稍后查看",不报错吓跑用户。

核心原则: 秒杀不要强一致,但要"读自己的写"——用户提交后自己能看到结果。对账脚本是最后的保险丝。


【面试题】8. 秒杀监控大盘怎么设计?哪些指标是你最关心的? ​

【标准答案】秒杀监控大盘不是为了好看,是为了出问题 30 秒内定位到根因。

核心指标分四类:

第一类:流量指标。 实时 QPS(按接口)、Nginx 连接数、网关限流拒绝数。这几个告诉你系统正在被多少流量打。限流拒绝数突然暴增,说明有异常流量或者限流阈值设低了。

第二类:业务指标。 实时库存剩余、下单成功率、订单转化率(下单/点击秒杀按钮)。库存掉得比预期快 10 倍,可能是刷单或者库存初始值写错了。

第三类:中间件指标。 Redis CPU/内存/连接数/慢查询、MQ 消息积压量/消费延迟、DB 连接池/慢 SQL/TPS。Redis CPU 飙到 90% 同时慢查询增多 → 有大 key 或者热点 key。

第四类:应用指标。 接口 RT(P50/P99/P999)、Tomcat 线程池使用率、GC 频率与耗时。P99 突然从 50ms 涨到 2 秒 → 下游某个依赖慢了或者 GC 抖动。

大盘布局: 顶部一条是"红灯绿等"——库存 > 0 且成功率 > 99% 显示绿色,否则红色。中间核心曲线图(QPS + RT + 库存),底部是各中间件的健康得分。

告警策略: 不是所有指标都配告警,只配三个最致命的:成功率 < 95%、库存异常(扣超过上限)、RT 超过阈值。其他的在仪表盘上用颜色标出趋势,不要告警轰炸。


【面试题】9. Kafka、RocketMQ、RabbitMQ 在秒杀场景怎么选?各有什么优劣? ​

【标准答案】秒杀消息场景主要是削峰 + 异步解耦,三者的核心差异在吞吐、延迟和事务消息。

Kafka:吞吐第一。 顺序写磁盘,批量拉取,单机轻松百万 QPS。适合日志、埋点、监控数据 - 秒杀的用户行为日志、风控事件流都用 Kafka。但延迟略高(几十毫秒),且没有延迟消息。秒杀下单这条链路不太用 Kafka,用在下游大数据分析侧。

RocketMQ:秒杀最佳解。 阿里出品,天然为电商设计。吞吐几十万 QPS,延迟 1-5 毫秒,关键是有事务消息——先发 half 消息,本地业务执行成功了再 commit,失败了 rollback。这完美解决"扣库存成功但消息没发出去"的问题。还有延迟消息,下单 30 分钟未支付自动取消。

RabbitMQ:稳定可靠。 Erlang 写的,稳定到几年不需要重启。功能丰富——死信队列、TTL、延迟队列插件。但吞吐量比前两者低一个量级(万级),秒杀高峰可能扛不住。

结论: 秒杀核心链路 → RocketMQ(事务消息+高吞吐+低延迟);日志/埋点 → Kafka;中小型秒杀 3000 QPS 以内 → RabbitMQ 完全够用。


【面试题】10. 面试官爱问的秒杀对比题:同步扣库存 vs 异步扣库存,什么时候用哪个? ​

【标准答案】这不是非此即彼的选择,而是分场景组合使用。

同步扣库存: 用户提交下单 → Redis Lua 原子扣库存 → 扣成功返回"抢到了"。用户实时感知结果,体验最好。适合库存少、竞争激烈的场景——茅台、iPhone。用户必须当场知道结果。

异步扣库存: 用户提交下单 → 进队列排队 → 轮询查结果。库存扣减在后台异步做。体验差一点——用户要等几秒才知道抢没抢到,但系统扛得住极高并发。适合 12306 这种,几十万人同时抢一张票。

要不要混合? 可以。先同步预扣,后异步确认。 Redis Lua 同步扣一次,返回用户"抢购中",然后发 MQ 异步让数据库做最终确认。数据库扣成功了订单生效,失败了回滚库存。用户看到的是"处理中→成功/失败"。这套组合兼顾了体验和性能。

核心权衡: 同步 = 体验好但系统压力大,异步 = 体验差但能扛更高并发。库存 1000 以内选同步,10000 以上考虑异步排队。没有银弹,只有合适场景的选择。