系统设计这门课,我啃了快六年才敢说自己真正入门。期间翻过的资料、记过的笔记、画过的架构图,摞起来比工位上的显示器都高。去年我把这些零散内容整理成了system-design-notes这个项目——一份从基础理论到实战案例全覆盖的系统设计笔记,目的是让后来者不用再像我当年那样,面对浩如烟海的英文博客和技术文档不知道从哪下嘴。
这份笔记解决的核心痛点很明确:系统设计知识太散、太杂,网上资料要么高屋建瓴讲理论,要么直接甩一个复杂架构图,中间缺少一条"从问题到方案"的完整推理链。而实际工作中,无论是做技术选型、评审架构方案,还是准备晋升答辩,都需要你具备"接到一个模糊需求,快速拆解出关键技术点,再给出合理设计"的能力。所以这套笔记适合所有想系统提升架构设计能力的人——不管是准备面试的候选人、刚带项目的技术负责人,还是单纯对"大系统是怎么造出来的"好奇的后端工程师。
1. 内容整体设计与思路拆解
1.1 为什么需要一份自己的系统设计笔记
先说说我踩过的坑。早期我也和其他人一样,收藏了一堆"Top 10 System Design Concepts"之类的文章,刷的时候觉得什么都懂了,合上电脑一周后连一致性哈希的虚拟节点是干嘛的都讲不清楚。问题出在哪?别人的笔记是别人的思维链,不是你的。系统设计本质上是一种"约束条件下的决策艺术",同样的需求,不同人基于自己的经验积累会做出完全不同的取舍。照搬别人的结论,等于跳过思考直接记答案,遇到变体题目立刻抓瞎。
这就是我建立system-design-notes的初衷:不是做一个知识搬运仓库,而是把每次设计方案时的决策过程和取舍依据记录成结构化笔记。比如记录"为什么这个场景选 Redis 而不是 Memcached",我会写清楚当时考虑的缓存值大小、是否需要持久化、集群扩容的运维成本对比,而不是简单一句话"Redis 功能更强"。这种笔记方式迫使你把每个技术选型背后的 trade-off 想透,时间长了,你会发现自己面对新问题时能自然形成一套分析框架。
笔记项目的组织方式,我参考了国外知名系统设计课程的大纲结构,再按自己的理解做了裁剪。整体分四大块:基础理论层(一致性、可用性、分区容错等抽象概念)、核心组件层(负载均衡、缓存、消息队列等具体部件)、案例实战层(短链接、Feed 流、秒杀等完整设计)、方法论层(如何把模糊需求转化为技术方案)。这个分层思路很重要——很多人学系统设计最大的问题是"只见树木不见森林",单个组件都认识,组合起来就不会了。分层笔记能强迫你先建立全局视角,再逐步深入细节。
1.2 笔记项目的整体架构与组织方式
具体到目录结构,我的system-design-notes长这样:
01-basics/:理论基础,包含 CAP、BASE、一致性哈希、幂等设计等02-components/:核心组件,负载均衡、缓存、消息队列、分布式 ID 等03-cases/:经典案例,短链接、聊天系统、推荐系统、秒杀系统等04-methodology/:方法论,需求澄清、容量估算、架构演进思路05-interview/:面试专项,高频题目速查、答题模板、模拟演练记录assets/:所有架构图的源文件,我统一用 Draw.io 画,方便随时改
这跟传统的"收藏夹式笔记"有个本质区别:每个专题不是一篇孤立的文章,而是一条完整的推理链路。比如缓存这个专题,我会从"为什么需要缓存"开始,讲清楚缓存能解决什么问题、引入后会带来什么新问题(缓存穿透、击穿、雪崩),再给出每个问题的对应解法,最后附上一个真实业务场景的选型对比。这样读者顺着笔记走一遍,等于亲手做过一遍完整的架构推演,而不是被动接受结论。
说实话,最开始整理这套笔记时我也犹豫过,网上现成的笔记项目太多了,自己再整理一份是不是重复造轮子?但用下来的体会是:整理笔记本身才是最有价值的环节。当我尝试把"如何设计一个高可用系统"这件事写成结构化文档时,才发现自己对不少概念的理解是模糊的——比如我一直以为读写分离能解决所有数据库压力问题,但真去算容量时才发现,写多读少的场景下主库才是瓶颈。这个发现直接影响了我后续的技术方案偏好。
2. 核心知识模块拆解
2.1 从 CAP 到一致性模型:先搞清楚你要什么
任何一个系统设计,绕不开的第一个决策点是:在一致性、可用性、分区容错性之间怎么取舍。CAP 定理说分布式系统最多满足两个,实际生产环境网络分区不可避免,所以 P 是必选项,真正纠结的是 C 和 A 怎么平衡。但这里有个新手特别容易误解的地方——CAP 里的"一致性"指的是线性一致性,即所有节点在同一时刻看到完全相同的数据,这是一种极端严格的定义。
真实的业务系统几乎不会追求这种级别的强一致。比如电商下单后扣减库存,允许用户稍后刷新才看到最新库存,这就是最终一致性——系统保证在没有新写入的情况下,经过一段时间后所有副本会收敛到相同状态。笔记里我专门做了一个一致性模型谱系图:强一致、顺序一致、因果一致、最终一致,从上往下约束递减、性能通常递增。实际设计时,我建议先用一个简单问题判断:"如果两个用户同时读到不同版本的数据,业务上能否接受?不能接受就上强一致方案,能接受就用最终一致加补偿机制。"
具体到工程实现,一致性模型直接决定了存储选型。需要强一致就老老实实用单机数据库或 Raft 等共识算法支撑的分布式数据库;能接受最终一致,就可以放心上多副本异步复制,甚至用消息队列做异步解耦。我在笔记里反复强调一个观点:所谓架构设计,本质是在"业务容忍度"和"技术实现成本"之间找最小交集。这个交集找得越准,方案越简洁,后续运维成本越低。
2.2 高并发场景的三板斧:缓存、异步、削峰
高并发是系统设计里最常被提及的话题,但很多人一上来就想着堆机器。实际经验告诉我,99% 的性能问题都可以用"缓存-异步-削峰"三个手段解决,只有极少数场景才需要真正意义上的分布式改造。笔记里我把这三板斧拆成了独立专题,每个专题都配了真实业务场景。
先说过载保护,最核心的思路是"让多余的流量在系统入口就失败,而不是拖垮整个系统"。比如秒杀场景,如果十万人同时抢一百件商品,业务上完全可以接受"九万九千人看到已售罄",那就不该让所有请求都打到数据库。做法通常是三层缓冲:CDN 挡静态资源,Redis 预扣库存挡动态请求,数据库只承担最终的原子扣减。每一层都加上自己的限流阈值,超出的请求直接返回失败,保护后端的稳定性。
异步解耦这块,我的切身体会是:消息队列最大的价值不是"快",而是"稳"。把写操作同步拆出去,主流程只需保证"消息已投递",后续的积分、通知、搜索索引更新全部异步执行。这样即使下游某个服务挂了,主流程不受影响,消息积压等恢复后慢慢消费。笔记里我记录了某次线上故障的复盘:促销活动期间用户通知服务超时,因为同步调用链太长导致主链路 RT 飙到 3 秒。改成 MQ 异步之后,主流程稳定在 200ms 以内,这就是异步的价值。
削峰也是类似逻辑。秒杀、抽奖这类瞬间流量极大的场景,与其把机器按峰值容量买齐,不如在入口加一个"漏斗",把瞬时流量拉平到一段时间内慢慢处理。最简单的手段就是队列削峰——请求进来先塞进队列,后端按自己的处理能力匀速消费。这里有个容易踩的坑:队列不是无限的,要提前估算积压上限和消费者的最大吞吐量,否则流量峰值远超消费能力时,队列本身就成了新的瓶颈。
2.3 数据层的扩展之路:从分库分表到读写分离
数据层往往是系统演进到一定阶段后最大的瓶颈。笔记里我记录了数据层扩展的完整路径:单库单表 -> 读写分离 -> 垂直分库 -> 水平分表。很多新手容易犯的错是过早做分布式改造,实际上大多数业务在单库性能优化到位的前提下,撑到几百万用户是没有问题的。我见过一个日活百万级的电商项目,核心订单表就是单库,靠的是合理的索引、归档和缓存,数据库负载常年低于 20%。
真正需要分库分表的信号很明确:单表数据量过大导致索引失效,或者写入并发触及单机瓶颈。这时候要考虑的不只是"怎么拆",更重要的是"按什么维度拆"。订单数据适合按用户 ID 取模拆分,因为查询总是从"我的订单"出发;消息数据适合按时间维度分表,因为冷热数据分明,旧数据直接归档。笔记里我特别标注了一个教训:拆分维度如果和主要查询维度不一致,会导致大量跨库查询,性能反而更差。
读写分离是另一个性价比很高的手段,但实现时有个隐蔽的坑——主从延迟。刚写入的数据马上去从库读,经常读不到。解决方案不外乎几种:关键读请求强制走主库、写入后短暂缓存最近数据、或者业务上接受这种短暂不一致。笔记里我倾向于第三种方案最多,因为大多数业务场景下"写完立刻刷新"并不是强需求,设计时提前和产品对齐预期,比在技术上绕来绕去省事得多。
3. 实操过程与核心环节实现
3.1 拿到一个设计题,第一步该做什么
不管是面试还是实际工作,系统设计的起点永远是同一个动作:澄清需求。我见过太多人(包括当年的自己)拿到题目就开始画架构图,结果方向错了,后面全白干。澄清需求要问的问题是有套路的,笔记里我总结了一个五连问:
- 功能范围:核心功能有哪些?非核心功能可以先不做?
- 用户规模:预估多少用户?多少日活?峰值 QPS 是多少?
- 数据规模:数据总量多大?每天新增多少?需要存多久?
- 读写比例:读多写少还是写多读少?这决定了缓存和数据分片策略
- 一致性要求:业务上能否容忍短暂不一致?能不能接受最终一致?
举个例子,拿到"设计一个短链接服务"这个经典题目,先不要想用什么技术栈,而是先算数据量。假设日新增 100 万条短链,每条记录 100 字节,一年下来大约 36GB 数据、3.6 亿条记录。这个量级下,单库完全撑得住。那问题核心就不是"怎么水平扩展",而是"短链生成算法怎么保证唯一且不可预测"以及"如何做到高性能重定向"。
容量估算这部分,很多人觉得难,其实核心就是小学算术加上几个经验常数。一台普通的数据库服务器,每秒大概能处理几千次简单查询;一台应用服务器配合本地缓存,单机扛住每秒一两千次请求是常有的事;Redis 单实例读写吞吐可以到十万级 QPS。把这些基准值背熟,任何题目都能快速算出"需要多少台机器"这个最关键的数字。笔记里我整理了常见组件的性能基准表,实测下来这些数字虽然有波动,但作为量级估算完全够用。
需求澄清和容量估算做完之后,整个设计就有了边界条件。接下来才是画架构图、选组件、定协议,每一步都可以对应回前面算出的数字和业务约束。这样产出的方案才是有理有据的,而不是"网上都是这么设计的我也这么画"。
3.2 经典案例拆解:设计一个短链接服务
我用短链接这个案例来展示笔记里完整的推演过程,因为它麻雀虽小五脏俱全,涉及生成算法、存储设计、重定向性能、数据统计等多个系统设计核心知识点。
第一步,短链生成算法。这是全网讨论最多的点,方案无非两类:哈希取余和发号器。MD5 或 MurmurHash 把长 URL 哈希成固定长度,再截取前 7 位作为短码。这种方案的问题是哈希碰撞,碰撞后要么重新加盐再哈希,要么落库时捕获唯一键冲突。发号器方案则用分布式唯一 ID 生成器(比如雪花算法或数据库自增),得到数字 ID 后再转换为 62 进制字符串,7 位 62 进制能表示 62 的 7 次方约 3.5 万亿个组合,远超实际需求。两相对比,我笔记里的结论是:中小规模系统用发号器更省心,因为天然无碰撞。
第二步,存储设计。短码到长 URL 的映射关系,用关系型数据库存最稳妥,表结构就两列:short_code做主键,long_url做普通字段。查询场景极端简单——按主键查一行,这种访问模式其实用 Redis 做缓存能把性能拉满。笔记里记录了当时的一个取舍:因为读多写少(读占比 95% 以上),缓存命中率会很高,于是采用 Cache-Aside 模式,先查 Redis,没命中再查数据库并回填缓存。这里有个小细节:短码要设置合理的过期时间防止脏数据长期占用内存,同时加布隆过滤器拦截不存在的短码请求,防止恶意攻击穿透到数据库。
第三步,重定向性能优化。用户访问短链时,为了 SEO 和历史兼容,通常返回 301 永久重定向,但这会让浏览器缓存导致统计数据失真;返回 302 临时重定向则每次都会请求服务端,便于记录点击数据。业务上更看重点击统计,所以倾向 302。这个选择题我特意写进了笔记,因为它是一个典型的"技术选型依赖业务目标"的例子——没有绝对正确,只有适不适合。
第四步,容量规划。回到前面估算的数字:几亿条记录、读 QPS 峰值可能上万。数据库主从加 Redis 缓存,两台应用服务器加一组哨兵实例,完全够用。整套方案下来不需要引入任何复杂的分布式组件,却能把每个环节的性能做到极致。这一点我想特别提醒:系统设计不是越复杂越好,用最朴素的组件解决问题是更高级的能力。
3.3 经典案例拆解:设计一个消息推送系统
第二个案例是消息推送系统,因为它更好地展示"写多读少"场景下异步架构的核心思想。这个案例比短链接复杂一个量级,涉及用户连接管理、消息分发策略、推送通道接入等多个模块。
先考虑规模:假设 1000 万注册用户,日活 200 万,每个用户平均一天收到 20 条站内推送通知,那么每天的消息量是 4000 万条。如果这些消息全部要求实时推送到 APP,而且每条消息要针对不同用户定制内容,这就不是简单查表能解决的问题了。
连接的接入层是关键。移动端和服务端保持长连接,服务器需要维护海量连接的会话状态。这个会话层必须设计成无状态的,这样任意一台服务器挂了,用户重连到任意一台机器都能继续收到消息。实际做法是建立一个连接网关层,所有的推送请求先进消息队列,由连接网关的消费者把消息推送到对应设备的长连接上。网关层的消费者实例数量可以根据消息积压情况动态扩缩容,这就是异步架构带来的弹性。
消息内容的个性化处理也值得记一笔。如果 4000 万条消息每条都要实时拼接内容,对上游服务的压力会非常大。常见的降级方案是预聚合:模板消息提前生成好,到了推送时间直接按用户 ID 分组批量发送;只有真正需要实时计算的个性化推送才走完整链路。笔记里我画了一张分层架构图,把"消息生成"和"消息投递"完全解耦成两个独立的服务,中间的 MQ 既是缓冲,又是天然的重试队列。这样做的好处很明显:投递失败的消息可以自动重试,完全不影响消息生成服务的稳定运行。
这套设计里没有用到任何高深的理论,全是基础组件的合理组合:MQ 削峰和异步、Redis 存储在线状态、连接网关做水平扩展。但它完整呈现了一个合格系统设计的思考过程:从需求澄清到容量估算,从模块拆分到选型决策,每一步都有依据,每一步都能自圆其说。这正是我在笔记里反复强调的目标——不是背下来某个系统的架构图,而是掌握拆解任意系统的方法。
4. 常见问题与排查技巧实录
4.1 学习系统设计时绕不开的四个误区
整理笔记的这几年,我接触过大量刚开始学系统设计的开发者,大家踩的坑出奇地一致,这里集中记录四个最典型的。
误区一:只记结论,不记推导过程。看别人的方案觉得"有道理",但没想过换一个业务背景这个方案还可能成立吗?比如看到"缓存用 Redis"就直接抄,却没分析自己的数据量是不是真的需要 Redis 的丰富数据结构,也许本地缓存加数据库就够了。破解方法是练习的时候强制自己写下"为什么":为什么这里要引入缓存?引入后多出的缓存一致性成本能否接受?写不出来的地方就是还没真正理解的地方。
误区二:一上来就追求"高可用、分布式、微服务"。我见过有人设计一个用户量几百人的内部系统,硬上了微服务和 Kubernetes,结果运维成本比开发成本还高。正确的姿势是从最简单的单体方案开始,逐步识别瓶颈,在瓶颈处做针对性扩展,这和业务增长曲线是匹配的。笔记里我专门有一节叫"演进式架构",强调设计不是一步到位的,而是随着业务发展不断演化的。
误区三:忽略非功能需求。很多同学设计的系统功能完备,但一问监控告警怎么做、数据如何备份恢复、故障时怎么降级,就答不上来了。实际上这些"看不见的部分"才是决定系统稳定性的关键。我参与过的线上事故复盘,十次里有八次不是代码逻辑写错了,而是没有限流导致连锁故障,或者没有降级开关导致故障范围扩大。
误区四:背诵架构图。这是最隐蔽也最浪费时间的误区。背下来"短链接系统架构图"并不等于会设计短链接系统,因为面试官稍微改一下约束条件(比如要求短码不可预测、要求支持自定义alias),背诵方案就崩盘了。系统设计能力的唯一来源是大量实战练习——把经典题目自己从头到尾推演一遍,再和优秀方案对比找差距,如此往复,才能形成真正属于自己的设计直觉。
4.2 面试与实战中的高频问题速查表
这部分笔记我后来整理成了一张速查表,方便临场快速回忆。以下是最常被问到也最容易被忽略的几个问题:
- "这个方案里数据库和缓存的一致性怎么保证?"我的标准回答框架:先区分是强一致还是最终一致场景,再给出缓存策略(Cache-Aside、Write-Through 等),最后说明极端情况下的补偿方案(如延迟双删、MQ 重试)。关键是要展现出"我知道这里有不一致窗口,并且我有应对预案"。
- "如果这个服务挂了,整个系统会怎样?"这是考察容灾意识的经典问题。回答时沿着调用链逐层分析:入口层挂了怎么办(多活、DNS 切换)、业务层挂了怎么办(优雅降级、服务熔断)、数据层挂了怎么办(主从切换、数据备份恢复)。核心是让面试官看到你的"故障思维"。
- "用户量再涨十倍,哪里会先扛不住?"这个问题的意图是考察对系统瓶颈的判断力。我通常按"入口->应用->缓存->数据库"的顺序排查,并给出量化的估算:当前 QPS 是多少、每层组件上限是多少、差了多远。能清晰说算出这些数字,比背诵任何架构模式都加分。
- "为什么不用 XXX?"这是最考验真实功底的提问。比如"为什么用 RabbitMQ 而不是 Kafka?"我回答时会从多个维度对比:消息可靠性要求、吞吐量需求、消息堆积能力、运维复杂度、团队熟悉度。关键是不能只说"RabbitMQ 功能更丰富"这种空话,要结合前面的需求澄清给出适配的理由。
我还在速查表里整理了一些答题套路的加分项:讲方案时顺手提一句"这里我加了一个布隆过滤器拦截非法请求"——这种小细节最能体现实战经验;主动指出方案的缺点和可能的改进方向——这比全程只说"没问题"显得成熟得多。
4.3 维护这套笔记一年后的心得
最后聊聊笔记本身的维护经验,这部分可能比任何技术内容都更值得分享。
第一,架构图一定要保存源文件。我见过太多人笔记里贴了个截图,后来想改却发现只能重画。从第一天起我就把所有的 Draw.io 源文件放在assets/目录,正文里只引用导出的 PNG。这样系统需求一变,改源文件重新导出就行,几分钟的事。
第二,每篇笔记都要标注日期和适用场景。同样的技术,2021 年的最佳实践和今天可能完全不同。比如一致性哈希的负载均衡方案,在云原生时代已经被服务网格的 LDS/CDS 机制大量替代了。笔记里每个方案我都标注了记录时间和使用背景,半年后回看时能清楚地看出技术认知的迭代痕迹。
第三,定期复盘很重要。我每做完一个真实项目和每完成一次模拟面试,都会回到笔记里修订相关内容——补充新的 trade-off、修正过时的结论、删除不再适用的方案。这套笔记之所以能越用越顺手,靠的就是这种持续迭代。现在它对我来说已经不只是一份复习资料,更像是我个人架构决策经验的"外部记忆库"。
如果你也想建一套自己的系统设计笔记,我的建议是从你最近做过的一个系统开始,把当时的设计决策完整记录下来,再补上"如果重做一次,哪里会不一样"。这比对着教程抄知识点有效得多。等技术积累到一定程度,自然会形成一套只属于你自己的设计方法论,那才是这个领域最值钱的东西。