Skip to content

第 23 周 · Day 3:系统设计白板实战——短链接与排行榜的完整示范 ​

对应手册任务:学习「系统设计模拟:短链接 / 排行榜」,动手「45 分钟白板设计一个系统,录音回听」,当日产出一份「设计录音」。本篇只解决一个问题:八股背得再熟,答案也是点状的,白板考的是把点串成一条 45 分钟的推理链,今天用两道出镜率最高的题完整示范一遍,让嘴和手跟得上脑子。

今日目标 ​

  1. 说得清短链接的四个考点:短码长度估算、发号三路线取舍、301 与 302 的选择、热点缓存
  2. 说得清排行榜的四个考点:ZSET 的复杂度、同分编码、冷热分层、更新风暴的批量聚合
  3. 独立完成一次 45 分钟白板录音,回听对照本文示范找出至少三处差距,重练一次

概念讲解:为什么八股背熟了,白板还是会卡 ​

前两天四十道八股刷下来,你脑子里已经堆了一仓库的点:ZSET 是跳表、301 会被缓存、号段要预分配。今天站到白板前,很可能第一句话就卡住:面试官说「设计一个短链接服务」,你张口「用 Redis」,三分钟把底牌全亮完,剩下四十二分钟无话可说。

卡壳的根源不是知识不够,是知识没有调用顺序。八股是一问一答,问什么抽什么;白板是一场独白,你要自己出题(需求澄清)、自己排议程(五阶段)、自己挑重点(深挖哪两个组件),每一环考的都是组织能力,不是记忆力。背题的人怕追问也是同一个原因:答案是一整块搬出来的,从中抽掉一根线,整块就塌了。

所以今天练的不是新知识,是编排。方法定成录音:讲的时候你觉得自己行云流水,回听才发现满嘴「反正就是」「大概」,推理的跳跃比想象中多十倍。耳朵是唯一诚实的复盘工具。下面两道题先由本篇完整示范,你再照着走一遍,差距会自己浮出来。

核心知识:两道题的完整 45 分钟 ​

流程沿用第 22 周 Day 1 的五阶段法:需求澄清 5 分钟、高层设计 10 分钟、深入组件 15 分钟、瓶颈演进 10 分钟、总结 5 分钟。每个阶段下面都给出可以直接说出口的话术,以及必须出现的数字。

1. 示范一:短链接服务 ​

阶段一,需求澄清。 照功能、规模、延迟、一致性四个维度自问自答,每个问题带默认假设:

  • 功能:长 URL 换短码,访问短码跳回原地址;自定义后缀、统计面板不做,但点击计数要做。这个假设直接决定后面选 301 还是 302。短码默认永久有效,过期回收不做,这个边界留到追问环节还会回来
  • 规模:读写比按 100:1 假设,读峰值 5000 QPS,写 50 QPS
  • 延迟:跳转 P99 50ms,用户等不起
  • 一致性:短码创建后立即可跳,要强一致;点击计数允许少量丢失

阶段二,高层设计。 核心实体只有一个:短码到长 URL 的映射。框图分两条流:

读路径(5000 QPS,主链路)
客户端 ──HTTPS──▶ 网关 ──▶ 跳转服务 ──▶ Redis 热点缓存 ──miss──▶ MySQL 映射表
                              │ 302 跳回长 URL

写路径(50 QPS)
客户端 ──HTTPS──▶ 创建服务 ──▶ 发号器 ──▶ MySQL 落库 + 回填缓存

数据流一句话:读是主链路,5000 QPS 几乎全压在跳转服务上;写是旁路,量小但一致性要求高。

阶段三,深入组件。 这道题的难度中心是发号器,三条路线摆开讲:

  1. 哈希截断:对长 URL 做 MD5,取前几位当短码,冲突就加盐重哈希。实现最简单,但每次创建都要查库验冲突,冲突率随存量上升;同一 URL 想复用同一短码,还得加唯一索引。
  2. 号段预分配:数据库只存一个 max_id,发号服务一次取走一千个号在内存里发。数据库压力从每次创建一次读,降为一千次创建才一次读;代价是实例宕机会丢一小段号,且号单调递增、可预测。
  3. 自增 ID 转 base62:ID 直接转 62 进制字符串。最省事,但短码连续,别人从 aaa000 开始遍历就能爬光你全部短链,要么接受,要么加随机置换。

容量现场算:62^3 约 24 万,平方一下 62^6 约 568 亿,再乘 62,62^7 约 3.5 万亿。按每天新建一百万条,七位短码够用近一万年。「七位」这个结论是算出来的,不是拍出来的。

重定向选 302 不选 301。301 是永久重定向,浏览器会缓存,第二次点击根本不回源,点击计数直接失效;302 每次回源,牺牲几毫秒换来统计能力,而点击计数在需求澄清里已经定下来要做。讲出「统计需求决定协议选择」,组件就和需求串起来了。

缓存:5000 QPS 直接打 MySQL 风险很大,上 Redis,热点集中在最近创建的短链,LRU 淘汰。命中率按 95% 估,数据库只剩 250 QPS,单实例就稳了。

阶段四,瓶颈演进。 量级乘十:读先死,缓存扩容,再加本地二级缓存;数据库其次,按短码哈希分片;发号器看似单点,号段模式下多实例各取各的段,天然可水平扩展。

阶段五,总结。 三十秒复述:一张映射表、一个发号器、一层缓存、一个 302。最大的取舍:用 302 的回源延迟换统计能力。

2. 示范二:实时排行榜 ​

阶段一,需求澄清。 功能:展示实时 Top100,用户能查自己的全局排名;不做多维度分榜。规模:一亿用户,行为事件峰值每秒十万条。延迟:榜单秒级新鲜度可接受,查排名 P99 100ms。一致性:排名允许短暂旧,但不能错。

阶段二,高层设计。 核心实体:用户、分数。数据流一条直线,重点是削峰:

行为事件 ──▶ Kafka(削峰)──▶ 聚合服务(按用户攒批,1 秒窗口)
                                 │
                  ┌──────────────┴──────────────┐
                  ▼                             ▼
          MySQL 行为流水                Redis 分片 ZSET × 100
          (真相源,定时对账)          (pipeline 批量 ZINCRBY)
                                                │
                                                ▼
                            局部 Top100 ──堆归并──▶ 全局热榜快照 ──▶ 查询服务

阶段三,深入组件。 第一个点,为什么是 ZSET。ZINCRBY 加分、ZREVRANGE 取前一百、ZREVRANK 查自己名次,全是对数级复杂度,底层是跳表,一亿 member 下单次操作仍是毫秒级,这是它成为排行榜标准起点的原因。

第二个点,同分怎么排。把时间戳编码进 score 低位:总分乘一个大基数,低位放「最大时间戳减去得分时间」的截断值,同分时先到的低位更大,排前面。注意 ZSET 的 score 是 double,只有 53 位安全整数,毫秒时间戳就占 41 位,编码前要先算位宽,否则分数一大就溢出。这个细节说出来就是加分项。

第三个点,冷热分层。一亿用户全塞一个 ZSET,按每条 100 字节毛估就是 10 GB 上下,单实例又贵又单点。分层:按用户哈希分 100 片,每片一百万用户,各自维护局部前一百;全局 Top100 用小顶堆把 100 个局部榜归并,一共一万个元素,毫秒级完成。查个人全局排名,各片统计分数比他高的人数(ZREVRANK / ZCOUNT)再求和。

第四个点,更新风暴。峰值每秒十万次加分直接打 Redis 会打穿。聚合服务先按用户攒批:同一秒内同一用户的多次加分合并成一次 ZINCRBY,一秒窗口刷一次,pipeline 批量提交,写入次数从十万级压到万级以内。

第五个点,持久化对账。Redis 是加速器不是真相源,所有行为流水先落 MySQL,定时任务对账,ZSET 挂了就用流水重放重建,分钟级恢复。不讲这一步,面试官必然追问「Redis 挂了怎么办」,先讲就抢到了主动权。

阶段四,瓶颈演进。 写再涨十倍:加大聚合窗口、Kafka 加分区。读爆了:Top100 固化成快照,查询服务读内存即可,甚至推到 CDN。Redis 到顶:分片数翻倍,归并成本只涨对数级。

阶段五,总结。 一句话:流水进 Kafka,聚合后写分片 ZSET,热榜归并读快照。最大取舍:用秒级聚合窗口换十倍写入余量。

3. 追问链预演 ​

白板的下半场是追问,两道题各预演一条最凶的链。

短链:「短码不够了怎么办?」先给结论:七位 3.5 万亿,正常业务用不完;真到那天,扩到八位容量直接乘 62,旧短码不动,新旧按长度区分路由。当初要是选了 301,浏览器缓存里的老跳转永远改不了,这是选 302 的又一个回报。追问还会接着压:「过期的短码能不能回收复用?」能,但要清缓存、防浏览器残留,回收池的复杂度不低,一般业务不值得。

排行榜:「ZSET 内存爆了怎么办?」先反问需求:真的需要一亿人的实时排名吗?多数产品只要前一百加自己的名次。裁掉全量榜,只留 Top100 小 ZSET,个人名次用分桶估算,内存从 10 GB 掉到百 MB 级;要精确名次再保留分片方案,把 score 编码压缩。答案的方向是把存储裁剪到查询需求,不是硬扛着加内存。

动手任务:45 分钟录音,一步一步 ​

手册任务:白板设计一个系统并录音回听。拆成 5 步,全程约 90 分钟。

第 1 步:备场。 大白纸或平板当白板,手机开飞行模式录音。纸面分三栏:左边写需求假设,中间画架构图、占一半面积,右边留给追问。开讲先划区域,这个动作本身就在向面试官展示结构感。

第 2 步:计时 45 分钟,两题选一。 推荐先选短链接,本篇示范更细。五阶段每段掐点,卡住就说出声「这里标待定,先往下走」,不停录音、不翻资料。嘴上要保持说话,沉默在真实面试里就是扣分项。

第 3 步:回听,对四张清单。 对照本文示范检查:需求澄清有没有先于画图;每个组件有没有带数字(QPS、内存、复杂度);发号三路线、301/302 这类取舍有没有讲「为什么不是另一个」;哪个阶段超时了。四条各记一笔,差距自然现形。

第 4 步:重练一次。 同题重走,专攻上一轮的差距项;时间充裕就换排行榜当第二题,白赚一次新题练习。两次录音各听开头两分钟,进步听得出来。

第 5 步:归档。 文件名带上日期和题目,收进本周目录。它是今天的产出「设计录音」,也是 Day 5 完整模拟面试前的水平基线。

回听技巧

回听把速度调到 1.5 倍,只听结构和口头禅。数一数「就是」「然后」出现多少次,再数出现了几个具体数字,两组数字的对比比任何自我评价都扎心。

常见踩坑 ​

坑 1:架构图画在纸正中间,越画越小。 后面二十分钟还要往里加缓存、加消息队列,地方不够只能挤。处方:动笔前先分栏,架构图永远占最大一块,四周留白。

坑 2:箭头光秃秃,框里没数字。 白板画图三件套:框图、箭头标协议、容量数字。每个箭头写清是 HTTPS、Kafka 还是 Redis 调用,每个框标上 QPS 或内存量级。面试官扫一眼图,不用听你开口就知道有没有量级感。

坑 3:深入组件贪多。 十五分钟最多讲透两三个点,短链接就发号加重定向,排行榜就分片加更新风暴。每个点都值十分钟,讲十个点等于什么都没讲。

坑 4:被追问就当场推翻方案。 追问是压力测试,不是否定信号。正确姿势是先稳住框架,「大架构不变,这里演进一个组件」,再局部改。整体翻案等于告诉面试官方案是纸糊的。

坑 5:录音只录一遍就归档。 第一遍只能发现「哪里卡」,重练一遍才能验证「改没改掉」。这个任务的价值在两次录音的对比,不在录音本身。

自测问题 ​

先自己答,再展开对照。答不上来的,回对应小节看一遍。

  1. 301 和 302 在短链接场景里怎么选?决定因素是什么?
参考答案

决定因素是要不要点击统计。301 永久重定向会被浏览器缓存,后续点击不再回源,统计拿不到数据;302 每次回源,能量到 PV,代价是每次多几毫秒。要统计选 302,纯跳转、追求极致性能才选 301。

  1. 发号三条路线各自的致命缺点是什么?
参考答案

哈希截断:冲突随存量上升,每次创建都要查库验冲突。号段预分配:实例宕机丢一段号,且号单调可预测。自增转 base62:短码连续可枚举,有被全量爬取的安全风险。

  1. 七位 base62 的容量是多少?现场怎么算?
参考答案

62^7 约 3.5 万亿。套路:62^3 约 24 万,平方得 62^6 约 568 亿,再乘 62 得 62^7 约 3.5 万亿。量级对了就行,面试官看的是你会不会现场推,不是背没背过结果。

  1. 排行榜同分怎么处理?编码时有什么坑?
参考答案

把时间戳截断后编码进 score 低位,总分乘大基数、低位放「最大时间戳减得分时间」,同分先到先排前。坑在 ZSET 的 score 是 double,只有 53 位安全整数,毫秒时间戳占 41 位,剩下的位宽要够放低位编码,先算位宽再定方案,否则大分数溢出、排名错乱。

  1. 一亿用户的全量榜为什么要分层?全局 Top100 怎么归并?
参考答案

一亿 member 按每条百字节毛估要 10 GB 上下,单实例又贵又单点。分层:按用户哈希分 100 片,每片维护局部前一百;全局 Top100 用小顶堆归并 100 个局部榜,一万个元素毫秒级。个人全局名次把各片「分数比他高的人数」求和。

延伸阅读 ​

  • System Design Primer,两道题的变体和更多细节都在里面,当字典查对应章节,别通读
  • 第 23 周目录,明天切到 LeetCode,图的遍历和优先队列正好给排行榜归并当底子
  • 第 1 周 Day 1,二十二周前第一课在练把类型变成参数,今天练的是把整个系统变成参数,坐在白板前的你已经不是同一个人

两段录音留好,第 23 周 Day 5 的完整模拟面试里,系统设计环节就是它们的正式版。