这个问题,本质上是缓存选型问题,我告诉你怎么选,就三条。
  • 流量小的,纯数据库够了
  • 流量中等的,Redis做中央缓存够了
  • C端大流量的查询场景,本地缓存结合中央缓存是标配

就是要看流量等级来选择。

缓存选型策略

按这个框架,分别说每一层该怎么做、要注意什么。

流量小:数据库够用

日活几千、QPS几十几百甚至几千的系统,数据库加上合理的索引就能撑住。这个阶段加缓存是在增加复杂度,多了缓存一致性的问题要处理,多了一个组件要运维,收益很小。

不少团队在项目初期就急着引入Redis,结果花了不少精力处理缓存和数据库之间的数据一致性,得不偿失。能用数据库撑住的阶段,不要急着加缓存。

流量中等:Redis做中央缓存

日活几万到几十万、QPS几万的系统,数据库开始有读压力,这时候引入Redis做中央缓存是最常见的方案。Redis承担读压力,数据库承担写操作和复杂查询,分工明确。

这个阶段用Redis而不是本地缓存,原因有几个。

线上服务部署多个实例,本地缓存是进程内的,每个实例各自维护一份副本。一个实例更新了数据,其他实例的缓存还是旧的。Redis是集中式的,所有实例读写同一份数据,不存在多副本不一致的问题。

Redis的缓存管理能力也完善得多。每个key可以单独设置过期时间,支持多种淘汰策略,自带INFO、SLOWLOG等运维命令,配合Prometheus和Grafana能搭建完整的监控体系。线上出了缓存穿透、击穿、内存不足的问题,有足够的工具去定位。

HashMap和Guava Cache在这些维度上差得远。HashMap没有过期、没有淘汰、没有容量限制,数据只增不减。Guava Cache和Caffeine好一些,有基于容量和时间的淘汰策略,但管理粒度有限,所有key共用一套规则。运维监控也基本是空白,缓存了多少key、内存占了多少、命中率多少,全得自己埋点。

C端大流量:本地缓存 + 中央缓存

C端产品,日活百万级,查询QPS上百万的。这个量级下,每次请求都走网络去Redis取数据,网络往返的延迟和带宽开销会成为瓶颈。

这时候需要在应用进程内加一层本地缓存。请求先查本地,命中直接返回,不走网络;没命中再查Redis;Redis也没有才查数据库。

你需要记住一句话:

在瞬时大流量下,每次查询请求,都尽量不要去数据库读。

那关键问题来了:本地缓存用哪种框架?

本地缓存不能用HashMap

不少人的第一反应是用HashMap或者Guava Cache。能跑,但在生产环境下隐患很大。

HashMap和Guava Cache的数据都存在JVM堆里。业务数据增长、缓存key设计不合理、淘汰策略没配好,堆内存占用可以在短时间内飙升。堆内存一紧张,JVM就会触发Full GC。Full GC期间所有业务线程暂停,接口响应全部超时。C端高并发场景下,一次Full GC就能引发连锁故障:上游调用超时,重试放大流量,系统雪崩。

HashMap还没有任何缓存管理能力。不支持过期、不支持淘汰、不支持容量限制。数据只增不减,迟早把内存撑满。

JVM一重启,堆内缓存全部丢失。服务重启后需要重新加热缓存,这段时间所有请求直接打到数据库。流量大的时候,冷启动可以直接把数据库打挂。

堆内存放业务对象已经够紧张了,别再往里塞大量缓存数据。

当然,你可以使用「堆内的本地缓存」去存一些数据量小,又很少变化的数据。这个没问题的。

堆外缓存方案

我之前做过一个C端项目,要扛住百万级的瞬时流量。当时用了本地缓存,但不是HashMap或Guava,用的是堆外缓存。数据放在JVM堆外的直接内存里,不参与GC,不会因为缓存数据量大而影响业务线程。

注意,需要在本地缓存里存很多数据的,堆外缓存算是最好的选择了。

如果业务场景需要本地缓存,又担心GC影响,堆外缓存是更稳妥的选择。目前主流的几个方案:

OHC(Off-Heap Cache):Apache Cassandra团队开发的堆外缓存库,纯Java实现。提供类似ConcurrentMap的API,支持LRU淘汰和过期策略。Cassandra内部用它来做行缓存。适合需要高吞吐的场景。

Ehcache 3:支持分层存储,可以配置堆内、堆外、磁盘三级缓存。堆外层基于java.nio.ByteBuffer实现。配置灵活,适合对缓存分层有要求的项目。

我当年没有选择Ehchche 3的原因,是开源协议不满足,具体我不太记得了。因此选择了一个当年还比较冷门的OHC。

如果对GC不太敏感、数据量也可控,Caffeine是堆内本地缓存的比较好的选择。

对比速查表

维度 HashMap Guava/Caffeine 堆外缓存 Redis
内存区域 JVM堆 JVM堆 直接内存 独立进程
GC影响
容量上限 受堆大小限制 受堆大小限制 受物理内存限制 独立扩展
过期淘汰 不支持 支持,粒度有限 部分支持 完善,按key设过期时间
多实例共享 不支持 不支持 不支持 支持
数据结构 KV KV KV String/Hash/List/Set/ZSet等
持久化 部分支持 RDB/AOF
运维监控 有限 有限 完善

小结

缓存选型不要上来就想用什么组件,先看流量在哪个量级。大部分系统用Redis做中央缓存就够了。到了需要本地缓存的阶段,说明流量已经大到需要认真考虑GC和内存管理了,这时候堆外缓存比HashMap靠谱得多。

缓存不是越多越好,每多一层就多一层复杂度。用最少的缓存层级满足当前的性能需求,是最合理的策略。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐