系统设计题是我见过最容易"会的人讲不清、不会的人讲太满"的题型。面试官说一句"设计一个秒杀系统",有人张嘴就是 Redis + MQ + 限流,讲了十分钟,面试官问一句"你这个系统 QPS 多少?库存放哪?超卖怎么保证?",就卡住了。我总结下来,系统设计不是背架构图,而是一套追问自己的流程:需求 → 估算 → 主流程 → 瓶颈 → 取舍 → 兜底。
这篇文章按 🌱/🌿/🌳 三层组织:
- 🌱 初级(0–3 年):能说清楚"核心流程和用到的组件",知道基本方案;
- 🌿 中级(3–5 年):能说清楚"为什么这么选、瓶颈在哪、常见坑是什么";
- 🌳 高级(5 年+,对标后端约 8 年):能做容量估算、讲取舍与降级、讲故障场景和数据对账、讲演进路径。
每道题给出:架构要点 + 容量估算 + 取舍,并附追问链。技术栈不局限,参考的是公开面经里反复出现的题目,具体数据均为"估算示例",面试时要用你自己业务的数字。
一、知识地图与回答框架
先给一个我自己一直用的"六步框架"。无论题目是什么,按这个顺序答,至少不会乱:
🌱 初级:回答系统设计题的基本套路
标准回答要点:
- 先复述并澄清:"我理解的需求是……,用户规模、读写比例、是否要强一致,我先做这几个假设,可以吗?"——这一步既争取思考时间,又体现工程习惯;
- 先给最小可用方案(MVP):单体 + 一个数据库怎么做,再逐步说明"当流量涨 100 倍会出什么问题、怎么加缓存/队列/分片";
- 用到的组件要说明为什么:为什么用 Redis(读多写少、低延迟)、为什么用 MQ(削峰、解耦、异步)、为什么分库分表(单表超千万行/单库写入瓶颈)。
追问链:为什么不直接上微服务?——小规模没必要,先保证主流程正确,演进路径清晰即可。
🌿 中级:容量估算方法论
估算不是玄学,就是几个固定公式:
| 指标 | 公式 | 例子 |
|---|---|---|
| 平均 QPS | 日请求数 / 86400(约 10 万秒) | 1 亿 / 10 万 ≈ 1000 |
| 峰值 QPS | 平均 QPS × 峰值系数(一般 3–10,秒杀/活动可到 100+) | 1000 × 5 = 5000 |
| 存储 | 单条大小 × 日增条数 × 保留天数 × 副本数 | 1KB × 1 亿 × 365 × 3 ≈ 110TB |
| 带宽 | QPS × 单响应大小 × 8 | 5000 × 10KB × 8 = 400Mbps |
| 单机能力(经验值) | Redis 单实例读写约 5 万–10 万 QPS;MySQL 单机热点写约千级 TPS;普通业务服务单机数百到数千 QPS | 决定机器数量与分片数 |
这些单机数字是经验量级,实际取决于硬件、命令复杂度、网络、SQL 复杂度,面试时说"按压测为准,这里取保守值"更稳。
🌳 高级:把设计题讲成"决策记录"
高级岗位的面试官其实在看你怎么做决策。我的建议是每个关键点都用"方案 A/B → 选择 → 代价 → 何时重新评估"的句式,比如"库存放 Redis 还是 DB:Redis 快但持久化有丢失窗口,所以 Redis 做预扣、DB 做最终一致落账,并有对账兜底;如果 QPS 下降到 DB 可承受,可以简化去掉 Redis 预扣"。这种讲法体现的是可演进性而不是"炫组件"。
二、秒杀系统
面试出现频率第一的场景题。核心矛盾:极短时间内的巨量请求 vs 极少的库存,绝大部分请求注定失败,所以设计目标是"把无效请求挡在越靠前的地方越好"。
🌱 初级:核心思路
- 页面静态化 + CDN:商品详情、图片、JS 全部走 CDN,只有"下单"请求打到后端;
- 按钮防连点 + 前端限流:倒计时到点才放开按钮,请求带时间校验;
- Redis 预扣库存:活动开始前把库存预热进 Redis,抢购时用
DECR/Lua 扣减,返回负数即售罄; - MQ 异步下单:Redis 扣成功的请求进队列,订单服务慢慢消费,保护数据库;
- 一人一单:Lua 中同时用 Set 判重(
SISMEMBER+SADD)或订单表唯一键(用户 + 活动)兜底。
-- 秒杀 Lua:判重 + 扣库存 一个原子步骤
-- KEYS[1]=库存key KEYS[2]=已购用户set ARGV[1]=userId
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return 2 end -- 已购买
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock <= 0 then return 1 end -- 售罄
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 0 -- 抢购成功🌿 中级:超卖、少卖、重复下单怎么防
- 超卖:Redis 端靠 Lua 原子扣减;数据库端最终落库用条件更新
update stock set num=num-1 where sku_id=? and num>=1,影响行数为 0 就失败,这是最后一道防线; - 少卖:Redis 扣了但订单创建失败(用户没付款/系统异常)→ 库存没回补。解决:订单超时未支付则关闭并回补 Redis 库存(回补也要幂等,记录"已回补"标记),失败进死信队列重试;
- 重复下单:MQ 至少一次投递,消费者按"用户 + 活动"唯一键去重;
- 热点 Key:单个商品库存 Key 全落在一个 Redis 分片上,成了热点。可以把库存拆成 N 份(
stock:1001:0~stock:1001:N),按用户 ID 哈希选一份扣,某份扣光再顺次尝试其他份。
🌳 高级:容量估算、降级与取舍
容量估算示例:某活动库存 1 万件,预计 50 万人同时点击,每人平均点 5 次,则峰值请求 250 万次,集中在开抢后 10 秒,即约 25 万 QPS。分层削减:CDN/静态化拦截页面类请求;网关按用户/设备限流(每人每秒 ≤1 次),假设可挡掉 70%,剩 7.5 万 QPS;Redis 单分片约 5 万–10 万 QPS,用 2–4 个分片承载;真正成功的只有 1 万单,MQ 消费者每秒处理 1000 单,10 秒内可处理完,数据库只承受约 1000 TPS,完全可控。结论:DB 从未直接面对峰值。
取舍与兜底:
- Redis 宕机怎么办:秒杀活动通常"可停不可错",可以暂停活动或切换到网关限流 + DB 条件更新的降级路径(吞吐低,但不会超卖);
- MQ 消息丢失:Redis 扣成功但消息没发出,用"本地消息表/事务消息",或活动结束后用 Redis 已购集合与订单表对账,补单或回补库存;
- 队列积压:秒杀成功后给用户展示"排队中",前端轮询结果(不阻塞连接);
- 防刷与黑产:验证码/答题削峰(同时拉长请求时间分布)、设备指纹、风控,秒杀的很多"流量"其实是脚本;
- 公平性:先到先得 vs 随机抽签,抽签模型可以把瞬时并发降为零(预约 + 开奖),是很多大促的实际做法。
追问链:库存扣减为什么不直接用分布式锁 + DB?——锁串行化会把吞吐限制在锁往返速度,峰值下大量线程排队超时,且锁本身可能成为故障点;原子脚本 + 预扣更轻。
三、短信/消息中台
核心是把"发送消息"从业务系统里抽出来变成公共能力:统一接入、模板管理、渠道路由、限流、幂等、重试、状态回执、成本统计。
🌱 初级:基本架构
业务方调用统一发送 API(携带业务方标识、模板编码、手机号、变量)→ 校验(签名、模板、黑名单、频控)→ 写入消息表并投递到 MQ → 发送服务消费,调用短信渠道(多家供应商)→ 回执回调更新状态。核心表:msg_task(任务)、msg_template(模板)、msg_channel(渠道配置)、msg_log(发送流水)。
🌿 中级:可靠性与治理
- 幂等:业务方传
bizId,中台用 (appId, bizId) 唯一键,避免业务重试导致用户收到多条; - 频控:同一手机号 60 秒 1 条、1 小时 5 条、1 天 10 条(验证码类),用 Redis 计数或滑动窗口;
- 多渠道路由与故障切换:按成功率/延迟/成本动态权重,某渠道失败率超过阈值自动熔断并切换备用渠道;
- 重试:发送失败按退避重试,区分"可重试(超时)"和"不可重试(空号、黑名单)";
- 优先级隔离:验证码(高优先、秒级)与营销短信(低优先、可延迟)使用不同的队列与线程池,避免营销群发把验证码挤住。
🌳 高级:合规、成本与观测
- 容量估算:营销群发 500 万条,要求 1 小时发完,则约 1400 条/秒;渠道方通常有并发限制,要在中台侧做"令牌桶按渠道限速",而不是无脑并发;
- 回执对账:发送成功≠送达。渠道回执可能晚到或丢失,需要定时主动查询状态,并做日对账(我方发送量、渠道计费量、回执量三方核对),差异用于结算与告警;
- 合规:营销类必须带退订、遵守发送时段,用户投诉/退订名单全局共享;
- 可观测:发送成功率、到达率、各渠道 P99、队列积压、各业务方用量,任意一项异常都应能告警;
- 演进:把"短信"抽象成"消息通道",扩展到站内信、邮件、App 推送、企业微信,共用模板、频控、路由、幂等能力。
四、短链接系统
🌱 初级:怎么把长链变短
核心是ID → 62 进制字符串。流程:接收长 URL → 生成唯一 ID(号段/雪花)→ 转成 base62(0-9a-zA-Z)→ 存 short_code → long_url 映射 → 访问时查映射并 302/301 重定向。6 位 base62 可表示 62^6 ≈ 568 亿个,7 位约 3.5 万亿,足够。
private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(long id) {
StringBuilder sb = new StringBuilder();
while (id > 0) { sb.append(CHARS.charAt((int) (id % 62))); id /= 62; }
return sb.reverse().toString();
}🌿 中级:细节与取舍
- 301 vs 302:301 永久重定向会被浏览器缓存,之后不再经过你的服务,减轻压力,但你统计不到点击、也无法修改/下线目标;302 临时重定向每次经过服务,可统计与控制。需要数据分析的业务用 302;
- 哈希法 vs 发号法:对长 URL 做 MurmurHash 取前几位,可能冲突,需要冲突检测与重试,且相同长链得到相同短链;发号法无冲突,但同一长链多次生成不同短码(可用长链哈希做去重表);
- 短码不可预测:顺序 ID 转出的短码可被遍历。需要保密的场景,对 ID 做可逆混淆(异或、Feistel)或加随机位;
- 缓存:读远多于写(约 100:1),热点短链放 Redis + 本地缓存(Caffeine),命中率 > 95% 后 DB 压力很小;对不存在的短码用布隆过滤器/空值缓存防穿透。
🌳 高级:容量与治理
容量估算:每天新增 1 亿条短链,保留 5 年约 1825 亿条(约需 8 位 base62);每条约 500 字节(长链 + 元数据)则约 90TB,需要分库分表(按短码哈希分片)+ 冷热分离(一年以上未访问的归档)。读 QPS:假设读写比 100:1,写 1200/s,读约 12 万/s,峰值再乘 3,缓存层是绝对主力。治理要点:过期与回收(设置有效期)、恶意链接检测与举报下线(钓鱼站点)、防刷(对同一 IP 创建频率限流)、点击统计走异步(写 MQ → 流计算 → OLAP),避免重定向链路被统计拖慢。
五、排行榜
🌱 初级:Redis ZSET
Redis 有序集合(Sorted Set)为排行榜而生:ZADD 添加/更新分数(O(log N))、ZINCRBY 增加分数、ZREVRANGE key 0 9 WITHSCORES 取前 10 名、ZREVRANK 查某人名次。同分时按成员字典序排序,如需"同分先到先得",可把时间戳编码进分数(例如 score * 2^20 + (MAX_TS - ts) 的整数组合,注意双精度 53 位尾数的精度上限)。
ZINCRBY rank:daily:20260929 10 user:1001
ZREVRANGE rank:daily:20260929 0 9 WITHSCORES
ZREVRANK rank:daily:20260929 user:1001🌿 中级:多维度与大 key
- 多周期榜:日榜、周榜、月榜各一个 key,带过期时间;周榜可由 7 个日榜用
ZUNIONSTORE聚合; - 大 key 问题:千万级用户全放一个 ZSET,单 key 过大影响迁移与删除。方案:只保留 TopN(如前 1 万)在 Redis,其余用户名次用"分段 + 估算"(按分数段分桶统计人数)给出近似名次;
- 数据来源:分数变化事件走 MQ 异步聚合后再写 ZSET,避免每个业务事件都直接写榜单;
- 持久化:ZSET 是缓存视图,明细账(积分流水)在数据库,可重建榜单。
🌳 高级:亿级用户榜单与一致性
- 分片榜 + 合并:按用户哈希分为 N 个 ZSET,每个取 TopK,再在应用层归并得到全局 TopK(K 小时代价很低,全局名次用采样估算);
- 实时 vs 准实时:对"我的名次"要求实时的场景很少,大部分场景 5–30 秒刷新一次快照即可,把读压力打到只读快照缓存;
- 防作弊与回滚:刷分行为被判定后需要"扣分重算",所以要保留流水、支持按用户重算;
- 结算:榜单结算(发奖)必须以数据库/流水为准,使用某个时间点的快照,不能拿实时榜单直接发奖。
六、红包系统
🌱 初级:发红包与抢红包
流程:发红包时扣发送者余额、创建红包记录(总金额、个数、过期时间),并预先拆分金额放入 Redis List(或用二倍均值法实时计算);抢红包时从 List 中原子弹出一个金额,记录领取,并异步入账。二倍均值法:每次抢到的金额在 (0, 剩余金额/剩余个数 × 2) 之间随机,保证期望公平且最后一个人不会为 0(每人至少 0.01 元)。
// 二倍均值法:剩余 remain 分钱、left 个人
long max = remain / left * 2;
long amount = Math.max(1, ThreadLocalRandom.current().nextLong(1, max)); // 单位:分
if (left == 1) amount = remain; // 最后一人拿走剩余🌿 中级:关键难点
- 并发抢与一人一次:Lua 脚本里"判断已领取 + 弹出金额 + 记录领取人"合成一个原子操作;
- 资金安全:发红包扣款与红包创建必须在同一本地事务,或用事务消息;过期未领完的金额退回发送者,由延迟任务触发(幂等、可对账);
- 入账异步化:抢到的金额先记录领取流水(状态"待入账"),MQ 异步增加用户余额,失败重试,入账幂等(红包 ID + 用户 ID 唯一键);
- 热点:大群红包(几万人)会让一个 key 成为热点,可以按红包 ID 分片到不同 Redis 节点,把金额列表拆成多个子列表。
🌳 高级:账务对账与"钱守恒"
红包最核心的不是抢的速度,而是资金守恒:发出总额 = 已领取总额 + 已退回总额 + 未过期剩余。设计上要做到:每一步都有流水;流水表只追加、不修改;定时任务校验守恒公式,发现不平立即告警并冻结相关红包;退回操作以"红包状态机 + 唯一退款单"为准,避免重复退款。峰值应对上,春节类活动通常提前扩容、关闭非核心功能(降级),并做全链路压测与"资金类接口不降级、体验类接口先降级"的分级预案。
七、扫码登录
🌱 初级:流程
PC 网页展示二维码(内容是一个带唯一 qrId 的地址)→ 手机 App 扫码,请求服务器"确认登录该 qrId"(携带手机端登录态)→ 服务器把 qrId 状态改成"已确认"并生成 PC 登录凭证 → PC 端通过轮询/长轮询/WebSocket 得知并完成登录。
🌿 中级:安全要点
- 二维码一次性 + 短 TTL:qrId 随机不可预测,2 分钟过期,确认后立即失效;
- "扫描"与"确认"分两步:扫码后手机端展示"某某设备请求登录,是否确认",防止用户被诱导扫码即登录(二维码钓鱼/"扫码即授权"攻击);
- ticket 一次性:PC 拿到 ticket 后换会话,换取动作用 Lua 原子"读取并删除",防止被截获重放;
- 绑定设备信息:把 PC 端的 IP、UA 在二维码生成时记录,确认页展示给用户核对;
- 通知方式:轮询实现最简单(1–2 秒一次,注意限频);长轮询/WebSocket 体验更好、请求更少。
🌳 高级:规模化与风险
估算:日登录 500 万次,峰值 300 QPS 的二维码申请,但轮询会放大 50–100 倍(每个二维码存活 2 分钟、每 2 秒一次 → 60 次),峰值可达 2 万 QPS 的读,所以状态读走 Redis、避免落库;改用长连接推送可降低一个量级。风控方面:异地扫码风险提示、对异常频繁申请二维码的 IP 限流;跨端登录还要考虑"同账号多端互踢策略"和 token 刷新机制。
八、海量数据:TopK 与去重
🌱 初级:思路总览
海量数据题的核心是"内存放不下怎么办",答案基本是三个词:分治(哈希分片)、位图/布隆过滤器(省空间的去重)、堆(TopK)。
| 问题 | 做法 | 代价 |
|---|---|---|
| 10 亿个 QQ 号去重/判断是否存在 | 位图 Bitmap:号码作下标,10 亿位 ≈ 125MB | 号码范围要可控 |
| URL 黑名单判断(允许少量误判) | 布隆过滤器 | 有误判率,不能删除(可用计数布隆) |
| 统计 UV(允许约 1% 误差) | HyperLogLog,Redis 最多约 12KB,标准误差 0.81% | 只能估算基数,不能取出元素 |
| 海量日志里访问最多的 Top100 IP | 哈希分片到多个小文件 → 各文件统计 → 小顶堆取 Top100 → 归并 | 磁盘 IO |
| 海量数据求中位数 | 按值范围分桶计数,定位中位数所在桶后再细分 | 多遍扫描 |
🌿 中级:分治 TopK 的完整步骤
题目:100GB 日志,每行一个 IP,内存 1GB,求出现次数最多的 100 个 IP。
- 哈希分片:
hash(ip) % 1000写入 1000 个小文件(约 100MB/个)。关键性质:相同 IP 一定落在同一文件,所以各文件的统计相互独立; - 每个文件内统计:用 HashMap 计数(100MB 文件内存放得下);
- 每个文件取局部 Top100:用大小为 100 的小顶堆,遍历计数结果,比堆顶大就替换;
- 归并:1000 个文件 × 100 = 10 万条候选,再用堆选出全局 Top100。
PriorityQueue<Map.Entry<String, Integer>> heap =
new PriorityQueue<>(Map.Entry.comparingByValue()); // 小顶堆
for (Map.Entry<String, Integer> e : counter.entrySet()) {
if (heap.size() < K) heap.offer(e);
else if (e.getValue() > heap.peek().getValue()) { heap.poll(); heap.offer(e); }
}🌳 高级:在线场景与近似算法
- 流式 TopK(热搜、热点检测):用 Count-Min Sketch 估计频次 + 小顶堆维护 TopK,内存恒定;或滑动窗口 + 分片聚合再归并;
- 布隆过滤器参数:位数组大小 m、哈希函数个数 k、元素数 n,误判率约
(1 - e^(-kn/m))^k;1% 误判率约需要每元素 9.6 位、k≈7。缓存穿透防护场景,误判只是多查一次库,可接受; - 实际工程选型:线上 UV 用 HyperLogLog(Redis PFADD/PFCOUNT/PFMERGE,按天分 key,周/月 UV 用 PFMERGE 合并);要求精确的(如结算)用离线 + 明细去重(Spark/Flink + 主键去重);
- 取舍:精确 vs 近似决定成本,先问业务"能容忍多大误差",能近似就近似。
九、大文件上传
🌱 初级:分片上传
把大文件切成固定大小的分片(如 5–10MB),逐个上传,最后合并。好处:失败只重传失败的分片、可并行上传提升吞吐、可暂停续传。对象存储(如 S3)官方就把它做成了"初始化 → 上传分片 → 完成合并"三步流程,官方建议 100MB 以上的对象使用分片上传;分片编号范围 1–10000。
🌿 中级:断点续传、秒传与校验
- 文件指纹:前端计算文件 hash(MD5/SHA-256,大文件用 Web Worker 分块增量计算,避免卡 UI);
- 秒传:上传前先带 hash 询问服务器,若已存在同 hash 的文件则直接返回,不再传输(注意校验防止伪造 hash:可要求补充文件头尾若干字节的校验,或服务端抽样校验);
- 断点续传:服务器记录该上传任务(uploadId)已收到的分片编号,客户端重新进入时先查询已上传列表,只传缺失分片;
- 分片校验与合并:每个分片带校验值;合并前检查分片数量与顺序;合并操作幂等;
- 推荐架构:客户端直传对象存储(服务端只签发预签名 URL 和管理元数据),文件流量不经过业务服务器,避免占满带宽与连接。
🌳 高级:成本与治理
- 分片大小取舍:太小 → 请求数多、元数据大;太大 → 失败重传代价高。经验值 5–20MB,网络差时自适应调整;
- 垃圾回收:未完成的上传会一直占用存储并产生费用,官方也建议配置生命周期规则清理未完成的分片上传,我们要有同样的定时清理机制;
- 安全:预签名 URL 设短过期、限制大小与类型;上传后异步做病毒扫描/内容审核,通过前不可访问;
- 去重存储:按内容哈希存储(内容寻址),多用户共享同一物理文件,引用计数删除;
- 容量估算示例:日上传 10 万个平均 200MB 文件 = 20TB/天,带宽峰值按 8 小时集中约 5.5Gbps,必须直传对象存储 + CDN 回源优化。
十、订单超时关闭
🌱 初级:几种方案对比
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 定时任务扫表 | 每分钟扫描 status=待支付 and create_time < now-30min | 最简单、可靠 | 有延迟;数据量大时扫表压力大(需索引 + 分页) |
| MQ 延迟消息 | 下单时发一条延迟消息,到时消费并检查订单状态 | 准时、无需扫表 | 依赖 MQ 延迟能力(如 RocketMQ 5.x 延迟消息默认最长 24 小时) |
| Redis 过期监听 | key 过期事件通知 | 看起来简单 | 不可靠:通知可能丢失,不建议用于关键业务 |
| Redis ZSET 延迟队列 | score=到期时间,轮询取到期元素 | 轻量 | 要自己处理重复消费、宕机恢复 |
| 时间轮 | 内存环形数组,槽位放任务 | 高效,适合海量定时任务 | 进程内,重启丢失,需要持久化配合 |
🌿 中级:关闭动作必须幂等且与支付竞争安全
最经典的坑:用户刚好在超时那一刻支付成功。处理方式:关闭订单与支付成功都用条件更新竞争同一个状态字段——
-- 关闭:只有仍是"待支付"才能关,影响行数=1 才继续回补库存/优惠券
UPDATE orders SET status='CLOSED' WHERE order_no=? AND status='UNPAID';
-- 支付成功:只有仍是"待支付"才能变成已支付
UPDATE orders SET status='PAID' WHERE order_no=? AND status='UNPAID';谁先更新成功谁赢,另一个影响行数为 0 就放弃。若关单先赢而随后收到支付成功回调,则走"自动退款"或"订单复活"流程,并且记录日志用于对账。关闭时回补库存、释放优惠券、解冻余额,这些动作都要幂等(用订单号作唯一标识),失败进入重试队列。
🌳 高级:海量订单与兜底
- 推荐组合:延迟消息为主 + 扫表兜底。延迟消息保证准时,定时扫描(例如每 5 分钟)捕捉因消息丢失或异常遗漏的订单,两者靠状态条件更新天然幂等,谁先谁后都不会出错;
- 分片扫描:订单表分库分表后,扫描任务按分片并行;分页按主键游标(
id > ? order by id limit 500),不要用大 offset;扫描 SQL 要走(status, create_time)联合索引; - 延迟消息的雪崩风险:大量订单同一秒到期(如整点活动),消费瞬时积压。官方文档也提示大量消息同一投递时间会造成压力。做法:延迟时间加随机抖动(如 30 分钟 ± 30 秒);
- 业务上的时间语义:支付渠道自身也有支付超时,订单关闭时间应略大于渠道支付超时,或关闭前主动向渠道查询支付状态,避免"用户其实付了款、我们把单关了"。
十一、高频追问速查表
| 问题 | 一句话答案 | 出处/延伸 |
|---|---|---|
| 系统设计怎么开场 | 先澄清需求与规模,再估算,再画主流程,最后讲瓶颈与取舍 | 第一节框架 |
| 秒杀如何防超卖 | Redis Lua 原子预扣 + DB 条件更新 num>=1 兜底 | 第二节 |
| 秒杀 Redis 挂了 | 暂停活动或降级为网关限流 + DB 条件更新,宁可慢不可错 | 第二节高级 |
| 短链 301 和 302 | 301 被缓存无法统计;需要统计/可下线用 302 | 第四节 |
| 排行榜大 key | 只留 TopN;分片榜 + 归并;名次近似估算 | 第五节 |
| 红包怎么拆金额 | 二倍均值法,最后一人拿剩余,每人至少 1 分 | 第六节 |
| 红包资金安全 | 流水只追加 + 守恒公式定时校验 + 幂等入账/退款 | 第六节高级 |
| 扫码登录安全 | 二维码一次性短 TTL、扫描与确认分离、ticket 用后即焚 | 第七节 |
| 10 亿数据去重 | 位图/布隆过滤器;容忍误差用 HyperLogLog | Redis 官方 HLL 文档 |
| 海量数据 TopK | 哈希分片 → 局部统计 → 小顶堆 → 归并 | 第八节 |
| 大文件上传 | 分片 + 断点续传 + 秒传 + 直传对象存储 | S3 分片上传文档 |
| 订单超时关闭 | 延迟消息为主 + 扫表兜底,状态条件更新保证幂等 | 第十节 |
| Redis 过期监听能用吗 | 不可靠,关键业务不要依赖 | 第十节 |
一句话备忘:系统设计 = 澄清 → 估算 → 主流程 → 瓶颈 → 取舍 → 兜底;把无效流量挡在最前面,把钱和库存放在有条件更新的数据库里做最终裁决,把所有异步链路配上对账。
最后说句实在话:系统设计面试没有"标准答案",但有"糟糕答案"——就是只报组件不讲取舍、不讲出错怎么办。哪怕你的方案朴素,只要能讲清楚数量级和退路,面试官通常就愿意继续往下聊。祝你面得从容。
参考资料
- Redis 官方:Sorted Sets:有序集合的用法、命令复杂度,是排行榜方案的基础。
- Redis 官方:HyperLogLog:PFADD/PFCOUNT/PFMERGE,最多约 12KB 内存、标准误差 0.81%,适用于 UV 统计。
- Apache RocketMQ 官方:延迟消息:延迟消息的最大时间范围与订单超时取消场景,以及大量同刻投递的压力提示。
- Amazon S3 官方:Multipart Upload 概览:分片上传的流程、优势、100MB 以上建议、分片编号范围与清理未完成上传的建议。
- JavaGuide:2026 Java 后端面试通关计划:系统设计方法论、容量规划与瓶颈分析在面试准备中的位置。
- Google SRE Book:Monitoring Distributed Systems:延迟、流量、错误、饱和度四个黄金指标,可用于系统设计中的监控与降级触发条件。
一起聊聊