Skip to content

【面试题】1. 秒杀页面打开一瞬间几万人狂点按钮,前端削峰具体怎么做? ​

【标准答案】前端削峰分四招,每一招都能砍掉一半以上的无效请求。

第一招:按钮置灰 + 倒计时。 秒杀开始前按钮是灰的,显示倒计时。到时间才点亮,避免用户在开始前反复点击。

第二招:点击防抖。 按钮点亮后,用户点一次立刻变灰,1 秒内不允许再点。不是等后端返回,而是点下去就直接禁用,配合 loading 动画。

第三招:请求排队化。 用户点击后不直接调下单接口,先调一个"获取下单资格 token"的轻量接口,拿到 token 才发起真正的下单。资格接口只做校验不下单,RT 极短(<5ms),能扛住极高 QPS。资格没拿到的直接劝退,不往下透传。

第四招:静态化页面。 商品详情页面全部 CDN 缓存,只留一个实时库存数字用 Ajax 异步刷新(2 秒轮询)。没有实时库存查询的请求就不回源站。

效果: 10 万用户同时点按钮,真正打到下单接口的可能就 2000 个,剩下都被前端消化了。


【面试题】2. 秒杀资格校验怎么做?百万级白名单怎么高效判断? ​

【标准答案】资格校验是秒杀的第一道业务闸口——没资格的人不应该进入扣库存环节。

方案:布隆过滤器 + Redis Bitmap。

第一层:布隆过滤器。 活动开始前把所有有资格的用户 ID 写入布隆过滤器。用户请求进来,布隆过滤器先判断"这个用户可能存在吗?"——返回"不存在"的直接拒绝,毫秒级。布隆过滤器的误判率设 1%,只会误判"有资格"不会误判"没资格"。

第二层:Redis Bitmap 精确判断。 布隆过滤器返回"可能存在"的,再查 Bitmap。用 SETBIT seckill:qual:20250619 userId 1 标记资格,GETBIT 精确判断。一个亿级用户的 Bitmap 只占 12MB 内存。

为什么不直接查数据库? 太慢。百万级白名单查询必须全在内存,一次磁盘 IO 都不允许。布隆过滤器挡住 99% 没资格的请求,剩余的 Bitmap 确认,两层加起来延迟 < 1ms。

预热时机: 资格数据在活动开始前 1 小时就写入布隆过滤器和 Bitmap,现场不临时加载。


【面试题】3. MQ 消费速度跟不上消息堆积怎么办?消费端你有哪些优化手段? ​

【标准答案】消息堆积是秒杀常见故障——1 秒涌入 10 万条消息,消费者一秒只能处理 1000 条,很快就堆起来了。

第一优先:动态扩容消费者。 RocketMQ 支持增加消费实例,队列数预先配够(比如 16 个队列),高峰期临时扩 8 台消费者,结束后缩容。Kafka 同理,增加 partition 对应的 consumer 线程数。

第二优先:批量消费。 消费者不要一条一条拉,改成一次拉 100 条批量处理。批量 INSERT 到数据库比逐条 INSERT 快 10-30 倍。RocketMQ 设置 consumeMessageBatchMaxSize,Kafka 调大 max.poll.records。

第三招:并行消费。 消费线程池加大,比如 20 个线程同时消费 MessageQueue 分配到不同线程。但要处理乱序问题——同一个用户的订单可能并行消费,需要业务层做幂等。

第四招:业务降级。 秒杀高峰期订单创建可以简化——只写核心字段,省掉冗余校验和日志。等过了高峰期再补全订单详情。

关键组合: 先扩消费者 + 批量消费,这两招解决 80% 的堆积问题。积压告警阈值设 5 万条,到了自动触发扩容。


【面试题】4. 客户端时间和服务端时间不一致,秒杀倒计时不准怎么办? ​

【标准答案】客户端时钟不可信,用户改了手机时间就能提前抢。

解决方案:服务端时间唯一权威。

第一步:服务端下发剩余时间。 前端拿到秒杀场次时,服务端返回两个值:serverTime(服务端当前 Unix 时间戳)、startTime(秒杀开始时间戳)。前端计算 startTime - serverTime 得到真实剩余秒数。前端的 setInterval 每 1 秒减 1,不需要信任客户端时钟。

第二步:NTP 校准。 每隔 30 秒,前端偷偷请求一次服务端时间,修正倒计时的累计误差。网络延迟怎么办?取请求往返时间 ÷ 2 近似单向延迟,能控制在 100ms 以内。

第三步:下单时间网关校验。 就算前端秒数归零了,服务端收到下单请求时,强制校验 serverTime >= startTime && serverTime <= endTime。客户端提前一秒发过来的请求直接返回"活动尚未开始"。

兜底话术: 秒杀打开前显示"距离开始还有 X 秒",时钟不准也差不到哪去。

一句话: 永远不信客户端,服务端时间是唯一的钟。


【面试题】5. 秒杀系统上线前,你的预热清单有哪些? ​

【标准答案】预热不是简单的"把数据写进 Redis",是一整套启动准备。

第一类:缓存预热。 秒杀商品信息、库存数量、用户资格(布隆过滤器 + Bitmap)活动前 30 分钟加载完毕。同时预热本地缓存(Caffeine),避免首次请求的冷启动开销。

第二类:连接池预热。 数据库连接池、Redis 连接池、MQ 连接、RPC 框架连接,全部设 minIdle 并启动时一次性建立。不要让第一个真实请求来建连接——建连接的开销在秒杀场景是致命的。

第三类:JVM 预热。 用压测流量提前触发 JIT 编译,让热点代码走 C2 编译而不是解释执行。否则秒杀开始前几千个请求都在解释执行,RT 飙高。

第四类:CDN 预热。 把秒杀页面的静态资源(HTML/CSS/JS/图片)提前推到 CDN 边缘节点,全国用户打开页面都能本地命中。

第五类:检查清单。 监控大盘确认数据采集正常、告警规则生效、降级开关处于关闭状态、限流阈值没有配错。每项逐一打钩。

最后一步: 用预热脚本模拟 100 QPS 跑一遍全链路,确认每个环节都正常,再正式开门。


【面试题】6. 扣库存和预占库存有什么区别?秒杀场景为什么不用预占? ​

【标准答案】两者本质区别:扣减是"直接减",预占是"先锁后减"。

扣库存: 用户下单 → 库存 -1 → 订单创建。一步到位,库存数实时减少。适合秒杀——写一次 Redis 就完成了,延迟极低。

预占库存: 用户下单 → 库存预占(扣可用库存但未真正扣减) → 用户支付 → 真正扣减并释放预占。或者超时未支付自动释放预占。多了一个"锁定"状态,存在超时释放和回滚逻辑。适合京东普通商品,不适合秒杀。

秒杀为什么不用预占? 三个原因:第一,预占多一个状态流转(可用 → 锁定 → 已售),多一次 Redis 写操作,延迟翻倍。第二,秒杀窗口 1-2 分钟就结束了,预占后超时释放没意义——秒杀结束就是结束了。第三,预占状态下用户不支付会"锁库存",秒杀里这个行为会被黄牛利用:脚本全锁住不付款,正常人抢不到。

结论: 秒杀 = 直接扣减,不留余地。普通电商 = 预占,保障付款前的体验。


【面试题】7. Redis Cluster 数据倾斜怎么办?热点 Slot 怎么处理? ​

【标准答案】数据倾斜就是某个 Slot 的请求量远远超过其他 Slot,导致该节点 CPU/带宽成为瓶颈。

为什么会倾斜? 一般是业务 Key 设计不合理。比如秒杀库存 Key 全部落到同一个 Slot,一个节点扛着 90% 的读写。

方案一:HashTag 主动隔离(利用倾斜)。 既然热点 Key 必然集中,不如主动用 HashTag 把它们全部路由到一个节点,然后给这个节点配更高的硬件配置。{seckill:hot}:stock:1001 全落在 slot_X 对应的节点上。加内存、加 CPU,至少不会拖累其他节点。

方案二:副本分散读。 热点 Key 的读压力可以分摊到从节点。Redis Cluster 不支持自动读写分离,但客户端可以自己实现——读请求随机打到主节点或任意从节点,写请求只走主节点。

方案三:多 Key 分片扣减。 把一个库存 Key 拆成 N 个分片 Key:stock_1001_0 到 stock_1001_9,扣减时随机选一个分片。缺点是库存一致性变弱(需要定时汇总总量),但能有效分散写热点。

运维侧: 配置热点 Key 监控(redis-cli --hotkeys),定期扫描。发现热点 Key 频率超过 5000 QPS 就告警,手动迁移 slot 或启动分片。


【面试题】8. CAP 理论在秒杀系统中你是怎么取舍的? ​

【标准答案】CAP 理论说分布式系统最多同时满足一致性、可用性、分区容错性中的两个。秒杀的选择是:AP(可用性 + 分区容错),放弃强一致性。

为什么要 AP? 秒杀场景下,用户最不能接受的是"点了没反应"(不可用),而不是"抢到了但订单出来晚了"(弱一致)。一致性降级的代价用户体验可接受——晚 1-2 秒看到订单不会死。但系统不可用 10 秒,活动就炸了。

具体体现:

Redis 集群选 AP。 Redis Cluster 在网络分区时追求可用,允许主从切换期间数据可能不一致。秒杀的库存扣在分区恢复后通过对账修正,能接受短暂不一致。

MQ 选 AP。 RocketMQ 采用 Dledger 主备自动切换,在网络分区时优先保证消息可写入,可能出现短暂重复消息。消费者做幂等处理就行。

数据库严格 CP。 只有最终落库这一步用 CP——MySQL 的 ACID 事务保证订单表不会出现脏数据。但这一步在异步链路里,不阻塞用户。

总结: 流量入口和高并发层全走 AP,最终落库层走 CP。一致性可以在对账环节补回来,可用性丢了就真丢了。


【面试题】9. 秒杀失败后用户体验怎么设计?排队、候补、补偿这三点你怎么落地? ​

【标准答案】秒杀失败的体验设计决定用户会不会卸载你的 App。

第一层:快速告知结果。 库存没了立刻返回"已售罄",不要转圈 5 秒再告诉用户。40ms 内给结果——抢不到不丢人,让人等才丢人。

第二层:排队机制(库存还有但 QPS 超限时)。 用户不是被拒绝,是被"排队"。返回一个队列 token 和预计位置,前端每 1 秒轮询一次队列状态。排队时显示"前方还有 X 人,请耐心等待",给用户预期管理。120 秒还没排到就结束排队,返回售罄。

第三层:候补登记(库存已空但有未支付订单)。 用户登记意向,15 分钟内如果有人未支付释放库存,按候补顺序自动发短信通知。候补不需要高性能,进数据库就行。同时推相似商品——"这个抢完了,看看这款替代品"。

第四层:补偿优惠券。 参与秒杀但没抢到的用户,发一张小额优惠券(满 100 减 5),引导去常规频道消费。把失败用户转化为常规用户。

忌: 不要报错、不要 500、不要白屏、不要让用户刷新页面自己兜着走。每一步都告诉用户发生了什么、接下来可以做什么。


【面试题】10. 秒杀结束后数据对账和清算怎么做?哪些数据必须对齐? ​

【标准答案】对账是秒杀的收尾工作,目的就一个:确认"没有多卖、没有少卖、钱和货对得上"。

第一层:库存对账。 Redis 最终扣减数 = 数据库订单数 = 实际库存消耗数,三者必须相等。不等就要查原因:是 Redis 回滚失败?还是有人绕过了正常流程直接写数据库?差 1 件都要查清楚。

第二层:支付对账。 已创建订单 vs 已支付订单。未支付订单超过 15 分钟的要自动取消并回滚库存。支付流水和订单清算金额要一致——金额对不上就是严重的资金问题。

第三层:用户视角对账。 用户说"我抢到了"但订单表里没有 → 查 Redis 扣减日志和 MQ 消息轨迹,定位是哪个环节丢了。能补订单就补,不能补就回滚库存 + 发补偿。

第四层:风控后验。 复盘异常用户:同一个收货地址下了 30 单的、下单支付间隔 < 500ms 的、IP 集中在某个机房的。标记作弊订单,人工审核后无效化并释放库存。

工具: 全量对账脚本每天凌晨跑一次,关键字段差异 > 0 就生成告警工单。实时对账(5 分钟粒度)监控扣减成功数 vs 订单创建数,差值超过 50 就立马告警人工介入。

一句话: 秒杀结束不是真正的结束,对账跑完、三方数字一致,才算真正结束。