照例先声明:本文是“根据公开面经整理与改编”的一场典型面试,不是我本人的亲历,也不指向任何具体公司或面试官。我读了近一年公开的秒杀、高并发、系统设计类面经与技术文章,发现电商方向的面试有一个特别稳定的“套路”:从一个秒杀题起头,然后顺着“库存 → 订单 → 缓存 → MQ → 分库分表 → 限流降级”一路追下去,像剥洋葱。所以我把它们串成了一场完整的面试,问答是拼的,题目和追问是反复出现的,回答思路是我的点评。
电商面试还有一个特点:面试官特别在意“钱和货”。少卖是损失机会,超卖是赔钱加投诉,重复扣款是事故。所以你会发现,几乎每一个方案的追问最后都指向同一个问题:“它出错了怎么办?你怎么发现?怎么修?”这也是本文的主线。
一、面试流程总览
这场面试按一小时算,节奏大致是:开场与项目(10 分钟)→ 秒杀总体设计(15 分钟)→ 库存与订单细节(15 分钟)→ 缓存、MQ 与分库分表(15 分钟)→ 限流降级与压测复盘(5 分钟)。下图是它的时间线和请求漏斗。
二、开场与项目(约 10 分钟)
考察目标:面试官先摸清你的“量级”:你做过的系统到底多大?后面的深度会照着这个量级来调节。
Q1:先介绍下你做过的最高并发的系统,QPS 多少,你负责什么?
参考回答要点:给出数字 + 峰值形态 + 你的职责 + 一个技术难点。例如“大促零点下单接口峰值约 X 千 QPS,我负责库存与下单链路,难点是热点商品库存扣减”。数字要真实,被追问“压测怎么做的、瓶颈在哪”必须答得出。
追问:“平均 QPS 和峰值差多少倍?”“瓶颈最后在哪一层?”——电商的答案往往是“热点商品的单行/单 key 争用”,而不是机器数量。
踩坑:夸大数字。面试官会用“那每秒写库多少次、连接池多大、Redis 单实例多少 QPS”来验算,露馅很快。
三、第一板块:秒杀总体设计(约 15 分钟)
考察目标:看你的架构框架:是否理解“削峰、隔离、异步、兜底”,是否知道每一层的职责与失败模式。
Q2:设计一个秒杀系统,10 万人抢 10 件商品
参考回答要点:先澄清需求,再分层。澄清:商品数量、活动时长、是否一人一单、是否需要排队、支付时限、是否允许少量超时未支付回补。核心原则:层层漏斗,把绝大多数请求挡在离数据库最远的地方。
- 入口层:活动页静态化走 CDN;按钮防重复点击与倒计时(服务端时间校准);验证码/答题打散瞬时流量。
- 网关层:按 IP/用户/接口限流;黑名单与风控拦截脚本请求。
- 预热:活动开始前把商品信息、库存写入 Redis,并在应用本地缓存商品静态信息。
- 资格判定:Redis + Lua 一次完成“库存是否充足、用户是否已抢、扣减库存、记录用户”,返回是否抢到。
- 异步下单:抢到资格的请求投递到 MQ,消费者创建订单;前端拿到“排队中”,通过轮询或推送获取最终结果。
- 数据库兜底:订单表 (用户, 活动/商品) 唯一索引防重复;库存表条件更新
where stock > 0防超卖。
公开的架构文章里有个数据我觉得很直观:把瞬时上万 QPS 的写请求放进队列,数据库按自己的承受能力(比如两千 QPS)消费,用户端换来的是几十秒内的可接受延迟;同时把“订单 + 库存”同步处理、把“优惠券、积分、推送”等次要动作异步化,响应时间可以从数百毫秒降到几十毫秒。
追问:“为什么不直接用数据库行锁扣库存?”——热点行锁串行化,10 万请求排队会占满连接池,DB 先崩;“为什么不用 Redis 分布式锁包住 DB 操作?”——锁本身是单点争用,吞吐上不去,而且锁过期后仍有超卖风险,不如让 Redis 原子操作直接承担判定。
加分:主动画出漏斗、给出每层“放行比例”的估算,并说明“抢不到的请求尽早返回,不占后端资源”。
Q3:Redis 里怎么扣库存才不会超卖?
参考回答要点:单个 DECR 是原子的,但业务需要“检查 + 判重 + 扣减 + 记录”多步,必须放进 Lua 脚本,让它们作为一个整体在 Redis 单线程里执行:
-- KEYS[1] = stock:{sku} KEYS[2] = bought:{sku} ARGV[1] = userId
if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then
return 2 -- 已抢过
end
local stock = tonumber(redis.call('get', KEYS[1]) or '0')
if stock <= 0 then
return 0 -- 没库存
end
redis.call('decr', KEYS[1])
redis.call('sadd', KEYS[2], ARGV[1])
return 1 -- 抢到Java 侧用 RedisScript 执行,返回值决定后续逻辑。
Long r = redis.execute(SCRIPT, List.of("stock:{1001}", "bought:{1001}"), String.valueOf(userId));
if (r == 1L) mq.send("seckill-order", new OrderMsg(userId, 1001L, requestId));
return r == 1L ? Result.queueing(requestId) : Result.fail(r == 2L ? "已抢购" : "已售罄");追问:
- “Redis Cluster 下 Lua 里的多个 key 怎么办?”——Lua 涉及的 key 必须落在同一个槽,用 hash tag(如上面的
{1001})保证同槽; - “单个热点 key 撑不住?”——把库存拆成 N 段(
stock:1001:0…N-1),随机/取模分段扣减,售罄判断要额外处理“某一段没了但别的段还有”; - “Redis 挂了或者主从切换,库存会不会多卖?”——异步复制下切换可能丢最近的扣减记录,所以数据库层的条件更新与唯一索引是最后防线,并在活动结束后做 Redis 与 DB 的库存对账。
踩坑:先 get 再判断再 decr(三次网络往返,非原子);或者用 DECR 后发现负数再 INCR 回补,高并发下短暂为负会误导别的读取。
Q4:怎么做到“一人一单”?
参考回答要点:多层:Lua 里用 Set 判重(快速拦截);订单表建 (user_id, sku_id/活动ID) 联合唯一索引(终极保证);消息消费端以 requestId 或业务唯一键做幂等,重复消息命中唯一索引时视为成功而不是报错重试。
追问:“Redis 里记录用户的 Set 会很大吗?”——按活动维度且活动结束后过期清理;参与人数极大时可改用 bitmap 或布隆过滤器做一层预判,但布隆有误判,不能作为唯一依据。
Q5:抢到资格但订单没创建成功,或用户没支付,库存怎么办?
参考回答要点:库存的“预占 → 确认/释放”模型:抢到时预占(Redis 扣减),订单创建成功且在支付时限内支付,则确认;创建失败(消息重试耗尽进死信)或超时未支付,则释放库存(Redis 加回,数据库库存回补),并从“已抢用户”集合移除,允许其再次参与(视业务规则)。回补必须幂等:用订单号做回补幂等键,避免一个订单被回补多次导致库存虚增。
追问:“超时未支付怎么触发?”——延迟消息(MQ 延迟队列)为主,低频扫描兜底;取消动作要用状态机条件更新(where status = 'UNPAID'),避免与支付回调并发时“已支付又被取消”。
四、第二板块:订单与库存落库(约 15 分钟)
考察目标:看你对“交易核心数据”的严谨度:ID、状态机、事务边界、幂等、对账。
Q6:订单号怎么生成?为什么不直接自增?
参考回答要点:自增 ID 在分库分表后不再全局唯一、还会暴露业务量。方案:雪花算法(时间戳 41 位 + 机器 10 位 + 序列 12 位,趋势递增,注意时钟回拨)、号段模式(批量取号,双 Buffer 预加载减少取号抖动)、或在订单号里嵌入分片基因(例如把用户 ID 的低几位拼进订单号),使得“按订单号查”与“按用户 ID 查”能路由到同一分片。
追问:“时钟回拨怎么办?”——小幅回拨等待追平,大幅回拨拒绝服务并告警,或切换 workerId;“订单号要不要带日期业务含义?”——可读性好但注意别泄露敏感信息。
Q7:下单流程里,库存、订单、优惠券、积分的事务怎么处理?
参考回答要点:不要追求一个大分布式事务。核心强一致的放一个本地事务(订单 + 库存扣减,可以在同一库或同一服务内),次要的走异步最终一致(优惠券核销、积分、通知)。异步部分用事务消息或本地消息表保证“至少一次”,消费端幂等。失败补偿:优惠券核销失败要有回滚/重试机制并可人工介入。
追问:“RocketMQ 事务消息怎么工作?”——先发半消息(对消费者不可见)→ 执行本地事务 → 提交或回滚半消息;Broker 长时间收不到结果会回查生产者本地事务状态。要点是回查接口必须能根据业务数据判断事务是否成功;“为什么不用 Seata?”——强一致成本高、对高并发热点不友好,除非资金类必须强一致,才谨慎使用并接受吞吐折损。
Q8:库存扣减在数据库层怎么写?
update inventory
set stock = stock - #{n}, version = version + 1
where sku_id = #{skuId} and stock >= #{n};
-- 影响行数为 0 表示库存不足,直接失败,不需要先查再改参考回答要点:一条 SQL 同时完成校验和扣减,避免先查后改的竞态;配合 InnoDB 行锁,热点商品仍是串行,所以只用作“通过 Redis 层后的最终写入”,把 DB 的写压力降到“有资格的请求数”。还要有库存流水表(谁、何时、因何、扣了多少),用于对账和回补幂等。
追问:“热点行还是慢怎么办?”——库存分桶(把一个 sku 的库存拆到多行,扣减时挑一行)、合并写入(批量聚合扣减)、或者在应用内排队串行化同一 sku 的扣减;“对账怎么做?”——定时对比“初始库存 − 已确认订单量 − 预占中 = 当前库存”,发现偏差告警并按流水修复。
Q9:订单状态机怎么设计?为什么重要?
参考回答要点:订单有待支付、已支付、已发货、已完成、已取消、退款中等状态,状态流转必须有白名单(如待支付只能到已支付或已取消),每次流转用条件更新(where id=? and status=?)并写状态变更日志。这样并发的“支付回调”“超时取消”“用户取消”不会互相踩踏,重复回调也天然幂等。
追问:“支付回调重复、乱序怎么办?”——以支付平台流水号做幂等,回调里不信任金额,主动查单核对;“状态卡在中间态怎么办?”——补偿任务扫描长时间处于中间态的订单,主动查询上游结果并推进。
五、第三板块:缓存一致性、MQ 与分库分表(约 15 分钟)
考察目标:看你对“三件套”的组合使用能力,以及每一个的失败模式。
Q10:商品详情走缓存,库存变了缓存怎么保证一致?
参考回答要点:先分层看待数据:商品静态信息(标题、图片)可接受较长缓存与最终一致;库存、价格敏感,展示可以用缓存近似值(“仅剩少量”),真正下单时以库存判定为准,不以展示为准。缓存更新采用 Cache Aside:更新数据库后删除缓存,设置 TTL 兜底;多业务入口修改同一份数据时,更好的做法是订阅数据库 binlog(如 Canal)由独立组件统一失效缓存,业务代码不再关心缓存维护,并配合失败重试。
追问:“为什么先更新库再删缓存,而不是先删缓存?”——先删缓存后,并发读会把旧值回填,形成长期脏数据;“删缓存失败怎么办?”——重试队列/binlog 订阅重投,加 TTL 兜底;“热点 key 过期瞬间被打穿?”——互斥重建或逻辑过期(返回旧值,后台异步刷新)。
Q11:缓存穿透、击穿、雪崩,电商里各怎么防?
| 问题 | 电商场景 | 防护 |
|---|---|---|
| 穿透 | 恶意请求大量不存在的商品 ID | 参数校验、缓存空值(短 TTL)、布隆过滤器 |
| 击穿 | 爆款商品缓存过期瞬间大量请求打到 DB | 互斥重建、逻辑过期、热点永不过期 + 主动刷新 |
| 雪崩 | 大批商品缓存同一时间过期,或 Redis 整体不可用 | TTL 加随机值、多级缓存(本地 + Redis)、限流降级、Redis 高可用 |
加分:提到热 key 探测,对超级爆款在应用本地缓存做短 TTL 副本,把 Redis 单 key 的压力摊到每个应用实例。
Q12:MQ 削峰具体怎么做?积压了怎么办?
参考回答要点:削峰的本质是把“瞬时写”变成“匀速写”:生产者快速入队、消费者按数据库能承受的速率消费。设计时要估算:峰值入队速率、消费能力(消费者数 × 单消费者吞吐)、可接受的最大延迟。分区/队列数决定并行度上限,要提前规划。
可靠性三件套:消费者处理成功后才 ACK;失败重试到上限进入死信队列,由人工或补偿任务处理;数据库唯一索引保证重复消息不会产生重复订单。
积压处理:先判断原因——生产暴增(预期内)还是消费变慢(异常)。临时扩容消费者(受分区数限制,必要时先扩分区或转发到临时的高分区 topic);排查慢消费根因(下游慢、数据库锁等待);对低优先级消息降级或延后处理。同时有积压量、消费延迟的告警,而不是等用户投诉才发现。
追问:“顺序怎么保证?”——同一订单/同一 sku 的消息路由到同一分区,单分区顺序消费;“消息丢了怎么发现?”——生产端发送结果检查与重试、业务侧对账(抢到资格数 vs 订单数)。
Q13:订单表要分库分表了,你怎么设计?
参考回答要点:先说触发条件(单表体量、写入瓶颈、容量增长,而不是“行数过千万就分”),再说设计:
- 分片键:订单场景通常按
user_id分片,因为“我的订单”是最高频查询;订单号里嵌入分片基因,使按订单号查询也能路由。 - 分片算法:取模/哈希路由简单但扩容要迁移;一致性哈希或“逻辑分片数远大于物理库数”(如 1024 个逻辑表映射到若干库),扩容时只迁移逻辑分片到新库,不改路由规则。
- 跨维度查询:商家要按商家 ID 查订单,而分片键是用户 ID,怎么办?——建异构索引:把订单数据通过 binlog/MQ 同步一份按商家分片的表(或 ES),商家端查询走这份;代价是最终一致。
- 全局唯一 ID:雪花/号段,不使用库自增。
- 跨库事务与 join:尽量避免;订单与订单明细用相同分片键放同一库;必须跨库的用最终一致方案。
- 中间件:ShardingSphere 一类的客户端分片/代理方案,注意 SQL 兼容范围、分页与排序在多分片上的归并成本。
追问:“怎么平滑迁移,不停机?”——存量数据全量迁移 + 增量同步(binlog)→ 数据校验 → 灰度切读 → 双写 → 切写 → 观察后下线旧表;“分页查询跨分片怎么办?”——业务上限制翻页深度,或用游标;“数据倾斜怎么办?”——大商家/大用户单独处理,或调整分片键与路由。
踩坑:分片键选了订单号却没有分片基因,导致按用户查订单要扫所有分片;或者一上来就分库分表,没有先考虑归档与读写分离。
六、第四板块:限流降级与压测复盘(约 5 分钟)
Q14:限流算法有哪些?你怎么选?
参考回答要点:
| 算法 | 特点 | 适用 |
|---|---|---|
| 固定窗口 | 实现最简单,窗口边界可能出现 2 倍突刺 | 粗粒度限制 |
| 滑动窗口 | 更平滑,需要记录时间片或请求 | 接口/用户级精确限流 |
| 漏桶 | 输出速率恒定,平滑流量,不允许突发 | 保护下游,整流 |
| 令牌桶 | 允许一定突发(桶容量),平均速率受控 | 入口限流,最常用 |
分布式限流可用 Redis + Lua 实现计数或令牌桶,网关层用 Sentinel、Nginx 限流等组件。要区分单机限流(简单、无网络开销、总量随实例数变化)与集群限流(全局精准、依赖中心组件)。
追问:“限流阈值怎么定?”——压测得到系统容量,乘 70%–80% 的安全系数;“被限流的用户体验?”——快速失败提示、排队页,而不是让请求超时。
Q15:降级和熔断怎么做?举例说明
参考回答要点:熔断是针对下游异常的自我保护:错误率或慢调用比例超过阈值时断开一段时间,快速失败,再半开探测恢复;降级是主动放弃非核心功能保核心:大促时关闭推荐、评价、积分展示,商品详情走静态兜底页。要点是预先定义降级开关与优先级:哪些是核心链路(下单、支付、库存),哪些可降级,降级后的兜底内容是什么,开关能否秒级生效。
追问:“熔断后怎么恢复?”——半开状态放少量探测请求,成功则恢复;“下游超时重试会不会加重问题?”——会,重试要有上限、退避与预算,避免重试风暴。
Q16:怎么证明你的系统能扛住大促?
参考回答要点:全链路压测(使用影子库/影子表与压测标记隔离真实数据)、容量评估(各层水位与扩容预案)、故障演练(Redis 主从切换、MQ 积压、下游超时)、上线前检查清单(限流/降级开关、告警、值班、回滚方案)。大促期间盯核心指标:下单成功率、库存对账差异、MQ 积压、DB 连接与慢查询。
六点五、加时赛:面试官临场追加的几道细节题
公开面经里很常见的一种情况:主线问完,面试官会在剩下的时间里从你的回答里挑几个“看着不太扎实”的点追问。下面是这类加时题里出现频率较高的几道。
Q17:怎么防止脚本刷单、黄牛?
参考回答要点:没有一招制敌,是多层组合:(1)账号维度:实名/绑定手机、账号等级与历史行为,新号限制;(2)设备与 IP 维度:设备指纹、IP 频率限制、代理与机房 IP 识别;(3)行为维度:验证码/滑块/答题,把“瞬间高频点击”变成“需要人类思考”的动作,同时起到打散流量的作用;(4)业务规则:一人一单、限购数量、收货地址/手机号维度限购;(5)事后:异常订单标记与拦截,风控团队复核。技术侧要意识到:风控规则要放在漏斗靠前的位置,而且不能依赖数据库查询,否则风控本身就会成为瓶颈,通常用 Redis 计数与本地规则引擎实现。
追问:“误伤正常用户怎么办?”——分级处置(提示验证 → 限速 → 拒绝),并提供申诉通道;“规则怎么快速更新?”——规则下发到配置中心,热更新,不发版。
Q18:秒杀开始前,库存怎么初始化到 Redis?会不会有“Redis 数据和库不一致”的时刻?
参考回答要点:活动前由定时任务或运营操作触发“预热”:从库存表读取可售库存写入 Redis,同时写入一个“预热完成”标记,秒杀入口在标记存在时才放行。预热与活动开始之间,库存不应被其他渠道修改(可以锁定该 sku 的库存变更入口,或者把活动库存单独划拨出来,与普通渠道库存隔离)。“活动库存独立划拨”是很实用的做法:从总库存里划出 N 件给秒杀,秒杀内部的超卖风险不会波及普通销售,回补也有明确去处。
追问:“活动过程中运营想加库存怎么办?”——通过受控接口增加 Redis 与数据库库存,并写库存流水,不允许直接手改缓存;“活动结束后 Redis 剩余库存怎么处理?”——把未售出的数量回写数据库并核对,然后删除 Redis 相关 key。
Q19:用户点了抢购,怎么让他知道结果?轮询会不会把服务打垮?
参考回答要点:抢到资格时返回一个请求 ID,订单异步生成。结果通知的方式:(1)前端轮询(间隔逐步拉长,如 1 秒、2 秒、3 秒,并设置总时长上限),服务端查询走 Redis 中的结果 key(请求 ID → 状态),不直接查库;(2)WebSocket/长连接推送,适合体验要求高的场景,但连接管理成本更高;(3)短信/站内信作为兜底通知。轮询接口本身也要限流,且结果 key 要有过期时间。
追问:“结果 key 写到哪一步?”——消费者创建订单成功后写“成功 + 订单号”,失败写“失败 + 原因”;未消费到时状态是“排队中”;“用户一直看到排队中怎么办?”——设定最长等待,超时提示并引导查看“我的订单”,同时后台补偿任务扫描长时间未处理的请求。
Q20:主从复制延迟下,读写分离会带来什么问题?电商里怎么规避?
参考回答要点:典型问题:用户刚下单,立刻查订单列表,从库还没同步,看不到订单。规避:(1)关键读走主库(如“下单后的订单详情页”);(2)写后一段时间内该用户的读强制走主库(用 Redis 记录标记,几秒过期);(3)用“等待复制位点”的方案,但实现复杂;(4)把强一致读的范围收窄到最小,其余读走从库。库存判定、余额校验这类写前校验的读绝不能走从库。
追问:“怎么监控复制延迟?”——数据库延迟指标告警,延迟超阈值时应用自动降级为读主库或提示;“延迟根因?”——大事务、从库单线程回放(可使用并行复制)、从库资源不足。
Q21:大促期间 Redis 出现大 key、热 key,怎么发现和处理?
参考回答要点:发现:大 key 用 redis-cli --bigkeys(生产使用需注意对性能的影响,建议在从节点或低峰期)、MEMORY USAGE、慢日志;热 key 靠客户端或代理侧统计、监控单节点 QPS 倾斜。处理大 key:拆分(一个大 Hash 拆成多个小 Hash)、用 UNLINK 异步删除而不是 DEL、避免一次性 HGETALL。处理热 key:本地缓存副本、key 打散加后缀读时随机、读写分离(读从节点)。事前预防:设计阶段就限定集合类 key 的上限,并加监控告警。
追问:“集群里某个节点负载明显高于其他节点?”——热 key 或大 key 落在同一槽,排查倾斜并按上述方法打散;“能不能在线迁移槽?”——可以,但要在低峰进行并留意迁移期间的性能影响。
Q22:库存对账具体怎么做?发现差异怎么办?
参考回答要点:三方对账:Redis 库存、数据库库存、订单/库存流水。基本恒等式:初始库存 = 当前可售 + 预占中 + 已确认售出 + 已回补的净变动。做法:活动中低频抽检(对比 Redis 与数据库差值是否在“在途消息量”范围内),活动结束后全量对账;差异处理:先判断方向——Redis 少于数据库(可能多扣、回补丢失)会导致少卖,通常能接受并补回;Redis 多于数据库(可能少扣)会导致超卖风险,需要立即关闭入口或降低放行量。修复必须基于流水,按订单号幂等地重算,禁止“拍脑袋改数字”。所有修复留痕并复盘:是消息丢失、回补重复,还是脚本 bug。
加分:提到把“对账差异数”作为大促监控大盘的核心指标之一,而不是事后才查。
Q23:(开放题)如果只给你两周时间把一个老的下单接口改成能扛大促,你先做什么?
参考回答要点:这道题考的是优先级判断,而不是技术堆砌。我会这样排:
- 第 1–2 天:摸底。梳理下单链路的调用图,压测出当前极限与瓶颈,列出所有同步远程调用与数据库写入点。没有数据不要动手。
- 第 3–5 天:砍同步。把非核心动作(积分、推送、日志)改成异步;把事务里夹带的远程调用移出事务;修复明显的慢 SQL 与缺索引。这一步往往拿到最大的收益,风险却最小。
- 第 6–8 天:保护。接入限流、超时、熔断与降级开关,并设定核心链路的优先级;给热点商品加库存预扣的缓存层,但保留数据库条件更新兜底。
- 第 9–11 天:验证。再次压测,做故障演练(Redis 不可用、MQ 积压、下游超时),准备对账脚本与回滚方案。
- 第 12–14 天:灰度与值班。小流量灰度、观察指标,制定大促值班表与应急手册。
加分:明确说出“不做什么”——比如两周内不做分库分表,因为风险高且收益不确定,先用缓存、异步与限流解决 80% 的问题。能取舍,才像高级工程师。
Q24:你觉得高并发方案里最容易被忽视的是什么?
参考回答要点:我会答“可观测与恢复能力”。方案再漂亮,如果没有链路追踪、按业务维度的指标(下单成功率、库存差异、消息积压)和明确的应急手册,出问题时团队只能靠猜。第二个容易被忽视的是数据修复能力:能不能按流水重放、能不能安全地回补、能不能在不停服的情况下修数据。这两点决定了事故是十分钟的小插曲,还是一整夜的灾难。
Q25:秒杀结束后,你会做哪些收尾和复盘?
参考回答要点:收尾是很多人忽略、但面试官爱听的部分:(1)数据收尾:关闭入口,处理 MQ 中残余消息,等待超时未支付订单释放库存,然后把 Redis 中的剩余库存回写数据库并对账;(2)资源收尾:清理活动相关的 Redis key(设置过期而不是手动大批量删除)、缩容临时扩出的实例、恢复限流与降级配置;(3)复盘产出:看四类数据——流量曲线(峰值是否符合预估)、错误与超时分布、各层拦截比例(漏斗是否合理)、用户投诉与风控命中。把“预估与实际的差距”写进下次活动的容量模板里。
追问:“如果复盘发现某一层拦截比例远低于预期怎么办?”——说明漏斗在这一层没有发挥作用,例如网关限流阈值过高、验证码没有真正打散流量,需要调整阈值或前置更早的过滤;“怎么让复盘不流于形式?”——每个结论落到具体的改进项,有负责人与截止时间,下次活动前检查完成情况。
这一题看似“软”,实则很能区分“做过完整一轮活动”和“只写过一段代码”的候选人。前者会自然提到对账、清理与容量模板,后者往往在活动开始的那一刻就结束了叙述。
七、这场面试暴露的通用规律
| 规律 | 表现 | 怎么应对 |
|---|---|---|
| 秒杀是入口,追问是主体 | 从 Redis 扣库存追到回补、对账、分片 | 每个方案都准备“失败了怎么办” |
| 多层兜底思维 | Redis 判定 + 唯一索引 + 条件更新 + 对账 | 不依赖任何单点保证正确性 |
| 一致性分层看待 | 库存/订单强一致,积分/通知最终一致 | 核心放本地事务,次要走异步幂等 |
| 削峰不是加队列这么简单 | 分区数、消费速率、积压、死信 | 会估算峰值与消费能力 |
| 分库分表看分片键 | 用户维度与商家维度冲突 | 分片基因 + 异构索引 + 逻辑分片 |
| 限流降级要预设 | 阈值、开关、兜底内容 | 压测定阈值,开关可秒级生效 |
八、复盘清单与准备建议
- 手写一遍秒杀 Lua 脚本,并讲清 Cluster 下的 hash tag。
- 画出“预占 → 确认/释放”的库存状态图,标出每个失败点的补偿手段。
- 背下分库分表三件事:分片键、全局 ID、跨维度查询(异构索引)。
- 准备一张“降级清单”:你的项目里哪些功能可降级、开关在哪、兜底是什么。
- 准备一次“库存/订单对账”的真实做法:对账频率、差异如何处理。
小林 coding 的系统设计题清单里总结过秒杀的“层层漏斗”原则和“抢购场景:入口限流 → Redis 原子检查扣减 → 一人一单校验 → MQ 异步下单 → 数据库乐观锁兜底”的路径,我在这场面试里基本都按它展开。要注意的是,这些方案在“教科书版本”和“生产版本”之间差着一堆细节:降级开关、灰度、对账、告警、值班。面试里能讲到这些细节,你就从“会背”变成“做过”。
最后给一个心理建设:高并发面试里,面试官问“如果 Redis 挂了怎么办”,并不是想让你说“不会挂”,而是想看你是否承认“它会挂”,并且知道系统在那种情况下会退化成什么样子、谁来发现、怎么恢复。坦诚地讲清楚退化路径,比假装完美更有说服力。
一句话备忘:电商高并发 = 漏斗挡流量、Lua 原子判定、MQ 削峰异步落库、唯一索引与条件更新兜底、预占释放加对账收尾,限流降级要提前设开关;每个方案都要能回答“它出错了怎么办”。
再次声明:本文是公开面经与技术文章的整理改编,不是某一场具体面试的实录。方案里的阈值、数字均为示意,请以你自己的压测与业务为准。
参考资料
- 小林coding《系统设计面试题》:秒杀“层层漏斗”、抢购场景路径、超时订单取消、QPS 突增 10 倍与 Redis 单点压力等要点,本文总体结构参考。
- 腾讯云开发者社区《剖析高并发秒杀:从流量削峰到数据一致性的架构演进与实践》:Redis Lua 原子扣库存、MQ 缓冲、联合唯一索引、死信队列与 Canal 订阅 binlog 维护缓存,本文 Q3、Q4、Q10 参考。
- 博客园《消息队列应用场景:缓存预热 + 限流排队 + Redis Lua 扣库存 + MQ 削峰填谷》:缓存预热、多层限流、异步化次要业务、削峰数值示例,本文 Q2、Q12 参考。
- 牛客网《阿里国际 java 社招一二面》:流量突增场景题与项目深挖的题型参考,本文 Q1 与限流降级部分参考。
- 牛客网《2026 Java 后端开发面试真题汇总》:Redis 分布式锁、项目 QPS 与瓶颈分析等高频题型参考。
- JavaGuide《2026 最新版 Java 后端面试通关计划》:口述式复习与问题跟踪表的备考方法,本文复盘清单参考。
一起聊聊