Module 4: 缓存、CDN与代理
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
来源:Acing the System Design Interview + DDIA散落的缓存内容
缓存是系统设计中”投入产出比最高”的优化手段。一层薄薄的缓存可以让系统吞吐量提升10-100倍,响应延迟降低几个数量级。CDN、负载均衡和反向代理则是将系统从”单机能跑”推向”生产可用”的关键基础设施。
第一部分:缓存
Section titled “第一部分:缓存”4.1 缓存基本原理
Section titled “4.1 缓存基本原理”定义:缓存是将频繁访问的数据存储在更快的介质中(通常是内存),以减少对慢速存储(如数据库、磁盘)的访问。缓存的核心思想基于”局部性原理”:大多数系统中,少数热点数据占据了大部分访问量(80/20法则——20%的数据承担80%的请求)。通过将这20%的热数据放在内存中,可以用很小的成本获得巨大的性能提升。
为什么重要:内存读取速度约100纳秒,SSD读取约100微秒,机械硬盘约10毫秒,数据库查询(含索引)通常1-10毫秒。缓存命中时,响应时间从毫秒级降到微秒级,这不仅改善用户体验,还直接降低了数据库负载——数据库不再需要处理那80%的重复查询,可以把有限的资源用在真正需要计算的请求上。
案例:URL Shortener系统中,虽然总共有数亿条短链接映射,但绝大多数访问集中在最近创建的、被社交媒体传播的热门短链接上。将这些热门短链接(shortCode → originalURL)缓存在Redis中,命中率可以达到90%以上。一次Redis查询约0.5ms,而一次数据库查询可能需要5-10ms。缓存让系统能以10倍小的数据库规模支撑同样的流量。
先想一想 🤔 如果缓存命中率是95%,系统每秒10万请求,缓存查询耗时0.5ms,数据库查询耗时10ms。平均响应时间是多少?如果没有缓存呢?
点击查看解析
有缓存:95%的请求走缓存(0.5ms) + 5%的请求走数据库(10ms) = 0.95 × 0.5 + 0.05 × 10 = 0.475 + 0.5 = 0.975ms。没有缓存:所有请求走数据库 = 10ms。缓存让平均响应时间从10ms降到约1ms,提升10倍。更重要的是数据库压力:有缓存时数据库只需处理5000 QPS(10万 × 5%),没有缓存要处理10万 QPS——20倍的差距,可能直接把数据库打挂。
4.2 缓存策略
Section titled “4.2 缓存策略”定义:缓存策略定义了缓存与数据库之间的读写协作模式。四种核心策略:(1) Cache-Aside(旁路缓存)——应用先查缓存,未命中则查数据库并回填缓存。最常用,应用控制缓存逻辑。(2) Read-Through——缓存层自动从数据库加载未命中的数据,对应用透明。(3) Write-Through——写入时同时写缓存和数据库,保证一致性但写入延迟增加。(4) Write-Behind(Write-Back)——写入时只写缓存,异步批量刷到数据库。写入极快但有数据丢失风险。
为什么重要:不同的业务场景对一致性、延迟和可靠性有不同要求。选错缓存策略可能导致数据不一致(用户看到过期数据)、写入丢失(缓存挂了数据还没刷到数据库)、或者性能提升不明显(策略与访问模式不匹配)。
案例:News Feed的信息流缓存适合使用Write-Behind策略。用户的信息流数据是”预计算”的——当关注的人发了新帖子,系统将帖子ID写入该用户的信息流缓存。如果用Write-Through(每次写入都同步写数据库),在大V发帖时需要同步写入数百万粉丝的信息流,延迟不可接受。Write-Behind让写入先落到缓存(Redis),再异步批量持久化到数据库。即使Redis偶尔丢失一些信息流数据,用户刷新时可以从关注列表重新拉取,不会造成严重后果。
先想一想 🤔 Hotel Reservation的房间库存数据应该用哪种缓存策略?为什么不能用Write-Behind?
点击查看解析
应该用Write-Through或Cache-Aside。库存数据是强一致性场景——如果用Write-Behind,库存变更只写入缓存就返回成功,此时缓存挂了,扣减操作丢失,导致超卖。更安全的做法是Cache-Aside:写入时直接写数据库(作为唯一真实来源),然后使缓存失效(而非更新缓存)。下次读取时从数据库加载最新库存到缓存。虽然写入路径多了一次缓存失效操作,但保证了数据不丢。
4.3 缓存淘汰策略
Section titled “4.3 缓存淘汰策略”定义:缓存空间有限(内存比磁盘贵得多),当缓存满了需要腾出空间时,必须决定淘汰哪些数据。常见策略:(1) LRU(Least Recently Used)——淘汰最久未被访问的数据,假设”最近用过的未来还会用”;(2) LFU(Least Frequently Used)——淘汰访问频率最低的数据,适合长期热点稳定的场景;(3) TTL(Time To Live)——数据写入时设置过期时间,到期自动删除,与LRU/LFU可组合使用;(4) Random——随机淘汰,实现最简单,在某些场景下效果出奇地好。
为什么重要:淘汰策略直接影响缓存命中率。选错策略可能导致本该缓存的热数据被淘汰,而冷数据长期占据缓存空间。比如一次全表扫描就可能把LRU缓存中所有的热数据冲掉(缓存污染),导致命中率断崖式下跌。
案例:Gaming Leaderboard系统中,Top 100排行榜是最频繁被查看的数据。可以将Top 100列表缓存在Redis的Sorted Set中,设置TTL为30秒。每30秒缓存过期后,从数据库重新计算一次最新排名并刷入缓存。这样在30秒内所有排行榜查询都走缓存(极快),而排名数据最多延迟30秒更新——对于游戏排行榜来说,30秒的延迟完全可以接受,但节省了巨大的数据库查询开销。
先想一想 🤔 一个电商大促活动,0点开始瞬间涌入大量请求。此时LRU缓存里装满了大促商品数据。大促结束后,这些数据还需要吗?LRU会怎么处理?
点击查看解析
大促结束后,这些商品的访问量骤降,但由于LRU的特性,它们在大促期间被频繁访问,所以在缓存中"排名很靠前"。大促后正常的热点商品开始访问,LRU会逐渐将大促商品推到淘汰边缘——但这个过程可能比较慢。更好的做法是:给大促数据设置TTL(如1小时),大促结束后自动过期。或者使用LFU,因为大促结束后这些商品的频率会迅速降低而被淘汰。实际中常用LRU + TTL组合,兼顾两者优势。
4.4 缓存穿透/雪崩/击穿
Section titled “4.4 缓存穿透/雪崩/击穿”定义:三种经典的缓存异常场景:(1) 缓存穿透——查询一个缓存和数据库中都不存在的key,每次都穿透缓存打到数据库。如果被恶意利用(大量不存在的key),可以把数据库打垮。(2) 缓存雪崩——大量缓存key同时过期,瞬间所有请求涌向数据库。(3) 缓存击穿——某个超级热点key过期的瞬间,大量并发请求同时发现缓存未命中,全部涌向数据库查询同一个key。
为什么重要:这三种问题都能导致数据库在短时间内被高并发请求压垮。缓存本应保护数据库,但在这三种场景下缓存”失效”了。理解并防范这些问题,是设计健壮缓存系统的关键。
案例:URL Shortener中,如果有人恶意请求大量不存在的shortCode(如随机字符串”xyz999”、“abc111”等),这些key在缓存中不存在(因为从未被创建过),在数据库中也不存在。每次请求都穿透缓存查数据库,数据库白白消耗资源。解决方案:(1) 布隆过滤器——用极小的内存快速判断shortCode是否”可能存在”,不存在的直接拒绝;(2) 缓存空值——第一次查询发现不存在后,在缓存中存一个空值(TTL较短如30秒),后续请求直接返回不存在;(3) 请求校验——在入口层校验shortCode格式,过滤明显非法的请求。
先想一想 🤔 如何防范缓存雪崩?如果你设置所有缓存的TTL都是1小时,凌晨0点全部过期,会怎样?
点击查看解析
所有缓存0点过期意味着0点瞬间所有请求涌向数据库——典型的雪崩。防范方法:(1) **TTL加随机偏移**——不要所有key都设置1小时,而是在1小时基础上加一个随机值(如50-70分钟),让过期时间分散开;(2) **提前刷新**——在TTL过期前的一段时间内(如最后5分钟),后台异步刷新缓存,保证key永不过期;(3) **多级缓存**——本地缓存(L1) + Redis(L2),即使Redis缓存雪崩,L1缓存还能扛一阵;(4) **限流降级**——数据库前加限流器,超过阈值的请求直接返回降级数据或排队等待。
第二部分:CDN
Section titled “第二部分:CDN”4.5 CDN原理
Section titled “4.5 CDN原理”定义:CDN(Content Delivery Network,内容分发网络)是由分布在全球各地的边缘节点(Edge Server)组成的网络。CDN将静态资源(图片、视频、CSS、JS等)缓存到离用户最近的边缘节点,用户访问时从最近的节点获取内容,而不是从远在千里之外的源服务器(Origin Server)拉取。CDN的核心价值是减少物理距离带来的网络延迟。
为什么重要:光速是有限的——光在光纤中的传播速度约20万公里/秒,从中国到美国的往返延迟约200ms。这200ms是物理定律决定的,无论你的服务器多快都无法突破。CDN通过在用户附近部署节点,把”中国→美国”变成”中国→中国CDN节点”,延迟从200ms降到10ms以下。对于YouTube这样的视频网站,没有CDN意味着全球80%的用户体验极差。
案例:YouTube每天有超过10亿小时的视频观看量。如果所有视频都从美国的数据中心分发,带宽成本和延迟都不可接受。YouTube通过Google的CDN网络(Google Global Cache),将热门视频缓存到全球数千个边缘节点。一个北京用户看视频时,数据从北京或上海的CDN节点加载,延迟极低。Google Maps的地图瓦片(Tile)也是类似原理——全球地图数据量巨大,但用户在某个时刻只看特定区域。CDN将该区域的瓦片缓存到就近节点,实现流畅的拖拽和缩放体验。
先想一想 🤔 CDN缓存的是”静态资源”,那API接口返回的JSON数据(如商品价格、库存)能用CDN缓存吗?
点击查看解析
可以,但需要谨慎。CDN传统上缓存静态资源,但现代CDN(如Cloudflare Workers、AWS CloudFront)也支持缓存API响应。关键是设置合理的缓存策略:(1) 不变的数据(如商品描述)可以长时间缓存;(2) 变化频繁的数据(如库存)要设置极短的TTL(如5秒)或不缓存;(3) 个性化数据(如用户购物车)绝对不能CDN缓存——否则A用户会看到B用户的购物车。一般通过HTTP头(Cache-Control)来控制CDN的缓存行为。
4.6 CDN推送 vs 拉取
Section titled “4.6 CDN推送 vs 拉取”定义:CDN有两种内容分发模式:(1) 推送(Push)——源服务器主动将内容推送到CDN边缘节点。适合内容数量有限、可预知哪些内容会被访问的场景。内容提前就位,首次访问零延迟。(2) 拉取(Pull)——用户首次请求时,CDN边缘节点发现本地没有,回源(向源服务器请求)获取并缓存。适合长尾内容,只有被请求到才缓存,节省存储空间。
为什么重要:推送和拉取各有成本。推送需要预判哪些内容会被访问,推送太多浪费CDN存储和带宽(CDN节点存了一堆没人看的内容),推送太少又等于没用。拉取的问题是首次访问有延迟(需要回源),如果回源频率太高(命中率低),CDN反而增加了一跳延迟。
案例:YouTube可以用混合策略。热门视频(播放量前1%)使用推送——当一个视频的播放量在短时间内飙升(如被社交媒体引爆),YouTube的算法检测到后主动将该视频推送到全球CDN节点,确保任何地区的用户点开都能秒加载。长尾视频(那些播放量只有几百的小众内容)使用拉取——只在被请求时才缓存到对应区域的CDN节点,如果长时间没人看就从CDN中淘汰,节省宝贵的CDN存储空间。
先想一想 🤔 一个新闻网站的突发新闻文章应该用推送还是拉取?如果是一周前的旧新闻呢?
点击查看解析
突发新闻应该用推送。突发新闻的特点是短时间内大量用户涌入,如果用拉取模式,全球第一批用户都要回源,可能把源服务器打垮。而且突发新闻是确定性的热点——编辑发布时就知道会是热点,可以直接推送到所有CDN节点。旧新闻应该用拉取。一周前的新闻访问量极低,推送到全球CDN节点纯属浪费。偶尔有人点击时,就近CDN节点回源拉取即可。这也是为什么很多CDN会为不同类型的内容配置不同的策略。
第三部分:负载均衡与反向代理
Section titled “第三部分:负载均衡与反向代理”4.7 负载均衡
Section titled “4.7 负载均衡”定义:负载均衡(Load Balancing)是将客户端请求分配到多个后端服务器的技术,目的是让每个服务器承担合理的负载,避免某些服务器过载而另一些空闲。常见算法:(1) 轮询(Round Robin)——请求依次分配给每个服务器,最简单。(2) 加权轮询(Weighted Round Robin)——性能强的服务器分配更多请求。(3) 最少连接(Least Connections)——将请求发给当前连接数最少的服务器。(4) 一致性哈希——相同的key(如用户ID)总是路由到同一个服务器,利于缓存命中。(5) IP哈希——按客户端IP哈希,保证同一用户总是访问同一服务器。
为什么重要:负载均衡是水平扩展的前提。如果你有10台应用服务器但没有负载均衡器,用户只能访问其中一台——其他9台形同虚设。负载均衡器是”一台对外、多台对内”的枢纽,让系统可以通过简单地添加机器来提升处理能力。它还负责健康检查:如果某台服务器挂了,自动将流量切到其他健康的服务器,实现高可用。
案例:所有11个系统案例在生产环境中都需要负载均衡。以Chat System为例:WebSocket连接是长连接,如果用简单轮询,可能导致某些服务器的连接数远多于其他(因为连接不会像HTTP请求一样快速结束)。这时”最少连接”算法更合适——新用户连接到当前活跃连接数最少的服务器。而对于URL Shortener,由于是短连接HTTP请求,简单轮询就足够了。
先想一想 🤔 如果负载均衡器本身挂了怎么办?它不是成了单点故障吗?
点击查看解析
确实,单个负载均衡器是单点故障。生产环境通常使用两种方式解决:(1) **主备模式**——部署两个负载均衡器,主的处理请求,备的实时监控。主的挂了,备的立即接管(通过VRRP等协议,切换通常在秒级)。(2) **DNS轮询**——DNS返回多个负载均衡器的IP地址,客户端随机选一个。即使一个挂了,下次DNS解析会选到另一个。实际中两者结合使用,比如Cloudflare这样的CDN/LB服务本身就是全球分布的,几乎不会单点故障。
4.8 反向代理
Section titled “4.8 反向代理”定义:反向代理(Reverse Proxy)是部署在后端服务器前面、代替后端接收客户端请求的服务器。客户端以为自己在和反向代理通信,但实际上反向代理将请求转发给了后端服务器。反向代理可以执行多种功能:SSL/TLS终止(解密HTTPS,后端只处理HTTP)、响应压缩(gzip/brotli)、静态资源缓存、请求限流、安全防护(隐藏后端IP、防DDoS)、请求路由。
为什么重要:反向代理将很多”横切关注点”从应用代码中剥离出来。没有反向代理,每个后端服务都要自己处理SSL证书、压缩、限流、CORS等——代码重复且容易出错。有了反向代理(如Nginx、Caddy、Envoy),这些事情统一在入口处理一次就够了。后端开发者可以专注于业务逻辑。
案例:所有生产级系统都使用反向代理,这是基础设施标配。以一个典型的部署架构为例:用户请求 → Cloudflare(CDN + DDoS防护)→ Nginx(反向代理:SSL终止 + 路由 + 限流)→ 应用服务器集群。在这个架构中,Nginx做了三件事:(1) 将/api/*的请求转发给后端API服务器,将/*的请求转发给前端静态文件服务器;(2) 对API请求做限流(如每个IP每分钟最多100次请求);(3) 终止SSL连接,后端服务器之间用HTTP明文通信(内网安全)。
先想一想 🤔 反向代理和负载均衡器有什么区别?它们是同一个东西吗?
点击查看解析
严格来说,它们是不同的概念,但在实践中经常合二为一。反向代理的核心职能是"代理"——代替后端接收请求并转发。负载均衡的核心职能是"分发"——将请求分配到多个后端。当后端只有一台服务器时,反向代理有意义(做SSL终止、缓存等)但负载均衡没意义。当后端有多台服务器时,反向代理通常也承担负载均衡的功能。Nginx、HAProxy、Envoy等工具同时具备两种能力。可以说负载均衡是反向代理的一个子功能。
4.9 一致性哈希
Section titled “4.9 一致性哈希”定义:一致性哈希(Consistent Hashing)是一种特殊的哈希算法,将key和节点都映射到一个哈希环(0到2^32-1的环形空间)上。每个key顺时针找到最近的节点,就是它应该被存储/路由到的位置。当增加或删除一个节点时,只有该节点”附近”的key需要重新映射,大部分key保持不变。虚拟节点(Virtual Nodes)技术进一步优化:每个物理节点在环上放置多个虚拟节点,保证负载更均匀。
为什么重要:在分布式缓存系统中,当缓存节点增减时(扩容、缩容、故障),需要决定哪些key应该在哪个节点上。如果用简单的hash(key) mod N,增减一个节点会导致几乎所有key重新分配(如Module 3中3.11讨论的),造成缓存几乎全部失效——相当于缓存雪崩。一致性哈希保证增减节点时只影响1/N的key,缓存失效可控。
案例:URL Shortener使用分布式Redis集群缓存短链接映射。假设有4个Redis节点,使用一致性哈希。当shortCode “abc123” 的哈希值为H,顺时针最近的节点是Redis-2,那么”abc123”的缓存数据存在Redis-2上。现在要扩容加一个Redis-5,Redis-5在哈希环上位于Redis-2和Redis-3之间。只有原来映射到Redis-3的一部分key(现在更靠近Redis-5)需要迁移到Redis-5,其他3个节点的数据完全不受影响。相比hash mod 5(80%的key要迁移),一致性哈希只迁移约20%的key。
先想一想 🤔 如果不使用虚拟节点,只有3个物理节点在哈希环上各占一个点,负载会均匀吗?
点击查看解析
很可能不均匀。3个点将哈希环分成3段,每段的长度取决于哈希值的分布——可能某段占60%、某段占30%、某段只有10%。这意味着某个节点要处理60%的数据,另一个只处理10%,负载极度不均衡。虚拟节点的解决方案是:每个物理节点在环上放置100-200个虚拟节点。3个物理节点变成300-600个点均匀分布在环上,每段的长度趋于均匀。统计上,虚拟节点越多,负载越均匀。实际系统中通常每个物理节点配置100-200个虚拟节点。
练习一:逐层排查故障
Section titled “练习一:逐层排查故障”故障场景 🔧 YouTube突然出现大面积故障:热门视频加载需要10秒以上,冷门视频反而正常。请从CDN → 缓存 → 数据库逐层分析可能的原因和排查步骤。
点击查看解析
第一层:CDN排查
- 现象分析:热门视频慢而冷门视频正常,说明热门视频的CDN缓存可能失效了
- 可能原因:(1) CDN缓存大面积过期(所有热门视频的TTL同时到期——缓存雪崩);(2) CDN节点故障,请求全部回源;(3) CDN配置变更导致缓存策略失效
- 排查步骤:检查CDN命中率指标,正常应>95%,如果骤降到<50%说明CDN层出了问题;检查是否有近期的CDN配置变更;检查各区域CDN节点健康状态
第二层:缓存排查(CDN回源到源站后)
- 现象分析:如果CDN回源正常,问题可能在源站的Redis缓存层
- 可能原因:(1) 热门视频的元数据缓存过期(缓存击穿)——某个超热视频的缓存key过期,瞬间数万请求同时打到数据库查同一条数据;(2) Redis内存满了,正在疯狂淘汰数据
- 排查步骤:检查Redis内存使用率和淘汰统计(evicted_keys);检查Redis慢查询日志;检查缓存命中率
第三层:数据库排查
- 现象分析:CDN和缓存都失效的情况下,所有请求打到数据库
- 可能原因:(1) 数据库连接池耗尽——大量回源请求把数据库连接占满;(2) 慢查询堆积——某些查询因为缺索引或数据量增长变慢,堵塞了其他查询;(3) 数据库主从复制延迟——从库数据太旧,应用层回退到查主库,主库压力暴增
- 排查步骤:检查数据库连接数和活跃查询;检查慢查询日志;检查主从延迟
最可能的根因:CDN缓存雪崩(热门视频TTL集中过期)→ 大量回源请求 → Redis缓存击穿(热门视频key也过期)→ 请求涌向数据库 → 数据库过载 → 响应变慢。冷门视频正常是因为它们的请求量本来就小,即使不走缓存也压不垮数据库。
修复方案:(1) 紧急:对热门视频的缓存key设置更长的TTL或永不过期;(2) 短期:给CDN缓存的TTL加随机偏移,防止集中过期;(3) 长期:对热门视频的缓存key加互斥锁(mutex),防止缓存击穿。
练习二:11个系统的缓存策略
Section titled “练习二:11个系统的缓存策略”系统设计总览 📋 为11个系统案例逐一标注:需要缓存吗?缓存什么?用什么策略?
点击查看参考答案
系统 需要缓存 缓存什么 缓存策略 淘汰策略 URL Shortener 必须 shortCode → originalURL映射 Cache-Aside LRU + TTL(24h) Web Crawler 可选 已爬取URL去重集合、DNS解析结果 Write-Through(URL去重集合) TTL(DNS缓存5min) News Feed 必须 用户信息流(预计算的帖子ID列表) Write-Behind LRU + TTL(1h) Chat System 可选 最近消息、用户在线状态 Cache-Aside TTL(在线状态30s) Search Engine 必须 热门搜索词的结果页 Cache-Aside LRU + TTL(10min) YouTube 必须 视频元数据、热门视频推荐列表 Cache-Aside + CDN LRU + TTL Google Drive 适度 文件元数据(文件名、大小、权限) Cache-Aside LRU + TTL(5min) Proximity Service 必须 附近商户列表(按地理网格缓存) Cache-Aside TTL(1h),商户变更时失效 Google Maps 必须 地图瓦片(Tile)、路径规划结果 CDN(瓦片) + Cache-Aside(路径) TTL(瓦片长期,路径短期) Hotel Reservation 必须 酒店信息、搜索结果;库存数据谨慎缓存 Cache-Aside TTL(酒店信息1h,库存30s) Gaming Leaderboard 必须 Top N排行榜 Cache-Aside(Redis Sorted Set) TTL(30s) 核心规律:
- 读多写少的数据最适合缓存(URL Shortener、Search Engine)
- 一致性要求高的数据要谨慎缓存或用短TTL(Hotel Reservation库存)
- 静态资源优先用CDN而非应用层缓存(YouTube视频、Google Maps瓦片)
- 实时性要求高的数据用极短TTL或不缓存(Chat System消息)