Skip to content

【面试题】1. 秒杀容量评估:1000件库存预计20万人抢,各层需要扛多少QPS?怎么推算? ​

【标准答案】容量评估从业务指标反推,不拍脑袋。核心公式:峰值 QPS = 用户数 × 点击率 ÷ 窗口秒数。 20万人 × 30%点击率 ÷ 60秒窗口 = 1000 QPS 理论值。但真人行为不是均匀撒的——高峰集中在前 5 秒,可能涌入 40% 的人。所以实际按 3 倍冗余:下单接口按 3000 QPS 设计。层层折算: 100万 PV → 10万独立用户进秒杀页(10%) → 6万人点按钮(60%) → 前端削峰拦掉 50% 无效点击 → 3万请求到网关 → 网关限流再拦 70% → 9000 到应用层 → Sentinel 拦 60% → 3000 到 Redis 扣库存 → 最后 1000 件成交。关键经验: 各层 QPS 梯度递减,网关层容量应该是应用层的 3-5 倍,Redis 层是数据库层的 10 倍以上。

【面试题】2. Redis Cluster 主从切换的 10-30 秒真空期,秒杀系统怎么不超卖? ​

【标准答案】Redis Cluster 主节点挂了到从节点接管,中间有 10-30 秒不可用。这 30 秒里不能扣库存也不能查库存——后果是库存可能丢、也可能超卖。兜底方案一:本地缓存挡读。 每个应用实例用 Caffeine 缓存 1 秒 TTL 的库存快照。主从切换期间,读走本地缓存返回"库存还剩 X",不报错不阻塞。兜底方案二:连接池快速切换。 Lettuce 客户端配置拓扑刷新间隔 3 秒,发现主挂了立即连接到新主。连接池 minIdle 设为 8,切换时不需要新建连接,直接复用。兜底方案三:数据库乐观锁守最后一道。 就算 Redis 这 30 秒里扣错了,数据库 UPDATE stock SET count=count-1, version=version+1 WHERE count>0 AND version=? 会拒绝超卖。关键一句话:不能用 Redis 的高可用当借口——数据库乐观锁才是超卖的终结防线。

【面试题】3. RocketMQ 事务消息的"三段式"怎么保证扣库存和发消息原子? ​

【标准答案】RocketMQ 事务消息解决的是一个经典难题:扣库存成功了但消息没发出去,订单就丢了。三段式流程:① Half 消息。 生产者先发一条对消费者不可见的 half 消息,MQ 返回成功。② 执行本地事务。 生产者收到 half 成功回执后,执行 Redis Lua 扣库存。扣成功了给 MQ 发 commit,失败了发 rollback。③ 事务状态回查。 如果生产者在第二步挂了没回执,MQ 每隔 1 分钟调生产者的回查接口问"那条 half 消息对应的扣库存到底成没成?"。回查逻辑很简单——查 Redis 里有没有扣减记录,有就 commit,没有就 rollback。核心原理: 扣库存的"结果"被固化在 Redis 里,回查阶段能从 Redis 恢复状态,保证最终一致。即使中途挂了,回查机制也能自愈。选 RocketMQ 的原因:Kafka 没有事务消息,RabbitMQ 的事务性能太差,只有 RocketMQ 专为这种场景设计。

【面试题】4. Redisson 分布式锁的看门狗续期机制是怎么回事?为什么需要自动续期? ​

【标准答案】用 Redis 锁有一个经典坑:业务还在执行,锁自己过期了——第二个请求冲进来拿到同样的锁,数据就乱了。Redisson 的看门狗(Watchdog)就是解决这个的。 加锁时如果不显式设过期时间,Redisson 自动设 30 秒,同时启动一个后台定时任务——每 10 秒(30 秒的 1/3)检查一下:锁还在不在?当前线程是不是还持有这个锁?两个都是就续期,把过期时间重置回 30 秒。为什么是 1/3? 续期太早浪费资源,太晚有可能没续上锁过期了。30 秒 / 3 = 10 秒的安全窗口恰到好处。什么时候需要看门狗? 锁持有的业务逻辑耗时不确定时——比如扣库存后还要调风控、发通知,50ms 到 2 秒都有可能。不设死过期时间、靠看门狗续期是最佳实践。如果业务逻辑卡住超过 30 秒呢? 看门狗会一直续,但锁不释放就是死锁——这种场景要设最大加锁时间 lock.tryLock(5, 30, TimeUnit.SECONDS),显式限制锁持有上限。

【面试题】5. 限流算法对比:固定窗口、滑动窗口、令牌桶、漏桶各有什么坑?秒杀各层用什么? ​

【标准答案】四种算法本质不同,用错了会出生产事故。固定窗口 —— 临界突刺是致命坑。 比如限制每分钟 100 个请求,窗口是 00:00-00:59。可能出现第 59 秒来 100 个、第 60 秒(新窗口)又来 100 个,2 秒内涌入 200 个——限流失效。滑动窗口 —— 解决了突刺但开销大。 把 1 分钟切成 6 个 10 秒格子,时间滑动。比固定窗口精确,但要多存 6 个计数器。令牌桶 —— 允许突发。 以固定速率放令牌,突发流量可以消费积攒的令牌。网关层用令牌桶最合适,burst 设 10-20,短时间超限不断然拒绝。漏桶 —— 强制恒定速率。 请求先进桶,以固定速率漏出。适合保护下游数据库——不管上游怎么冲,到 DB 就是恒定 500 QPS。秒杀实战搭配: 网关层令牌桶(允许微突发),应用层 Sentinel 滑动窗口(精确控制热点接口),数据库层漏桶(恒定速率保活)。

【面试题】6. Caffeine 本地缓存 + Redis + DB 三级缓存,数据一致性怎么做? ​

【标准答案】三级缓存的挑战不是性能,是一致性——Caffeine 里是旧数据,Redis 里是新数据,用户看到了什么?方案一:超短 TTL + 主动失效(推荐)。 Caffeine 设 1-3 秒 TTL,最多 3 秒不一致。同时 Redis 更新后通过 MQ 广播 Cache Invalidation 消息,所有应用实例消费消息后主动 caffeineCache.invalidate(key)。TTL 是兜底,MQ 通知是主路径——延迟 100ms 内清除。方案二:版本号校验。 数据里带版本号,Caffeine 取到数据后向 Redis 对一下版本。版本一致直接返回,不一致重新从 Redis 拉。这比方案一多一次 Redis 查询。方案三(慎用):Caffeine 只缓存不变数据。 商品详情、用户信息这类很少变的数据放 Caffeine,秒杀库存这种实时数据只放 Redis,不走本地缓存。一句话: 三级缓存的一致性靠"短 TTL + 主动失效",不要试图做一个强一致的三级缓存——成本高收益低。

【面试题】7. 秒杀降级开关怎么做到秒级生效?配置中心推送 + 本地缓存的协作机制? ​

【标准答案】降级开关慢一秒,线上可能多几千个失败请求——必须秒级生效。方案:Apollo/Nacos 推送 + 本地缓存双通道。 应用启动时从配置中心拉全量降级配置,缓存在本地 ConcurrentHashMap。配置中心收到变更后,通过长轮询(Nacos)或 HTTP 长连接(Apollo)推送到所有应用实例。为什么不能只靠定时轮询? 轮询间隔至少 5 秒,出事时 5 秒的延迟是不可接受的。推送模式延迟 100ms 级。双通道兜底: 主通道是推送,备用通道是本地缓存。万一推送丢了(网络抖动),本地缓存保留上一次配置值,不会因为收不到配置而降级失效。降级开关的代码写法: if (degradeSwitch.isOn("seckill.recommend")) { return fallbackResult; }。开关的判断走内存 HashMap 读,没有 IO,0.01ms 开销。关键原则: 降级开关不依赖被降级的服务——不能因为 Redis 挂了导致读不到降级配置。配置中心自身要高可用,本地缓存是保底。

【面试题】8. 秒杀流量漏斗每一层转化率大概是多少?从 100 万 PV 到成交 1000 单? ​

【标准答案】面试官问这个是想看你对"流量衰减"有没有体感。漏斗七层,层层砍半: ① CDN 缓存命中率 99%,100 万 PV 只有 1% 回源 = 1 万实际请求到 Nginx。② 网关层限流 + 时间校验再拦 50%,剩下 5000 到应用层。③ 前端削峰(按钮防抖 + Token)+ 资格校验再拦 60%,剩下 2000 进扣库存环节。④ Redis Lua 扣库存——库存 1000,2 人抢 1 件,扣成功率 50% = 1000 次扣成功、1000 次扣失败。⑤ MQ 削峰再缓冲一下,1000 条消息逐条消费。⑥ DB 乐观锁兜底,正常情况下 100% 通过。⑦ 最终成交 1000 单。漏斗比例: 100万 PV → 1万 Nginx → 5000 应用 → 2000 扣库存 → 1000 成交。每一层都是"漏斗孔",最终的成交率约 0.1%。面试官想要的关键洞察: CDN 和网关这两层做的是最便宜的拦截——用最少成本拦住最多流量,后面的 Java 层只处理精筛后的请求。省钱 + 分层 = 秒杀架构的灵魂。

【面试题】9. RocketMQ 延迟消息实现"下单 30 分钟未支付自动取消"怎么落地? ​

【标准答案】秒杀里未支付订单不自动取消,库存就锁死了——这是大忌。RocketMQ 延迟消息完美匹配: 用户下单成功后,订单服务发一条延迟消息到 RocketMQ,设延迟级别 messageDelayLevel=18(对应 30 分钟)。30 分钟后消息投递到取消订单的消费者。消费者在做什么? 取出订单号 → 查订单状态 → 如果还是"待支付",执行取消:① 更新订单状态为"已取消";② 发补偿消息回滚 Redis 库存(incrby stock:sku001 1);③ 如果这笔订单对应的是候补用户,触发补货通知。幂等性怎么保证? 取消消息可能重复消费——在 Redis 里记录 cancel:order:order123 标记,消费前先 SETNX,成功了才执行取消逻辑。几个关键细节: ① 延迟级别在 messageDelayLevel 配置里查,常用 1s/5s/10s/30s/1m/2m/.../30m。② 如果 RocketMQ 延迟消息积压,可切到 Redis 过期回调——对 order:{orderId} 的 Key 设 30 分钟 TTL,监听 Redis Keyspace Notification 触发取消。但 Redis 的通知不可靠,MQ 是正经解法。

【面试题】10. 秒杀候补补货机制:有人未支付释放库存后,怎么公平分配给候补用户? ​

【标准答案】秒杀售罄后有人 15 分钟没付款,库存还回去了——谁有资格抢回来?方案:Redis 有序队列按时间排队。 库存售罄后,后续请求不直接拒绝,而是把 userId + timestamp 写入 Redis Sorted Set:ZADD seckill:waitlist:sku001 timestamp userId。按时间戳排序,先点的排前面。补货触发: 取消订单的回滚逻辑里,除了 incrby stock,还查候补队列:ZRANGE seckill:waitlist:sku001 0 0 取出排第一的用户,发一条"候补成功"的 MQ 消息。消息体里带用户 ID + 商品 ID + 预锁 token。候补用户的体验设计: ① 候补成功时发 Push 通知"您候补的商品有货了",给 60 秒专属下单时间,token 一次性有效。② 60 秒内没下单,释放 token 给下一位。③ 候补超时 30 分钟还没有就结束候补,发"候补失败",同时推荐替代商品。公平性保证: Redis Sorted Set 按 score(时间戳)严格有序,不存在插队。候补人数超过库存 50 倍就关闭候补入口——排队太长体验更差。