2、缓存分层模型:客户端缓存、应用层缓存、分布式缓存、数据库缓存

说到缓存,很多人的第一反应就是 Redis。但说实话,一个成熟的券商交易系统,缓存绝不是单点作战。我这些年拆解过的生产事故里,有一半以上都是因为「缓存分层没做对」。

今天我们就来聊聊缓存的分层模型。我个人习惯把它分成四层:客户端缓存、应用层缓存、分布式缓存、数据库缓存。每一层都有自己的使命,也有自己的坑。

核心观点:缓存不是越底层越好,也不是越上层越好。关键是要让每一层各司其职,形成合力。

2.1 客户端缓存:离用户最近的一层

客户端缓存,说白了就是数据存在用户的浏览器或 App 里。你想想看,如果每次刷新页面都要去服务器拉数据,那服务器早被压垮了。

我在项目中遇到过这样一个场景:行情页面上的 K 线图,用户每切换一次时间周期就发一次请求。后来我们在前端加了本地缓存,把最近 10 次查询的结果存下来。效果立竿见影,后端压力降了 40%。

客户端缓存常用的手段有:

  • HTTP 缓存头:Cache-Control、ETag、Last-Modified
  • 浏览器 LocalStorage / SessionStorage:适合存储用户偏好、临时数据
  • Service Worker:适合离线场景,比如交易 APP 的首页快照

避坑指南:我曾经在某个项目中,把用户的交易密码 hash 值存在了 LocalStorage 里。后来安全审计被直接打回。记住:客户端缓存永远不要存敏感信息!

2.2 应用层缓存:进程内的「快车道」

应用层缓存,就是跑在应用进程里的缓存。比如 Java 里的 Caffeine、Guava Cache,或者 Go 里的 sync.Map。这一层的特点是:快,非常快。因为数据就在本地内存里,没有网络开销。

但快是有代价的——容量有限,而且进程重启就丢了。所以它适合存什么?

  • 热点数据:比如某只股票的实时行情,一秒内被查询上万次
  • 配置信息:交易规则、费率表,这些几乎不变
  • 临时计算结果:比如风控校验的中间结果

我记得有一次线上事故,就是因为应用层缓存没设过期时间。一个用户的持仓信息被缓存了 30 分钟,结果他平仓了,系统还显示他有持仓。嗯,这里要注意:应用层缓存一定要设置合理的 TTL

// 以 Caffeine 为例,设置最大容量和过期策略
Cache<String, StockPrice> stockCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.SECONDS)
    .recordStats()
    .build();

2.3 分布式缓存:系统的「共享内存」

当应用层缓存不够用了,或者多个服务需要共享数据时,就该分布式缓存登场了。Redis、Memcached 是这里的常客。

分布式缓存解决了什么问题?

  • 数据共享:多个服务实例可以访问同一份缓存数据
  • 容量扩展:通过集群模式,可以存下几十 GB 甚至 TB 级的数据
  • 高可用:主从切换、哨兵模式,保证缓存不丢

但分布式缓存也有自己的坑。我曾经在项目中遇到过「缓存雪崩」——大量缓存同时过期,请求直接打到数据库,数据库瞬间被打爆。后来我们做了两件事:

  1. 给过期时间加一个随机偏移量,避免集体过期
  2. 对热点数据做「永不过期 + 后台异步刷新」

警告:分布式缓存不是万能的。网络延迟、序列化开销、连接池耗尽,这些都是真实存在的坑。我见过一个团队把 Redis 当数据库用,存了上亿条记录,结果每次查询都要几十毫秒——那还不如直接用 MySQL。

2.4 数据库缓存:最后的防线

数据库缓存,指的是数据库自带的缓存机制。比如 MySQL 的 Buffer Pool、InnoDB 的 Change Buffer,还有查询缓存(虽然 MySQL 8.0 已经废弃了)。

这一层往往被很多人忽略。你想想看,如果应用层和分布式缓存都 miss 了,最后扛住压力的就是数据库缓存。Buffer Pool 的大小设置是否合理,直接决定了数据库的读写性能。

我个人的经验是:

  • Buffer Pool 大小:建议设置为物理内存的 60%-80%
  • 预热策略:系统启动后,手动加载热点表到 Buffer Pool
  • 监控指标:关注 Buffer Pool 命中率,低于 95% 就要警惕了
-- 查看 MySQL Buffer Pool 命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW STATUS LIKE 'Innodb_buffer_pool_reads';
-- 命中率 = (read_requests - reads) / read_requests * 100%

2.5 四层模型如何协同工作?

说了这么多,四层缓存到底怎么配合?我画了一张图,你看完就明白了。

缓存分层模型架构图 客户端缓存 浏览器缓存 / App 本地缓存 / Service Worker 应用层缓存 Caffeine / Guava Cache / 进程内 Map 分布式缓存 Redis Cluster / Memcached / 缓存中间件 数据库缓存 MySQL Buffer Pool / InnoDB Change Buffer

从这张图可以看得很清楚:越往上,速度越快但容量越小;越往下,容量越大但速度越慢。一个合理的缓存策略,应该是让请求尽量在上层命中,实在不行再往下走。

我举个例子:用户查询某只股票的实时行情。

  1. 先看客户端缓存有没有(比如 5 秒内的数据)
  2. 没有的话,去应用层缓存找(进程内,微秒级)
  3. 还没有,去 Redis 查(毫秒级)
  4. Redis 也没有,最后去数据库 Buffer Pool 碰运气
  5. 如果 Buffer Pool 也 miss,那就只能走磁盘 IO 了

我的经验:在券商系统里,90% 以上的查询应该在前两层命中。如果分布式缓存的命中率低于 70%,说明你的缓存策略有问题——要么过期时间太短,要么缓存的数据不对。

2.6 分层缓存的核心原则

最后,我总结几条原则,你在设计缓存分层时可以拿来参考:

原则 说明 我的踩坑记录
就近原则 数据尽量放在离用户最近的地方 曾经把所有缓存都放 Redis,忽略了客户端缓存,导致带宽浪费
分层隔离 每一层独立管理,不要跨层依赖 应用层缓存依赖 Redis 做数据源,Redis 挂了应用层也崩了
逐级降级 上层 miss 时,下层要能兜底 没做降级策略,缓存雪崩时数据库直接被打死
监控全覆盖 每层都要有命中率、延迟、容量监控 漏了应用层缓存的监控,内存泄漏了三天才发现

嗯,缓存分层这个话题,说起来简单,做起来全是细节。我见过太多团队一上来就上 Redis,结果应用层缓存和客户端缓存完全没用上。说白了,缓存不是越底层越高级,而是越合适越好

下一层我们聊聊缓存穿透、缓存击穿、缓存雪崩——这三个「缓存杀手」在券商系统里有多可怕,以及怎么防。到时候我会分享几个我亲手处理过的线上事故,保证让你印象深刻。


专注资料整理