刚接手一个交易类系统的时候,印象最深的一次故障,是数据库连接数被打满,整个服务集体超时。上游接口响应从几十毫秒一路涨到几秒,最终牵一发动全身。事后复盘的时候发现,其实根子上的问题很简单:缓存部署得太单薄了,所有请求在缓存未命中之后全都穿透到数据库。那套系统的缓存只有一个Redis集群,本地进程内缓存完全没有用起来,预热更是不存在的。那次事故之后,我花了很长时间重新梳理整个系统的缓存架构,把多级缓存和缓存预热系统地落到了生产环境。这篇文章就把这套设计思路、落地过程、以及我踩过的坑,尽量完整地写出来,希望对正在做架构设计或者性能优化的你有点参考价值。
1. 多级缓存的底层逻辑:一次请求的完整链路
1.1 从数据库扛不住说起
很多系统最开始的形态很简单,服务直接查数据库,逻辑清晰,开发也快。可一旦流量上来,数据库的压力会先于所有指标暴露出来。连接池数量有限,慢查询会占住连接,数据库CPU飙升,最终请求排队甚至超时。引入缓存看起来是标准答案,但把所有缓存都放在一个Redis集群里,相当于把所有赌注压在同一个篮子里。
单个Redis的好处是集中管理、容量大、过期策略统一,坏处也明显:每次缓存查询都是一次网络RPC,响应时间在0.1到0.5毫秒级别,可一旦Redis出现网络抖动或者热key集中,服务立刻跟着遭殃。数据库本身的访问耗时普遍在几毫秒到几十毫秒,这一层加一层的耗时叠加,最终用户感受到的接口时延很难看。
1.2 多级缓存的本质是“远近搭配,层层拦截”
业界常说的多级缓存,并不是把同一个数据复制到多个地方就完事,而是让不同距离、不同速度、不同容量的存储各司其职。一个典型的模型是三层:浏览器或客户端侧的本地缓存、应用进程内的本地缓存、以Redis为代表的分布式缓存。再往下才是数据库本身。
进程内缓存离代码最近,查询耗时可以忽略不计,通常都在微秒级。但它的容量受限于JVM堆或者进程内存,而且每个节点各自维护一份,更新时很容易出现节点间的不一致。Redis缓存离服务有网络开销,但容量大,可以跨节点共享。浏览器缓存离用户最近,命中后连请求都不会发到后端。
这三层从性能上呈数量级递减,从容量和一致性维护成本上看又呈递增趋势。多级缓存的真正设计目标,是用最廉价的机制挡住大部分流量,让昂贵的后端只处理不得不处理的那部分请求。
1.3 各级缓存的职责划分
我给系统中的各级缓存做了明确的职责划分,这在后面写代码、设计更新策略的时候会非常有用。
浏览器侧主要用来缓存静态资源和极少变化的公共数据,比如图片、静态页面片段。这里用HTTP缓存头控制即可,业务代码不用过多介入。
进程内缓存主要放两类内容:一类是变化频率极低、几乎是静态配置的数据,比如商品类目、城市列表;另一类是访问量极高、同时可以容忍短暂不一致的数据,比如热点新闻、排行榜。进程内缓存的特点是快,但容量有限,适合温热点数据,不适合全量。
Redis这一层作为主缓存层,承担了大部分后端逻辑的数据承载,同时因为它是集中式的,数据更新后可以快速同步到所有节点。那些需要强一致、又没法容忍本地节点缓存漂移的数据,应该优先放到这一层。
数据库永远是兜底,但兜底不意味着随意压榨。我后面会讲到预热和缓存空值标记,目的就是让数据库暴露在真实流量下的概率尽可能低。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多级缓存的整体设计思路与核心选型
2.1 按数据特征决定缓存层级
在动手搭建之前,有一件事必须先做:把业务数据按特征分类。我一般会分成三类。
第一类是静态配置类数据,变化周期以天甚至以周计。这类数据适合全部放进程内缓存,比如启动时加载,之后定时刷新。即使不做主动刷新,靠过期时间也能撑住。
第二类是热点类数据,总量很大,但某一小部分key的访问频率极高。这类数据必须多级缓存配合,热key用进程内缓存拦截,防止Redis被单点尖峰打爆;Redis中存放全量热点子集,回源时只查有限范围。
第三类是强一致类数据,比如支付订单状态、库存扣减。这类数据原则上不推荐用进程内缓存,因为本地缓存天然存在节点间不一致的问题,不同节点读到不同数据会引发业务逻辑错误。合理做法是把这类数据放在Redis,并配合数据库事务与版本控制。
