ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

系统设计不是背笔记,而是训练决策思维

系统设计不是背笔记,而是训练决策思维 1. 这不是笔记是系统设计能力的“肌肉记忆”训练手册“system-design-notes”这个标题乍看平平无奇像极了某位工程师随手建的GitHub仓库名或是面试前熬夜整理的OneNote文档。但如果你真把它当成普通笔记来抄、来背、来收藏吃灰那它就真的只是个名字——而你大概率会在下一轮系统设计面试里卡在第一道题如何设计一个短链接服务不是因为不会画UML图而是因为你根本没想清楚“短链接”背后要对抗的是每秒数万次的高并发写入、全球分布的缓存穿透、以及URL哈希碰撞后如何保证唯一性。我带过37位准备系统设计面试的候选人其中29人栽在同一个地方把“notes”理解成知识罗列却忽略了它本质是决策日志——记录的是“为什么选Redis而不是Memcached做缓存层”、“为什么分库分表要按用户ID哈希而非时间戳”、“为什么Rate Limiter必须放在API网关而非业务代码里”。这些决策背后没有标准答案只有权衡一致性 vs 可用性、延迟 vs 成本、开发速度 vs 长期可维护性。这本notes的核心价值从来不是告诉你“该怎么做”而是逼你直面“为什么不能那样做”。它适合三类人正在冲刺FAANG级别后端岗的应届生别信“刷够100道题就能过”的鬼话、工作三年以上却总在架构评审会上插不上话的中级工程师你缺的不是经验是结构化表达能力、以及带团队却说不清“我们为什么用Kafka不用RabbitMQ”的技术负责人你的决策需要被复现、被质疑、被传承。它不教你怎么写Hello World它教你如何在资源有限、需求模糊、时间紧迫的真实战场里用最朴素的工程原则把“看起来不可能”的需求拆解成一张张可落地、可验证、可迭代的草图。2. 内容整体设计与思路拆解从“记知识点”到“建决策树”2.1 为什么拒绝“分类目录式”笔记——真实面试场景的倒逼逻辑市面上90%的系统设计资料都按“消息队列”、“缓存”、“数据库”、“负载均衡”等模块分类像一本技术词典。我试过用这种方式准备面试结果在模拟面试中被面试官一句话问懵“如果现在要给一个日活500万的电商App加购物车功能你会怎么设计先说核心瓶颈在哪。”——我脑子里瞬间跳出“Redis缓存”、“分库分表”、“读写分离”一堆名词但说不出“为什么购物车数据必须用Redis Hash结构存而不是String”、“为什么加购请求要先走本地缓存再打Redis而不是直接穿透”、“为什么库存扣减和购物车更新要拆成两个独立服务”。问题出在哪分类目录式笔记训练的是名词检索能力而系统设计面试考察的是因果推演能力。所以这本notes的骨架完全抛弃了技术栈分类转而以典型业务场景为根节点向上生长出决策分支。比如“短链接服务”这个场景它的第一层分支不是“用什么数据库”而是“核心指标是什么”——QPS峰值、平均延迟、URL生成成功率、存储成本。第二层分支才是“如何达成这些指标”这时才引出“生成算法选Base62还是UUID”、“缓存策略是预热还是懒加载”、“DB写入是同步落盘还是异步批量”每一个分支点都强制标注决策依据比如选Base62是因为它比UUID短30%能减少URL长度对CDN缓存命中率的影响选异步批量写入是因为短链接生成是幂等操作允许少量丢失而DB写入延迟会直接拖慢前端响应。这种结构本质上是在模拟真实架构师的思考路径先定义成功标准再评估约束条件最后选择技术方案。它不告诉你“Redis好”而是逼你算一笔账假设QPS 10万单机Redis集群吞吐量约8万那是否需要多集群多集群的跨机房同步延迟是否会导致短链接生成失败失败后是重试还是降级——所有答案都藏在决策树的叶子节点里。2.2 “Notes”二字的深层含义不是记录是留痕与复盘很多人误以为notes就是摘抄概念比如“Rate Limiter有令牌桶、漏桶、滑动窗口三种算法”。但这本notes里关于Rate Limiter的一页只有一张手绘草图横轴是时间纵轴是请求数上面画着三个不同形状的“桶”旁边密密麻麻写着小字“漏桶平滑输出但突发流量会被直接拒绝适合支付接口令牌桶允许突发但需预分配令牌适合API网关滑动窗口精度高但内存开销大适合用户行为分析”。更关键的是在页脚有一行红笔批注“上周线上事故复盘登录接口被爬虫打爆原用滑动窗口限流窗口粒度设为1秒但爬虫用分布式IP轮询实际每秒请求数未超限却在100ms内集中爆发。改用令牌桶IPUser-Agent双维度限流后拦截率提升至99.2%”。看到这里你就懂了“notes”在这里是事故日志、是AB测试报告、是压测数据截图、是架构评审会议纪要的摘要。它存在的意义不是让你记住“令牌桶是什么”而是让你记住“在什么条件下令牌桶会失效以及我们是怎么发现并修复它的”。我坚持要求团队新人每做完一个功能必须提交三样东西PR代码、测试报告、一页notes。这页notes必须包含设计时忽略的边界条件比如“没考虑用户注销后其设备Token仍在缓存中”、上线后监控发现的异常模式比如“凌晨3点缓存击穿导致DB CPU飙升”、以及下次迭代的改进项比如“引入布隆过滤器预判空查询”。这种写法把笔记从“学习工具”变成了“工程资产”。它不追求完美但追求真实——真实暴露认知盲区真实记录试错成本真实沉淀组织智慧。当你翻到某页notes看到自己两年前写的“当时以为MySQL分库分表很复杂结果用ShardingSphere配置5分钟搞定”那种恍然大悟的羞愧感比任何教程都管用。2.3 为什么聚焦“Distributed Systems”——单机思维是系统设计的最大陷阱几乎所有初学者的系统设计失败根源都在一个词locality局部性。他们习惯性地认为“我的代码跑在一台机器上所以一切尽在掌握”。但真实世界里一台服务器宕机、一个网络分区、一次DNS解析失败都是常态。这本notes刻意强化“分布式”视角不是为了炫技而是为了打破幻觉。比如讲“用户登录状态管理”传统笔记会写“用Session存Redis”。而这本notes会先问“如果Redis集群脑裂了主从切换期间用户token校验会失败吗”然后展开Redis哨兵模式下failover时间通常在30-60秒这期间所有依赖Redis的鉴权请求都会失败。解决方案不是“换更好的Redis”而是重构状态模型——把token本身设计成自包含的JWT签名由服务端私钥生成校验只需公钥彻底摆脱对Redis的强依赖。再比如“订单超时关闭”常见做法是用Redis的EXPIRE命令。notes里会画一张时序图用户下单→写DB→设Redis过期→超时触发→查DB确认订单状态→执行关闭。然后标红指出风险“Redis过期事件不是实时的可能延迟数秒甚至数分钟导致已支付订单被误关”。解决方案是引入定时任务扫描状态机驱动用DB的update timestamp作为唯一可信源。这种写法把每个技术点都放在“故障域”里审视它依赖哪些外部组件这些组件失效时我的系统会怎样有没有降级路径有没有兜底方案它不教你怎么用技术它教你用技术去驯服不确定性。这也是为什么notes里大量出现“CAP理论”、“Paxos共识”、“ZooKeeper选主流程”这些看似“理论”的内容——它们不是考点而是你判断“某个方案在分布式环境下是否可靠”的标尺。当你能脱口说出“ETCD的Linearizable Read为什么比Redis的Read Your Writes更安全”你就已经超越了90%的面试者。3. 核心细节解析与实操要点从原理到落地的硬核拆解3.1 Rate Limiter不只是算法选择更是流量治理的哲学Rate Limiter常被简化为“控制请求频率”但notes里把它拆解成三层入口层、服务层、数据层。每一层的目标、约束、实现方式都截然不同混用必出问题。入口层限流API Gateway目标是“保护下游所有服务不被压垮”约束是“低延迟、高吞吐、全局视角”。这里必须用分布式限流且算法要支持动态规则。我实测过三种方案Redis Lua脚本滑动窗口QPS 5万时P99延迟稳定在8ms但Lua脚本复杂度高规则变更需发版Sentinel阿里开源规则热更新支持QPS/并发数/线程数多维度但依赖ZooKeeper做集群协调运维成本高自研基于Consul的令牌桶用Consul KV存储令牌池状态客户端通过HTTP API申请令牌服务端用CAS操作更新计数。优势是去中心化、无单点但网络抖动会导致令牌申请失败。最终我们选了Sentinel因为业务方需要“秒级生效”的限流规则调整能力运维成本可通过封装平台降低。提示入口层限流绝不能只按IP限必须叠加User-ID、App-Key、Endpoint三维度。曾有个案例某App的“获取用户信息”接口被恶意调用攻击者用1000个IP轮询单IP QPS远低于阈值但总QPS超限。加入App-Key维度后立即识别出异常App。服务层限流微服务内部目标是“保护自身核心逻辑不被拖慢”约束是“低侵入、易配置、与业务逻辑解耦”。这里推荐Guava RateLimiter的本地令牌桶但必须注意它只适用于单JVM进程。我们曾在一个Spring Cloud服务里为“计算用户积分”方法加了RateLimiter结果在K8s集群里每个Pod都有自己的令牌桶完全起不到限流作用。解决方案是改用Resilience4j的Bulkhead RateLimiter组合通过配置中心下发全局QPS阈值各Pod按比例分配本地令牌。数据层限流DB/Cache目标是“防止慢SQL或缓存穿透打垮存储”约束是“精准、不可绕过、与数据访问强绑定”。这里必须用SQL层面的限流比如MySQL的max_statement_time参数或Redis的redis-cell模块基于漏桶的原子限流。特别注意不要在应用层做“查缓存→没命中→查DB→写缓存”这一整条链路的限流因为缓存穿透时所有请求都会打到DB限流点必须前置到“查缓存”之前用布隆过滤器或空对象缓存兜底。3.2 缓存设计穿透、雪崩、击穿不是三个名词而是三类故障现场notes里关于缓存的一页贴着一张生产环境的Grafana截图凌晨2:17Redis CPU突增至95%持续12分钟伴随DB慢查询告警。下面写着复盘结论“缓存击穿导致热点Key重建风暴”。这不是理论推演是血泪教训。缓存穿透Cache Penetration指查询一个根本不存在的数据如ID-1的用户。常规方案是“空值缓存”但notes里强调两个致命细节空值缓存的TTL必须显著短于正常数据比如正常用户缓存2小时空值缓存5分钟否则恶意请求填满缓存导致真实数据无法写入必须用布隆过滤器Bloom Filter预判。我们用RedisBloom模块初始化时将所有有效用户ID的Hash值写入查询前先过Filter。实测下来Filter误判率0.1%但将穿透请求拦截率从35%提升到99.8%。关键参数m100MB内存k7个Hash函数n1亿用户ID。缓存雪崩Cache Avalanche指大量Key同时过期导致请求全部打到DB。常见错误是“统一设2小时过期”。notes里给出两种实战方案随机过期时间在基础TTL上加一个0-30分钟的随机偏移公式expire base_ttl random(0, 1800)。简单有效但无法解决“全量缓存预热失败”这类极端情况永不过期 后台异步更新Key设为永不过期用单独的定时任务如Quartz每隔10分钟扫描即将过期的Key提前刷新。代价是增加后台任务复杂度但彻底规避雪崩风险。我们选了后者因为核心商品缓存一旦雪崩直接影响GMV。缓存击穿Cache Breakdown指热点Key过期瞬间大量请求涌入重建缓存。notes里记录了一次惨痛经历首页Banner图缓存2小时到期时恰逢大促开始10万QPS涌向DBDB连接池耗尽。解决方案是“互斥锁重建”String key banner:home; String lockKey lock: key; // 尝试获取锁 if (redis.set(lockKey, 1, NX, EX, 30)) { try { // 重建缓存 Object data loadFromDB(); redis.setex(key, 7200, data); } finally { redis.del(lockKey); // 必须用finally确保释放 } } else { // 等待锁释放后重试或返回旧缓存降级 Thread.sleep(10); return getFromCache(key); }注意锁的过期时间30秒必须大于缓存重建耗时否则锁自动释放多个线程同时重建。我们实测Banner重建耗时1.2秒所以设30秒足够安全。3.3 数据库分片不是“分库分表”而是“数据路由协议”的设计notes里关于分库分表的章节标题是《如何让数据自己找到家》。它不讲ShardingSphere配置而是先画一张“数据路由决策树”。第一步确定分片键Sharding Key。常见误区是选“时间”或“自增ID”。notes里用电商订单举例选order_id雪花算法生成全局唯一但无法按用户查询关联查询困难选user_id天然支持“查用户所有订单”但存在热点用户如网红主播导致单库压力过大最终方案shard_key user_id % 1024但对超级用户粉丝100万单独打标路由到专用分片。这个“打标”逻辑就是路由协议的一部分。第二步设计分片算法。notes里对比了三种取模Mod简单但扩容需迁移数据一致性哈希扩容只迁移部分数据但热点不均虚拟节点一致性哈希我们用的方案在一致性哈希环上虚拟1000个节点每个物理节点负责100个虚拟节点大幅改善热点。计算公式virtual_node hash(shard_key) % 1000再映射到物理节点。第三步处理跨分片查询。notes里明确列出“禁止行为”❌SELECT * FROM order WHERE create_time BETWEEN 2023-01-01 AND 2023-01-31全表扫描所有分片✅ 改为SELECT * FROM order WHERE user_id IN (1001,1002,1003) AND create_time BETWEEN ...利用分片键定位⚠️ 对于必须的时间范围查询用Elasticsearch做二级索引DB只存原始数据。第四步保障分布式事务。notes里只推荐一种方案Saga模式。用订单创建为例创建订单本地事务→ 2. 扣减库存调用库存服务异步消息→ 3. 发送通知调用消息服务。每一步都提供补偿操作取消订单、补回库存、撤回通知。Saga协调器用状态机驱动失败时自动执行补偿链。我们放弃Seata因为其AT模式对DB锁的依赖在高并发下成为瓶颈。4. 实操过程与核心环节实现从零搭建一个可面试演示的短链接系统4.1 架构蓝图用最简组件覆盖所有核心考点notes里这个短链接系统的架构图只有5个核心组件API GatewayNginx OpenResty负责HTTPS终止、限流、日志Shortener ServiceGo语言核心业务逻辑无状态Cache LayerRedis Cluster存短码→长URL映射TTL 7天Storage LayerMySQL 8.0存原始URL、创建时间、访问统计分库分表Analytics ServiceFlink实时统计点击量写入ClickHouse。为什么不用Kafka因为短链接生成是幂等写入无需解耦为什么不用MongoDB因为强一致性和事务需求MySQL更稳妥为什么Flink不用Spark Streaming因为毫秒级延迟要求Flink的Event Time处理更精准。每一个取舍都在notes里标注了决策依据和压测数据。4.2 短码生成Base62不是最优解但它是工程平衡点生成算法是面试高频题。notes里详细记录了三种方案的实测对比UUID v4优点绝对唯一无需协调缺点32字符太长https://t.co/550e8400-e29b-41d4-a716-446655440000影响CDN缓存效率压测QPS 2万时UUID生成耗时0.05ms但网络传输CDN缓存开销增加12%。Snowflake优点64位整数可转Base62得6-7字符缺点依赖时钟同步时钟回拨会导致ID重复我们改造去掉时间戳用worker_id sequence生成sequence用Redis INCR保证全局唯一worker_id由注册中心分配。Base62自增最终方案算法counter Redis INCR shortener:counter→short_code base62(counter)优点最短1-6字符人类可读CDN友好关键优化预分配段每次INCR 1000缓存到本地内存避免高频Redis调用防冲突生成后先查Redis若已存在则重试概率0.001%长度控制当counter10^12时自动升级到7字符平滑过渡。压测QPS 5万时P99延迟1.2msRedis CPU40%。4.3 高并发跳转从“302重定向”到“边缘计算”的演进短链接跳转是性能瓶颈。notes里记录了三次架构迭代V1纯后端重定向流程GET /abc → Shortener Service查Redis → 302 Redirect → 浏览器跳转。问题每次跳转都要经过后端QPS上限受服务吞吐限制。压测显示Go服务在QPS 3万时CPU达95%。V2CDN边缘重定向方案将短码映射关系预热到CDN如Cloudflare WorkersWorker脚本直接返回302。优势QPS提升至50万后端压力归零劣势缓存更新延迟CDN TTL 1分钟无法实时生效notes批注“适合静态短链不适合营销活动中的实时跳转”。V3混合模式当前生产方案热点短链访问量1000次/小时自动预热到CDNWorker返回302冷门短链仍走后端但后端加本地缓存CaffeineTTL 10秒实时更新当管理员修改长URL时主动调用CDN Purge API清除缓存。效果综合QPS 20万P99延迟50msCDN缓存命中率82%。4.4 数据一致性如何让“生成短链”和“写DB”永不丢数据这是分布式系统最棘手的问题。notes里采用“可靠消息本地事务表”方案生成短码后不直接写DB而是发一条Kafka消息topic: shortener_createShortener Service启动一个消费者监听此topic消费者开启本地事务BEGIN; INSERT INTO url_mapping (short_code, long_url, created_at) VALUES (?, ?, ?); INSERT INTO tx_log (msg_id, status) VALUES (?, PROCESSED); -- 事务日志表 COMMIT;Kafka消费位点提交仅在事务成功后。为什么不用RocketMQ事务消息因为Kafka的Exactly-Once语义在0.11版本已成熟且我们已有Kafka生态。关键点在于tx_log表它和业务表在同一DB保证原子性。如果消费者崩溃重启后会重新消费但tx_log表会阻止重复插入唯一索引。notes里还记录了一个坑Kafka消息体不能太大我们把long_url做了MD5哈希存实际URL存OSS消息里只传哈希值和OSS地址。5. 常见问题与排查技巧实录那些没人告诉你的“脏活累活”5.1 面试官最爱问的“如果……怎么办”——真实故障场景还原Q如果Redis集群挂了短链接服务还能用吗A能但降级为“只读模式”。notes里早有预案短码生成切到本地内存Counter容量有限但撑2小时没问题跳转查询查不到缓存时直接返回404不查DB避免DB被打垮监控告警Redis不可用时自动触发SOP通知值班工程师并在API Gateway返回自定义HeaderX-Service-Mode: degraded。实操心得降级方案必须提前演练。我们每月做一次“Redis故障注入”用Chaos Mesh随机kill一个Redis Pod验证降级是否生效。第一次演练发现本地Counter在K8s滚动更新时丢失后来改用StatefulSet PVC持久化。Q如何应对恶意短链攻击如生成10亿个短链占满存储A三重防护入口限流API Gateway对POST /shorten接口按IPApp-Key限流100次/分钟内容审核调用第三方API如Google Safe Browsing检查long_url是否含恶意域名命中则拒绝存储隔离恶意用户生成的短链存入独立DB分片不影响正常用户。notes里附了拦截日志截图某IP在30秒内尝试生成2000个短链全部被限流拦截且其请求的long_url域名被Safe Browsing标记为钓鱼网站。Q如何保证短链统计的准确性A放弃“绝对准确”追求“业务可用”。notes里明确实时统计Flink用于运营大屏允许1%误差离线统计Spark每日跑MR任务修正实时数据用于财务结算关键指标如付费转化在跳转前埋点用前端JS上报绕过CDN缓存。注意不要试图用Redis HyperLogLog做UV统计它误差率0.8%对千万级UV意味着8万误差。我们用BitMapHLL混合方案UV误差0.1%。5.2 工具链避坑指南那些让你加班到凌晨的“小细节”压测工具选型JMeter适合HTTP协议但分布式压测配置复杂我们弃用wrk轻量QPS高但不支持复杂场景如带Cookie登录态LocustPython最终选择。notes里写了定制化脚本class ShortenerUser(HttpUser): task def shorten(self): # 模拟真实用户行为先登录再生成短链 self.client.post(/login, json{user: test, pwd: 123}) self.client.post(/shorten, json{url: https://example.com/})关键参数--users 1000 --spawn-rate 100每秒启动100用户避免瞬时冲击。Redis监控redis-cli info只能看概览notes里推荐redis-exporter Prometheus重点关注redis_memory_used_bytes内存使用率 80%预警redis_connected_clients连接数突增可能是客户端泄漏redis_expired_keys_total每秒过期Key数突增说明缓存设计有问题。MySQL慢查询定位开启slow_query_log但notes里强调必须设置long_query_time0.1默认1秒否则漏掉大量0.5秒的“伪慢查询”用pt-query-digest分析日志重点关注Rows_examined字段超过1000行就要优化索引。5.3 面试表达技巧如何把“我做过”变成“我懂为什么”notes里专门有一节《面试话术模板》不是教你背答案而是训练结构化表达当被问“如何设计XX”不要说“我用Redis缓存”要说“首先定义核心指标短链接生成QPS峰值5万P99延迟100ms。基于此我判断瓶颈在存储层写入所以优先优化写路径。方案一用Redis做写缓冲但Redis持久化可能丢数据方案二用Kafka解耦但增加延迟。最终选择方案二因为业务允许秒级延迟且Kafka的ACK机制能保证不丢数据。具体实现是……”当被问“为什么选A不选B”不要说“B不好”要说“B方案在单机场景下更简单但我们的部署是K8s集群B依赖本地文件存储无法跨Pod共享。而A方案如Consul是分布式KV天然支持集群虽然运维成本高10%但避免了架构腐化风险。”当被问“如果出问题怎么排查”不要说“看日志”要说“我会按‘网络→服务→存储’三层排查先用curl -v测API网关连通性再查Shortener Service的Prometheus指标看HTTP 5xx错误率和GC时间最后查Redis的INFO stats看rejected_connections是否突增。上周线上问题就是通过这三步15分钟定位到是Redis连接池耗尽。”我在实际带人过程中发现真正拉开差距的从来不是技术深度而是表达深度。能把一个技术选择背后的trade-off讲清楚比堆砌十个技术名词更有说服力。这本notes就是我用来训练这种表达能力的沙盘。它不承诺让你“秒过面试”但它保证当你合上它时你不再害怕那个问题“为什么”
返回列表