|

Aimee

Write the Code. Change the World.

缓存架构:多级缓存怎么搭

· 分享镜

"慢了?加个 Redis"——单层缓存确实能扛很久。但流量再往上走,你会发现 Redis 也不是万能的:它也有带宽和连接上限,一个超级热点 key 能把某个 Redis 节点单独打爆,每次访问还都要走一趟网络。这时候问题就从"用不用缓存"变成了"缓存该摆在系统的哪几个位置"。

《缓存》篇讲的是缓存组件本身——为什么快、Cache-Aside 怎么用、穿透击穿雪崩怎么防、和数据库怎么保持一致。这一篇换个视角,站到架构层看:一个大流量系统怎么把缓存分层布局,从用户的浏览器一路排到数据库,每一层各挡一段流量。组件的坑还是请回《缓存》篇,这里只讲"位置"。


一、为什么单层缓存不够 —— Redis 也有它的上限

是什么。 单层缓存,就是应用前面架一个 Redis(或集群),所有读请求未命中就回填,大部分场景这一层足够好用。多级缓存则是在这条"应用 → Redis → DB"的链路上,再插入若干层缓存,让流量一层层被拦截、越往后越少。

为什么单层会到瓶颈。 Redis 快,但它不是没有天花板。单个实例的吞吐有上限(通常一台 Redis 单线程主流程,几万到十几万 QPS 量级),网卡带宽也有限;当某个 key 热到一定程度——比如一篇全网刷屏的内容——所有请求会集中砸向存这个 key 的那一个节点,这就是业界常说的**超热点 key(hotkey)**问题,加再多分片也分摊不掉,因为它就在一个分片上。

还有两笔隐性成本。 一是网络开销:应用每次读 Redis 都要走一趟 TCP 往返,单次零点几毫秒看着不痛,但乘上每秒几十万次就很可观;二是:Redis 集群越堆越大、内存越买越多,成本是线性甚至更陡地涨。

于是有了分层的思路。 既然一层挡不住,就多设几道闸:能在用户浏览器里命中的,就别回源;能在应用进程内存里命中的,就别打 Redis;能在 Redis 命中的,就别碰数据库。每一层的目标都一样——把流量尽量挡在离用户更近、成本更低的地方,别让它流到下一层。

一句话:多级缓存不是"缓存的高级版",而是把缓存从一个点,铺成一条纵深防线

二、多级缓存的层次 —— 从浏览器一路排到数据库

是什么。 一个典型的大流量读链路,缓存大致排成这样五层,流量从上往下逐层衰减:

客户端 / CDN → 网关缓存 → 本地缓存(进程内)→ 分布式缓存(Redis)→ 数据库

下面逐层看它挡的是什么流量典型场景是什么。

第一层:客户端 / CDN。 离用户最近的一层。浏览器自己的 HTTP 缓存(靠 Cache-ControlETag 这类响应头控制)能让用户根本不发请求;再往后是 CDN(内容分发网络)——把静态资源(图片、JS、CSS、视频)缓存到全国各地的边缘节点,用户就近取,请求压根到不了你的机房。CDN 主要扛静态资源和不常变的内容,这部分流量量极大,挡在这里最划算。

第二层:网关缓存。 请求进了机房,第一道是接入层 / API 网关(Nginx、APISIX 这类)。它可以对一些读多写少、且短时间内结果相同的接口做缓存(比如首页配置、热门榜单),命中就直接在网关返回,不进后端应用。这一层挡的是重复的接口请求

第三层:本地缓存(进程内)。 缓存直接放在应用进程自己的内存里(Java 里常见 Caffeine、Guava Cache)。它没有网络开销,读的就是本机内存,快到纳秒级。它的拿手好戏正是超热点 key——把那几个被疯狂访问的 key 放进每台应用机的本地缓存,流量就在本机消化掉了,根本不会去挤 Redis 的那个热点分片。代价是每台机器各存一份、容量有限、且天然有一致性问题(后面第四节讲)。

第四层:分布式缓存(Redis)。 这是大家最熟的那层,也是扛起大部分读的主力——所有应用机共享同一份缓存数据,容量大、可集群扩展。前面几层没挡住的、变化相对频繁的业务数据(商品、用户信息),绝大多数都在这里命中。Redis 这层的用法、三大坑、和 DB 的一致性,全在《缓存》篇。

第五层:数据库。 缓存的最后兜底。理想情况下,只有"缓存全都没命中"的少量请求才真正落到 DB。多级缓存做得好,DB 的 QPS 能被前面四层削掉一两个数量级——这正是分层的全部意义:让最贵、最脆弱的数据库,只接它必须接的那一小撮流量。

直觉上记一句:越往上(离用户越近)越便宜、越快、越能挡量;越往下越贵、越权威、越扛不住量。 分层就是让流量尽量死在上面。

三、业界主流方案 —— 各层用什么,读写模式怎么选

本地缓存:Caffeine / Guava Cache。 Java 生态里,本地缓存的事实标准是 Caffeine——它基于一种叫 W-TinyLFU 的淘汰算法,命中率和性能都很好,是 Spring 默认推荐的本地缓存。Guava Cache 是更早的方案,API 类似但性能和命中率不如 Caffeine,新项目一般直接上 Caffeine。其他语言也有各自的进程内缓存库,思路一致:一块带淘汰策略的本机内存

分布式缓存:Redis。 跨进程共享的那层,业界几乎是 Redis 的天下(早年也有 Memcached)。集群、持久化、丰富的数据结构,使它既能当"数据库的快副本",也能当"短命数据的主存储"——这两种角色《缓存》篇讲过。

CDN:云厂商的标准件。 CDN 基本不用自己造,直接买云厂商的(阿里云 CDN、AWS CloudFront 等),配好回源规则和缓存时间即可。它和上面两层最大的区别是:CDN 缓存的是 HTTP 响应(尤其静态资源),按地理位置就近分发

缓存读写模式:决定"谁负责读写缓存"。 这是多级缓存里很容易被忽略、但很关键的一组概念——同样是缓存,谁来填、谁来更新有几种固定套路,客观陈述如下:

模式谁读写缓存一句话适合
Cache-Aside(旁路)应用代码自己管读未命中→应用查库回填;写→更新库+删缓存最通用,绝大多数业务;《缓存》篇主讲的就是它
Read-Through(读穿透)缓存层自己管读未命中时,由缓存组件自动去加载数据源、回填把回填逻辑收进缓存库,应用只管读(Caffeine 的 loading cache 就是这思路)
Write-Through(写穿透)缓存层自己管写时同步写缓存+写数据源,两边一起落要求缓存和源强一致、且能接受写变慢
Write-Behind(回写 / 异步写)缓存层异步管先只写缓存,攒一批再异步刷回数据源写极频繁、能容忍短暂不一致和宕机丢数据的风险(如计数器)

怎么选,一句话点破: 读多写少、对一致性要求一般 → Cache-Aside / Read-Through(最常见);写也要强一致、不在乎写慢 → Write-Through;写量大到源扛不住、且数据丢一点能忍 → Write-Behind。注意 Write-Behind 用先写缓存换吞吐,代价是宕机可能丢掉还没刷回的数据,用在钱、订单这类地方要极其谨慎。

四、注意事项 —— 多级带来的新麻烦

层与层之间的一致性,是多级缓存最大的代价。 单层 Redis 时,一致性只需操心"Redis 和 DB"两边;一上多级,变成了"本地缓存、Redis、DB"三方甚至更多方对不齐。最棘手的是本地缓存:它散在每台应用机里,你更新了 DB、删了 Redis,但十几台机器各自的本地缓存还揣着旧值,且它们之间互相不知道。业界常见两种处理:一是给本地缓存设很短的 TTL(比如几秒),用"忍受几秒旧数据"换简单;二是用消息广播(如借助消息队列或 Redis 的发布订阅)通知所有应用机失效本地缓存。核心权衡:本地缓存只适合放能容忍短暂不一致的数据,强一致的东西(余额、库存)别往本地缓存放。

缓存预热,别让系统冷启动就裸奔。 服务刚上线或缓存刚被清空时,缓存是空的,海量请求会直接砸穿到 DB(本质上就是一次人造雪崩)。缓存预热就是在流量进来前,提前把热点数据加载进缓存——比如大促开始前先把爆款商品灌进 Redis 和本地缓存。多级缓存下,预热往往要逐层预热,尤其别忘了进程内的本地缓存。

超热点 key,这正是本地缓存存在的理由。 前面说过,hotkey 加分片也分摊不掉,因为它就在一个分片上。架构层的标准解法,就是把它提到本地缓存——让它在每台应用机的内存里被消化,根本不去挤 Redis。识别 hotkey 通常靠监控统计访问频次,自动把高频 key 推到本地缓存。(雪崩、击穿这类"key 失效"层面的防护,见《缓存》篇。)

本地缓存的容量与淘汰:LRU / LFU。 本机内存很小,装不下全部数据,所以本地缓存必须配淘汰策略,满了就按规则踢掉一些。两种最常见的规则:LRU(Least Recently Used,淘汰最久没被访问的)和 LFU(Least Frequently Used,淘汰访问次数最少的)。Caffeine 用的 W-TinyLFU 就是 LFU 思路的改良版,对"偶尔来一次的冷数据"抵抗力更强。关键:本地缓存的容量上限一定要设(按条数或内存),否则缓存涨到把应用进程内存撑爆,反而把服务搞挂——这个坑很容易被忽略。

和 DB 的最终一致,沿用《缓存》篇那套。 多级缓存没有改变一个底层事实:缓存只能做到最终一致。Redis 与 DB 之间"更新库 + 删缓存""延迟双删",以及对钱 / 库存这类极高一致性数据"要么不缓存、要么订阅 binlog 同步"的处理,全部沿用《缓存》篇,本篇不重复。多级只是在这之上,多叠了一层"本地缓存也要跟着失效"的问题而已。

五、AI 应用的缓存:语义缓存

调大模型又慢又贵,AI 应用特别想缓存它的回答——但有个新问题:传统缓存靠 key 精确匹配,可用户问"今天天气咋样"和"今天天气如何",意思一样、字面不同,精确匹配根本命中不了。

语义缓存(Semantic Cache) 换了匹配方式:把问题转成向量(embedding),按语义相似度找——只要意思够接近,就命中缓存里那条答案直接返回,省掉一次大模型调用(省钱又省时:大模型按 token 收费、还要等几秒,命中缓存几乎零成本、毫秒返回)。

怎么搭: 在应用和大模型之间加一层——查询先转向量,去向量库(Milvus、pgvector)找相似的历史问答,相似度超过阈值就返回缓存答案,否则才真调大模型、再把新问答存回去。本质还是 Cache-Aside,只是"命中判断"从"key 相等"换成了"向量相似"。

注意:相似度阈值是把双刃剑——太松会把"看着像、其实不一样"的问题答错,太紧又命中不了;精确性要求高的场景(法律、医疗)慎用。

六、一张表:各级缓存(挡什么 / 典型方案 / 注意)

把全文收进一张表——这就是多级缓存这条纵深防线的"布防图":

层级挡什么流量典型方案主要注意事项
客户端 / CDN静态资源、不常变内容,挡在机房外浏览器 HTTP 缓存、云 CDN(阿里云 CDN / CloudFront)缓存时间设置、刷新 / 回源规则
网关缓存读多写少、短时结果相同的接口Nginx、APISIX 网关缓存只缓存能容忍短暂旧值的接口
本地缓存(进程内)超热点 key、零网络开销读Caffeine、Guava Cache必设容量上限 + 淘汰(LRU/LFU);多机一致性靠短 TTL / 广播失效
分布式缓存(Redis)大部分业务读(主力)Redis(集群)三大坑、与 DB 一致性见《缓存》篇
数据库兜底,只接没命中的少量请求关系型 / NoSQL别让缓存全失效时被打穿(预热、限流降级)

一句话收尾:多级缓存的全部功夫,就是让流量从上到下越走越少,把最贵最脆弱的数据库,保护在最后。


名词解释

  • 多级缓存(Multi-Level Cache):在"应用 → 缓存 → DB"链路上铺多层缓存(客户端/CDN、网关、本地、分布式),让流量逐层被拦截、越往后越少。
  • 本地缓存 / 进程内缓存(Local / In-Process Cache):缓存直接放在应用进程自己的内存里,无网络开销,快但容量小、每台机器各一份、有一致性问题。
  • 分布式缓存(Distributed Cache):跨进程共享的缓存(如 Redis),所有应用机读同一份,容量大、可集群,是扛读的主力层。
  • CDN(内容分发网络):把静态资源缓存到各地边缘节点,用户就近获取,请求不进源站机房。
  • 超热点 key(Hotkey):被极高频访问的单个 key,集中压在某一个缓存分片上,加分片也分摊不掉,标准解法是提到本地缓存。
  • Cache-Aside(旁路缓存):由应用代码自己管缓存——读未命中回填、写时删缓存,最通用的模式(详见《缓存》篇)。
  • Read-Through(读穿透):读未命中时,由缓存组件自动去数据源加载并回填,应用只管读。
  • Write-Through(写穿透):写时同步写缓存和数据源,两边一起落,强一致但写变慢。
  • Write-Behind / Write-Back(回写):先只写缓存,异步攒批刷回数据源,吞吐高但宕机可能丢未刷回的数据。
  • 缓存预热(Cache Warming):在流量进来前,提前把热点数据加载进缓存,避免冷启动被打穿。
  • LRU(Least Recently Used):淘汰最久未被访问的条目。
  • LFU(Least Frequently Used):淘汰访问次数最少的条目;W-TinyLFU 是其改良版(Caffeine 采用)。
  • 网关缓存:在接入层 / API 网关对读多写少、短时结果相同的接口做缓存,命中即返回,不进后端。

本文属《研发都要懂的事》·服务端架构设计系列——架构层的缓存布局。缓存组件本身(穿透/击穿/雪崩、与数据库一致性)见《缓存》篇;本篇只回答"缓存在系统架构里摆在哪几层"。完整代码与系列在 GitHub · backend-notes

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部