大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
系统设计入门
这一课你将能做到
- 用三个公式独立完成容量估算:QPS、存储量、带宽,数字不用精确但数量级必须对
- 按 4S 分析法(Scenario → Service → Storage → Scale)在 20 分钟内搭出一个系统设计答案的骨架
- 说清楚短链、限流器、Feed 流三个高频案例的核心取舍,并能对照本站判题平台讲出真实架构
为什么要学
算法题考「你能不能把一个小问题写对」,系统设计考「你能不能在模糊和约束下做权衡」。中高级后端面试(一般 3 年经验以上必考)会给你一句话题目,比如「设计一个短链系统」,然后看你:会不会先问需求而不是闷头画框、能不能估出规模、选组件时讲不讲得出为什么。
对初学者来说,系统设计最大的障碍是觉得它「无边无际」。实际上入门只需要两样东西:一组用于估算的常识数字,一个固定的回答框架。框架保证你不漏项、不冷场,估算保证你的设计和规模匹配——给 100 个用户设计分布式集群,和给 1 亿用户用单机数据库,都是不及格的答案。
先建立图像
一个典型 Web 系统从请求到数据的完整路径:
用户 → CDN(静态资源) → DNS → 负载均衡(LB)
│
┌─────────┴─────────┐
应用服务A 应用服务B ← 无状态、可水平扩展
│ │
┌─────┴──────┬────────────┴─────┐
缓存 Redis 消息队列 MQ 对象存储 OSS
│ │
数据库主库 ──同步── 从库×N ← 读写分离、分库分表
记住三个「分层直觉」:能静态化就不动态算(CDN)、能缓存就不查库(Redis)、能异步就不同步等(MQ)。所有大规模系统都是沿着这三招反复演进出来的。
跟着做
步骤 1:先背常识数字,再做容量估算
面试默认你知道这些数量级:
- 一天 86400 秒,按 10 万秒估算,方便心算
- 平均 QPS = 日活跃用户(DAU)× 人均请求数 ÷ 86400;峰值 QPS 通常取均值的 2~3 倍
- 一条文本记录(含索引开销)按 0.5 KB ~ 1 KB 估;一张图片按 1 MB ~ 5 MB 估
- 带宽 = QPS × 单次请求/响应大小;出网、入网分开算
- 存储量 = 每日新增条数 × 单条大小 × 保存天数
例题:一个日活 1000 万的短链服务,人均点 5 次短链、创建 1 条短链。
读 QPS(跳转)= 1000万 × 5 ÷ 86400 ≈ 580,峰值约 1500
写 QPS(建链)= 1000万 × 1 ÷ 86400 ≈ 116,峰值约 300
每天新短链 1000 万条,每条存 0.5 KB → 每天 5 GB,一年约 1.8 TB
一句话说清结论:读多写少(约 5:1),热点明显,一年 1.8 TB 数据,单机扛不住但分库很容易。这就是后面所有设计决策的依据。
步骤 2:用 4S 分析法拆短链系统
4S 分别是:Scenario 场景、Service 服务、Storage 存储、Scale 扩展,严格按顺序走。
S1 Scenario——先问需求,只做核心功能
- 功能:给长 URL 生成短 URL;访问短 URL 302 跳转到原地址
- 追问确认:短链有效期?是否需要自定义短码?是否统计点击量?
- 规模:用步骤 1 的数字,读远多于写
S2 Service——拆成无状态接口
POST /api/v1/shorten:长链 → 短链GET /{shortCode}:查长链并返回 302- (可选)
GET /stats/{shortCode}:点击统计
S3 Storage——两张核心表 + 一个关键算法
CREATE TABLE short_url (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(8) NOT NULL,
long_url VARCHAR(2048) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_code (short_code)
);
短码怎么来?两种方案:① 发号器生成自增 ID,再用 Base62(0-9a-zA-Z)编码,6 位就能表示 568 亿条,且不会冲突;② 对长 URL 哈希后取前 7 位(如 MD5/MurmurHash),可能冲突,需要查重重试。生产中优先方案 ①,因为 ID 单调、索引友好。
S4 Scale——按瓶颈演进
- 跳转量大 → Redis 缓存热点短码(短码 → 长 URL),挡掉 90% 以上数据库读
- 数据超单机 → 按短码哈希分库分表;冷数据归档
- 点击统计 → 跳转链路上只往 MQ 发一条消息,异步落库,不拖慢跳转
- 可用性 → 应用无状态多实例 + 负载均衡;短链服务挂了等于所有营销链接失效,必须多机房部署
步骤 3:设计限流器——四种算法逐个看
限流器是本站后端真实在用的组件(登录、接口防刷都有),也是面试常考题。
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 每个时间片计数,超限拒绝 | 实现最简单 | 窗口边界可能放过 2 倍流量 |
| 滑动窗口 | 细分子窗口,统计最近一个大窗 | 解决边界突刺 | 内存占用略高 |
| 漏桶 | 请求进桶,恒定速率流出 | 输出绝对平滑 | 允许不了合理突发 |
| 令牌桶 | 恒定速率放令牌,取到令牌才放行 | 支持突发、整体受控 | 实现稍复杂,最常用 |
分布式场景下计数必须共享:单机用内存(Guava RateLimiter 即令牌桶),多实例用 Redis + Lua 保证「读计数、判超限、写回」原子执行。本站同时实现了内存版和 Redis 版限流器,单机够用时不上 Redis,实例一多自动切 Redis——这本身就是「按规模选型」的范例。
步骤 4:设计 Feed 流——推、拉、推拉结合
题面:用户刷新首页,看到关注的人发的动态,按时间倒序排列。三种模式:
- 拉模式(读扩散):发动态只写自己的收件箱;刷新时实时拉取所有关注者的动态再合并。优点是写便宜、实现简单;缺点是大 V 粉丝多时读放大严重。适合用户量小、关注数少的阶段。
- 推模式(写扩散):发动态时 fan-out,把动态 ID 写进每个粉丝的收件箱(Redis List/ZSet)。优点是读极快;缺点是大 V 一条动态要写几百万次,僵尸粉丝也白写。
- 推拉结合:普通用户发动态走推;大 V 不推全量,粉丝刷新时「拉取大 V 动态 + 读取普通关注者的收件箱」合并。微博、Twitter 都用这个折中。
判断依据还是容量:先估算读 QPS 和写 QPS 谁是瓶颈,再决定扩散成本花在哪一边。
步骤 5:对照本站判题平台
你在本站刷题时,真实链路是:浏览器打开题目页 → Nginx 接收并转发 → Spring Boot 应用(多实例、无状态)→ 题目详情先走 Redis 缓存,未命中再查 MySQL;你提交代码后,提交记录入库,判题执行与结果回写相互隔离,判题这种耗时任务通过异步方式处理,前端再查询结果;学习进度、练习记录同样落在 MySQL,高频读取走缓存。这个架构里每个组件都能对应到上面的原则:Nginx + 多实例是水平扩展,Redis 是「能缓存不查库」,判题异步是「能异步不同步等」,登录与接口过滤器上的限流器是保护系统不被突发流量打垮。
讲清楚要点
- 需求澄清永远是第一步:系统设计题没有标准答案,只有约束下的取舍。不先问清楚用户规模、功能边界、可用性要求,后面的设计都是空中楼阁。
- 无状态才能水平扩展:应用服务器不存会话(会话放 Redis),请求打到任何一台都一样,扩容只需加机器。有状态的部分(数据库、缓存)才是真正的难点。
- 负载均衡解决单点和分摊:Nginx/LVS 用轮询、最少连接、一致性哈希等策略分发;一致性哈希还能解决「增减节点时大量缓存失效」的问题。
- 缓存要想清楚三件事:缓存什么(热点读)、过期策略(TTL + LRU)、一致性怎么保证(先更新库再删缓存,即 Cache Aside;允许短暂不一致)。还要防缓存穿透(不存在的 key 用布隆过滤器或空值缓存)、击穿(热点 key 加互斥重建)、雪崩(TTL 加随机抖动)。
- 数据库扩展有明确顺序:先优化 SQL 和索引 → 加从库读写分离 → 垂直拆库(按业务)→ 水平分片(按用户 ID 哈希)。分片会引入跨片查询和分布式事务,能不分就不分。
- 异步是削峰和解耦的核心手段:消息队列让快的调用方不等慢的处理方,还能在流量高峰时把请求缓冲下来。代价是引入最终一致性,需要幂等设计(同一消息重复处理结果不变)。
- CAP 是取舍语言不是背诵口号:网络分区必然发生,此时强一致(CP,如支付余额)和高可用(AP,如点赞计数)只能侧重一边。面试时用「这个业务错了会不会赔钱」来判断。
你可能会懵的地方
- 估算数字算错了怎么办?面试官会不会觉得我不专业? 系统设计估算考察的是数量级和推导过程,不是精确值。把公式、假设、结果都说出来(「假设人均 5 次」),面试官会直接纠正你的假设,能顺着调整反而是加分项。
- 4S 里 Service 和 Storage 到底怎么区分? Service 是「系统提供什么动作」,用接口和模块描述,不关心数据存哪;Storage 是「每个动作的数据落在哪、什么结构」。先有动作,再为每个动作设计表和选型,不要一上来就画数据库 ER 图。
- 短码为什么不直接用自增数字,还要 Base62? 纯数字短链既暴露业务量(竞品看 ID 就知道你每天新增多少),又容易被遍历爬取。Base62 编码后短、无序、不可猜,6 位容量还足够大。
- Redis 挂了缓存全没了怎么办? 多级防护:Redis 本身做主从/哨兵保证高可用;应用侧对缓存不可用做降级(直接查库但限流);数据库扛不住全部流量时,还可以返回兜底数据。设计时要想「每个组件挂了系统变成什么样」。
- 最终一致性怎么跟用户解释? 大多数场景用户天然接受:点赞数晚几秒更新、Feed 晚几条都无所谓。真正要强一致的只有钱和库存,这类场景用同步链路 + 事务,不滥用异步。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 拿到题目直接开始画架构图 | 需求和规模都未知,设计无的放矢 | 先用 3~5 分钟澄清需求、估算规模 |
| 一上来就微服务、Kafka、分库分表 | 过度设计,运维成本远超收益 | 从单体 + 主从库起步,按瓶颈逐步演进 |
| 应用服务器里存 Session/本地缓存 | 实例间状态不一致,无法水平扩展 | 无状态应用,状态放 Redis |
| 先更新缓存再更新数据库 | 并发时缓存与库长期不一致 | Cache Aside:更新库,再删缓存 |
| 限流器各实例本地计数 | 集群实际限流阈值 = 单机阈值 × 实例数 | Redis + Lua 共享计数 |
| Feed 流对大 V 也做写扩散 | 一条动态写数百万次,系统被写爆 | 推拉结合,大 V 走拉取 |
| 跳转/下单链路上同步做统计 | 慢任务拖垮核心链路 | 发 MQ 异步处理,消费端保证幂等 |
| 只讲组件不讲为什么 | 听起来像背名词,无法体现取舍 | 每个决策跟一句「因为规模/瓶颈是……」 |
卡点急救
- 面试中途被面试官打断说「不用这么复杂」:说明你过度设计了,立刻收回,说「确实,如果规模是 X,主从库加缓存就够了,分片是规模到 Y 之后的事」——展示你知道每个组件的触发门槛。
- 画不出架构图、脑子空白:按固定顺序救场——先画一个 LB 后面挂 3 个应用框,下面依次补 Redis、MQ、MySQL 主从,然后从用户请求开始讲每一跳,讲的过程设计自然就出来了。
- 被追问到没研究过的组件:不要硬编。承认没用过,但讲你会如何评估:「我没用过 X,但选型时我会看它在写放大/一致性/运维成本上和现有方案的差别」,诚实且有方法论比不懂装懂强得多。
面试常问
- 设计一个短链系统,短码冲突怎么处理? 首选发号器 + Base62,从源头不冲突;若用哈希方案,则建链时拿短码查唯一索引,冲突就换一种哈希种子或追加随机位重试,并对重试次数设上限。同时短码字段加唯一索引做最后兜底。
- 令牌桶和漏桶的区别?什么时候用哪个? 漏桶恒定速率流出,输出平滑但不允许任何突发;令牌桶按固定速率攒令牌,桶里有存量时可以一次性消费,允许合理突发。保护接口(调用方有正常波动)用令牌桶;对下游做流量整形(要求绝对均匀,比如对接第三方严格 QPS 限制)用漏桶。
- 缓存和数据库不一致怎么解决? 采用 Cache Aside:读时先查缓存未命中再查库并回填;写时先更新数据库、再删除缓存。为什么删而不是更新缓存——避免并发写导致旧值覆盖新值,且很多缓存值计算昂贵、未必马上被读。极端情况下的短暂不一致用短 TTL 兜底。
自测三问
- 给你 DAU 5000 万、人均 20 次请求的系统,你能在 1 分钟内报出平均 QPS、峰值 QPS 和一年存储量吗?
- 不看资料,能按顺序说出 4S 的四个步骤和每步要回答的核心问题吗?
- 短链、限流、Feed 三个案例,每个你能讲出一个「为什么这样选」的取舍依据吗?
小结一句话
系统设计入门 = 用常识数字估清规模,用 4S 框架不漏项,在缓存、异步、扩展之间按瓶颈做诚实的取舍。
延伸
- System Design Primer(GitHub 高星,有中文翻译)
- ByteByteGo(Alex Xu,图解系统设计)
- 《系统设计面试:内幕指南》(Alex Xu 著,有中文版,卷 1 适合入门)
- Google SRE Book(容量规划与可用性)
- 下一课:行为面试——STAR 法则、高频行为题、反问环节与谈薪技巧,这是本系列的最后一课。