Skip to content

【面试题】1. 秒杀场景下的分布式事务你是怎么处理的?降低库存和创建订单分属不同服务,怎么保证一致性? ​

【标准答案】秒杀场景对一致性要求是"最终一致"而非"强一致",具体分三步:

第一步:扣库存用 Redis Lua 原子操作,这一步是同步的。 库存扣成功了才继续往下走,扣失败直接返回售罄。

第二步:扣减成功后发 MQ 消息,订单服务异步消费创建订单。 这里用 RocketMQ 的事务消息或 RabbitMQ 的 Publisher Confirm + 本地消息表双保险。核心思路:业务操作和消息发送绑定在一起。

第三步:定时对账任务做兜底。 每天晚上跑一次对账:对比 Redis 扣减记录和订单表,发现有扣库存但没订单的,自动补偿回滚库存。有订单但库存对不上的,人工介入。

TCC 在秒杀里太重了, Try-Confirm-Cancel 三个步骤加两次 RPC,不适合秒杀的高并发场景。秒杀选"可靠消息最终一致",简单、够用。


【面试题】2. 秒杀系统中动静分离是怎么做的?CDN 缓存了什么? ​

【标准答案】动静分离的核心思路:把不变的、变化慢的内容前置到离用户最近的地方,只让变化快的数据回源。

静态数据全部走 CDN: 商品图片、CSS/JS 文件、HTML 模板这些推送到 CDN,设置很长的缓存过期时间(如 30 天),文件名带 hash 实现版本更新。用户打开页面,静态资源命中的是 CDN 边缘节点,几百毫秒就加载完。

动态数据用异步刷新: 商品标题、价格、库存这些"准静态"数据,在 CDN 层用 Edge Side Includes 或 SSR 渲染后缓存 30 秒。页面框架是静态的,里面动态片段通过 Ajax/SSI 异步加载。

真正实时的是库存状态: 只用一个轻量 JSON 接口查 Redis,不做页面渲染。秒杀按钮是否亮起、库存数字,靠这个接口 1-2 秒轮询一次。

效果: 90% 以上的请求在 CDN 层解决,打到源站的只有 10%,源站压力骤降。


【面试题】3. 秒杀缓存预热怎么做?热点 Key 怎么识别和处理? ​

【标准答案】缓存预热要抢在秒杀开始前做完,不能等活动开始了才"现预热"。

预热方案: 活动开始前 30 分钟,用脚本把商品信息、库存数量、用户资格等核心数据批量写入 Redis。提前构建热点商品的本地缓存(Caffeine/Guava),设置较短的刷新间隔。上线前做一次"缓存演练"——模拟高并发读,确认数据都在缓存里。

热点 Key 识别: 分事前和事中。事前靠业务规则——参加秒杀的商品自然是热点,直接标记。事中靠中间件:Redis 的 hotkey 检测、Sentinel 的热点参数流控,或者自己写一个滑动窗口统计——请求次数超过阈值自动识别为热点。

处理方式: 热点 Key 分散到多个副本 key(如 stock_1001_0、stock_1001_1),随机路由,避免单 key 打爆一个 Redis 节点。对于读多写少的热点数据,放到本地缓存,和 Redis 两级命中。库存这种需要强一致性的热点,用 Redis Cluster 的 HashTag 隔离到单独节点。


【面试题】4. 下单链路怎么优化才能扛住更高 QPS? ​

【标准答案】优化方向很简单:少做事、并行做、异步做。

第一,减少 RPC 调用次数。 用户下单必经之路是:校验资格 → 扣库存 → 创建订单。原来每个步骤都单独查 Redis/DB,优化后把校验数据打包到一个 Redis hash 里,一次 hmget 拿到所有信息。RPC 从 5 次砍到 1 次。

第二,能并行的绝不串行。 校验用户资格、校验商品状态、风控检查这三者没有依赖关系。用 CompletableFuture 并行调用,总耗时 = max(三个中最慢的),而不是 sum(三个加一起)。

第三,能异步的绝不阻塞。 扣库存成功就返回用户"抢到了",创建订单、发短信、写日志这些全部异步丢消息队列慢慢处理。用户的等待时间从 500ms 降到 50ms。

第四,链路瘦身。 对秒杀链路做 RT 监控,找出最慢的环节,逐层优化。接入层用异步 Servlet(Tomcat NIO),不阻塞 IO 线程。


【面试题】5. 秒杀 URL 为什么要隐藏?怎么防止用户提前下单? ​

【标准答案】秒杀 URL 如果不隐藏,黄牛写个脚本提前把接口抓出来,秒杀开始的一瞬间直接调用,正常用户根本抢不过。

具体做法分三步:

第一步:动态 URL。 秒杀地址不是固定的 /seckill/123,而是 /seckill/{商品ID}/{动态加密串}。加密串 = MD5(商品ID + 秒杀开始时间 + 盐值),活动开始前 3 秒才下发到前端。脚本拿不到这个加密串就调不了接口。

第二步:接口时间校验。 即使拿到了 URL,服务端拿到请求后先校验:当前时间是否在秒杀时间窗口内。提前一秒请求都直接拒绝,返回"活动未开始"。

第三步:多重验证。 即使绕过了前两步,还有用户资格校验、防重复提交、风控规则等着。用户必须登录、有购买资格、没超过限购次数,少一个都下单失败。

一句话: URL 是门锁,时间校验是狗,风控是枪。三道防线,黄牛破得了一两层也破不了全部。


【面试题】6. 分布式 ID 在秒杀订单里怎么生成?雪花算法有什么坑? ​

【标准答案】秒杀订单 ID 要求:全局唯一、趋势递增(方便建索引)、高性能。

方案:雪花算法(Snowflake)。 64 位长整型,1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。每毫秒生成 4096 个 ID,单机 QPS 轻松百万级,不依赖数据库,完美适合秒杀。

但雪花算法有三个坑:

第一,时钟回拨。 服务器时间被 NTP 往回拨了,可能产生重复 ID。解法:美团 Leaf 的方案是缓存过去几毫秒的最大 ID,发现时钟回拨就拒绝生成,或者用序列号段位补偿。也有方案是检测到回拨就等待时间追上来。

第二,机器 ID 怎么分配。 10 位机器 ID 不能手动配,容易冲突。一般用 ZooKeeper 的临时顺序节点自动分配。也可以用 Redis incr 分配 worker ID。

第三,worker ID 满了怎么办。 可以用 IP + 进程 PID 的 hash 来扩展,不一定死绑 10 位 worker ID。

简单场景可以直接用美团的 Leaf 方案, 已经帮你在雪花算法上做了容错和号段模式。


【面试题】7. Nginx + Lua 在秒杀中能做哪些前置校验?为什么不是纯后端做? ​

【标准答案】Nginx + Lua 相当于在流量入口就做一次"安检",把无效请求拦在应用层外面。

能做的校验:

1. IP 黑名单。 维护一个 Redis 里的黑名单集合,Nginx 通过 Lua 读取,匹配的 IP 直接返回 403,不往后透传。

2. 秒杀时间窗口。 秒杀开始前,所有下单请求直接返回"活动未开始",不需要到后端判断。OpenResty 的 ngx.now() 获取时间,对比秒杀起止时间。

3. 用户频控。 用 Redis 的 incr + expire 做用户级别限流,同一用户一秒内只能请求一次。超频的返回 429。

4. 简单库存校验。 库存为 0 时直接返回售罄,减少打到后端的无效请求。但这个库存数据需要后端异步推送给 Nginx 的共享内存。

为什么不在纯后端做? 因为 Nginx 是 C + Lua,单机轻松 10 万 QPS。无效请求在这里拦截掉,后端压力直接减半。如果全部打到 Java 应用层再拒绝,线程池和连接数都被浪费了。


【面试题】8. 秒杀压测怎么设计?如何保证压测结果真实可靠? ​

【标准答案】压测目标不是"跑一遍没挂就行",而是要算出系统的真实水位。

第一步:业务场景建模。 不能只压一个接口。按线上真实流量比例来:浏览商品页 60%、点击秒杀 25%、下单 10%、支付 5%。用 JMeter 或 wrk 配置不同的线程组模拟。

第二步:数据隔离。 压测数据必须和线上数据隔离开,用影子库或独立的 Redis instance。压测流量在 header 里带特殊标记,中间件识别后路由到影子环境。

第三步:梯度加压。 从 1000 QPS 开始,每次翻倍,一直跑到系统出现第一个瓶颈。记录每个梯度下的 RT、成功率、CPU、内存。找到拐点——QPS 到了某个值,RT 突然飙升,这就是系统的极限水位。

第四步:全链路压测。 不要只压应用服务器,要连带 Nginx、Redis、MQ、数据库一起压。生产环境做全链路压测最真实,但要做好数据隔离和回滚方案。

容量评估结论: 线上极限水位 × 60% = 安全水位。比如极限 8000 QPS,那告警阈值设 5000,到了就扩容或限流。


【面试题】9. 用户秒杀成功但库存回滚了怎么办?补偿机制怎么设计? ​

【标准答案】这个问题问的是:Redis 扣成功了,但下游(订单、支付)失败了,库存怎么恢复?

第一层补偿:Redis 实时回滚。 当订单创建失败(比如用户资格校验不通过),立即用 Lua 脚本把库存加回去。这个操作和扣库存一样,必须原子化,不能加超了。

第二层补偿:定时对账任务。 每 5 分钟跑一次对账脚本:拉出过去 10 分钟内 Redis 扣减记录(有标记"已扣未下单"的),对比订单表。Redis 扣了但订单没创建成功的,自动回滚库存。防止实时回滚丢失的情况。

第三层:数据库最终兜底。 对账脚本同时对比数据库库存和 Redis 库存,两边不一致就告警并人工介入。核心原则:数据库里的库存是最终真相,Redis 可以校正。

用户体验的细节: 库存回滚的同时,告诉用户"抢购失败",同时可以给一张"下次优先"的优惠券,减少用户流失。回滚时注意不要触发超卖——加了库存之后,要保证总扣减数 ≤ 总库存。


【面试题】10. 秒杀系统中怎么实现热点库存隔离?Redis Cluster 的 HashTag 怎么用? ​

【标准答案】热点库存隔离的核心思想:把秒杀商品的库存放在单独的 Redis 节点上,不让热点 Key 影响其他业务。

具体做法:

第一,HashTag 路由。 Redis Cluster 默认按 key 的 CRC16 哈希分布到 16384 个 slot。用 {} 包裹的部分作为哈希依据。比如把秒杀库存 key 写成 {seckill:stock}:1001,所有带 {seckill:stock} 的 key 会落在同一个 slot,进而落在同一个节点。

第二,独立实例。 更彻底的方案是给秒杀业务一个独立的 Redis Cluster,和用户缓存、商品缓存完全物理隔离。秒杀的 Redis 炸了,不影响首页、商品详情、用户登录这些核心流程。

第三,副本冗余。 热点 Key 在单节点上克隆多份,比如 stock_1001_0、stock_1001_1、stock_1001_2,客户端随机选一份去扣减,后台定时同步三个副本的库存总量。虽然一致性弱一些,但能做到"分而治之",避免单节点 CPU 瓶颈。

原则: 热点不怕有,怕的是热点绑在一个点上把整个集群拖垮。隔离 + 分片 + 副本三件套,确保爆炸半径可控。