RuoYi-Cloud跑久了,很多团队都会碰到一种尴尬情况:Redis也接上了,数据库也做了索引,接口一个比一个写得规范,但压测的时候QPS就是上不去,CPU和带宽先亮红灯。后来我把Caffeine本地缓存引入RuoYi-Cloud的微服务链路里,用Caffeine、Redis、数据库组成三级缓存,线上接口平均耗时从原来的几十毫秒降到了不到一毫秒。这篇文章就把我这次的完整实践拆开讲透,从架构设计、代码落地、一致性方案到实测调优,适合正在用RuoYi-Cloud做微服务开发、又被热点查询性能卡住的读者参考。
1. 为什么RuoYi-Cloud纯Redis缓存跑不满性能——先找到真正瓶颈
1.1 Redis缓存看似快,实际被网络IO和序列化拖住了
很多RuoYi-Cloud项目组对缓存的认知就是“有Redis就够快了”,这个说法在低并发下确实成立。因为在单体应用里,程序调用Redis本质上就是一次本机或者内网TCP请求,单次往返通常只有零点几毫秒,加上序列化、反序列化,整体延迟约1到2毫秒,看起来完全够用。
但是当业务量上来了,情况会完全不同。RuoYi-Cloud本身的架构是多个微服务独立部署,Gateway网关、系统服务、认证服务之间还要经过RPC调用。假设一个用户信息查询接口先经过网关,再转发到某个微服务,该微服务再通过RedisTemplate去查缓存,单次请求在内网的网络链路上就要经过两跳甚至三跳。如果某个微服务的Redis连接池满、网卡队列堆积,或者Redis实例本身的过期Key频繁淘汰,接口耗时会从“看起来很快”的2毫秒恶化到几十毫秒甚至上百毫秒。
另一个容易被忽略的开销是序列化。RuoYi-Cloud里RedisTemplate默认使用的是JDK序列化(某些版本配置为FastJson或Jackson),JDK序列化生成的字节数组结构臃肿,一个简单的用户对象序列化后体积远超JSON格式。当响应体里包含权限标识列表、角色编码、部门ID集合这类字段时,序列化和反序列化的CPU开销会呈线性增长。我曾在一个权限查询接口上做过对比,关闭Redis缓存改用本地对象直接返回,接口单次耗时居然比走Redis还快。这说明Redis在网络和序列化上的开销已经超过了数据库查询本身的成本。
1.2 本地缓存的回归:Caffeine为什么是首选
既然Redis慢在网络和序列化,那么把数据直接放进应用进程内存里,由应用层直接读取,就能绕开所有网络开销。进程内读取Map或者Caffeine缓存,耗时通常在微秒级,比一次本机Redis访问快两个数量级。这就是多级缓存里第一级缓存的价值。
那为什么不用HashMap做本地缓存?原因很简单:无过期、无淘汰、无并发控制。一个HashMap长期存放用户权限数据,如果后台修改了权限,旧数据会一直驻留在内存里;数据量大时也没有自动剔除冷数据的机制,内存会慢慢膨胀。Guava Cache倒是支持过期和LRU,但它是在Google Guava库内部维护的,API和功能相对固定,更新活跃度也不如Caffeine。
Caffeine是目前Java生态里综合能力最强的本地缓存库。它内部实现了基于Window-TinyLFU的淘汰算法,既能识别热点数据,又能防止偶发的“一次性流量”把热点挤出缓存,同时支持基于大小的淘汰、写入后过期、访问后过期、异步加载、统计命中率等一系列功能。我在RuoYi-Cloud里选型时也考虑过Ehcache,但Ehcache更偏向重量级的独立缓存中间件,配置复杂、依赖较重,在Spring Boot环境下没有Caffeine轻量顺手。Caffeine单个Jar包只有几MB,无额外依赖,直接引入就能跑,对微服务镜像体积也友好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多级缓存整体设计:Caffeine负责近端,Redis负责远端,DB兜底
2.1 三级缓存的分层与数据流
多级缓存不是简单地在Redis前面再塞一个Map,而是要把不同数据特征放到合适的层级里,每一级都有自己的容量、速度和一致性代价。我最终落地的是三层结构:第一层Caffeine本地缓存,第二层Redis,第三层MySQL数据库。
- 第一层Caffeine:容量最小,通常几千条,存放的是高频热点数据,目标是让80%的读请求直接命中进程内缓存,完全不出网络。
- 第二层Redis:容量中等,存放全量活跃数据,目标是在本地缓存未命中时,跨实例共享数据,避免每个实例都去数据库打一遍。
- 第三层数据库:只承担缓存未命中时的兜底查询。
读取链路很清晰:先从Caffeine查,没命中再从Redis查,还没命中就查MySQL,查到结果后逐级回填。这样做的好处是,一个请求无论如何都不会轻易打到数据库,数据库的QPS压力能降一两个数量级。
这里要特别注意回填策略。MySQL查到的数据先写入Redis,再写入Caffeine;而Redis查到的数据只写入Caffeine,不需要回写Redis。如果Redis查到的数据还要再写一次Redis,属于无意义的重复写入,白白增加一次网络IO。这个初始版本我也犯过,回填逻辑里多写了一次Redis,压测时发现Redis写QPS比读QPS还高,仔细排查才意识到做了重复劳动。
2.2 写入与更新链路:先更新DB,再删缓存,而不是先更缓存
多级缓存的写入链路比读取更容易出错。我采用的是经典的Cache-Aside模式:发生数据变更时,先更新数据库,再删除Redis缓存,同时通过消息广播让各微服务实例删除本地Caffeine缓存。
为什么是“先更新数据库,再删缓存”,而不是“先删缓存,再更新数据库”?举个并发例子:线程A先删了缓存,线程B随后查询发现缓存未命中,去数据库查到旧数据写回缓存。此时线程A才去更新数据库,最终结果是数据库是新值,缓存是旧值,下一次查询会一直拿到过期数据。而先更新数据库再删缓存,即使删除缓存那瞬间有并发读把旧值写入缓存,也会因为缓存已删除,后续查询会重新拉取数据库新值,缓存污染窗口极小。
删除Redis缓存后,本地Caffeine的清理依赖广播通知。RuoYi-Cloud各微服务之间天然通过Nacos注册中心互通,但这种方式适合服务配置推送,不适合做高频的缓存失效消息传递。我当时用的是Redis的Pub/Sub能力,专门开一个ruoyi:cache:invalidation频道,任意一个服务实例更新数据后,向频道发布一条包含缓存键的消息,所有订阅了该频道的实例收到消息后,删除自己本地Caffeine里对应的键。Redis Pub/Sub没有消息堆积能力,如果某个实例短暂断开可能丢失消息,所以必须设置一个兜底TTL,本地缓存即使没收到主动失效通知,也会在几十秒到几分钟内过期重建。这个组合方案在RuoYi-Cloud这种内部服务数量可控的环境下已经足够可靠。
2.3 一致性取舍:最终一致是现实选择,无法也没必要做到强一致
我必须先把丑话说在前面:任何多级缓存都做不到强一致。写操作更新数据库后,Redis缓存删除和Caffeine清空都需要时间,这些时间窗口内读请求仍有可能拿到旧值。所以设计多级缓存前,一定要判断业务能不能接受这种短时间的最终一致。
RuoYi-Cloud里适合做多级缓存的场景通常是字典数据、参数配置、用户基础信息、菜单路由这类“读多写少、变更频率低、数据量小”的数据。这类数据本身容忍几十毫秒甚至几秒的旧值窗口。而不适合的场景是库存扣减、余额变动、实时状态查询,这些必须每次从数据库或者分布式锁保护下读取。我给项目组定的规矩很简单:涉及金额、库存、订单状态的一律不走本地缓存;普通配置类数据、展示类数据才允许加入多级缓存。
ThreadLocal也顺便提一句:曾经有同学试图用ThreadLocal做缓存,因为每个请求线程内读取贼快,结果缓存跟着线程池复用到其他租户的请求上,酿成数据串号的事故。本地缓存必须是进程级且带全局键,不能依赖线程上下文。
3. RuoYi-Cloud中接入Caffeine的完整落地步骤
3.1 引入依赖与基础参数配置
Caffeine本身没有Spring Boot官方starter,但Spring Cache抽象层对它支持得非常好。我在RuoYi-Cloud服务里加依赖非常简单,只需要在pom.xml里增加Caffeine的坐标:
xml复制<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
如果项目里已经引入了Spring Boot的spring-boot-starter-cache,Caffeine会自动被Spring Cache识别为缓存管理器。不过我实际落地时没有走Spring Cache注解,因为Spring Cache自带的@Cacheable只能作用于方法返回值,且缓存键生成规则对多级缓存不太友好,我自己封装了一套更灵活的工具类。不过基础依赖和参数配置仍然有效,可以在application.yml里预留配置项:
yaml复制ruoyi:
cache:
caffeine:
# 单个服务实例本地缓存最大条目数
maximum-size: 5000
# 写入后过期时间(秒)
expire-after-write: 60
# 是否开启命中率统计
record-stats: true
redis:
# Redis缓存过期时间(秒)
expire-after-write: 300
注意这里有两个不同的过期时间。Caffeine的过期时间要短于Redis的过期时间,因为本地缓存没有持久化能力,进程重启即丢失,设置较短TTL可以更快地丢弃潜在脏数据。如果Caffeine的TTL设得比Redis还长,一旦某次主动失效消息没送达,本地缓存会长期保留错误数据。我线上环境的配置比例通常是1:5,比如Caffeine 60秒、Redis 300秒,既能保证热点数据在本地停留足够久,又能控制数据污染窗口。
3.2 封装统一的多级缓存访问服务
为了让业务代码用起来简单,我封装了一个MultiCacheService,对外只暴露三个方法:查询缓存、更新缓存、清理缓存。内部自动处理Caffeine、Redis、DB三级链路。
java复制@Component
public class MultiCacheService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CaffeineCacheManager caffeineCacheManager;
private final Cache<String, Object> caffeineCache;
public MultiCacheService() {
this.caffeineCache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.recordStats()
.build();
}
/**
* 查询缓存:先查本地,再查Redis
*/
public Object get(String key, String redisKey) {
Object localValue = caffeineCache.getIfPresent(key);
if (localValue != null) {
return localValue;
}
Object remoteValue = stringRedisTemplate.opsForValue().get(redisKey);
if (remoteValue != null) {
caffeineCache.put(key, remoteValue);
return remoteValue;
}
return null;
}
/**
* 直接放入本地缓存(通常由DB查询回填调用)
*/
public void putLocal(String key, Object value) {
caffeineCache.put(key, value);
}
/**
* 清空本地缓存(收到失效广播时调用)
*/
public void evictLocal(String key) {
caffeineCache.invalidate(key);
}
}
这里有个关键点是Caffeine缓存不允许缓存null值。如果数据库没查到数据,直接把null放进去会抛异常。处理方式是使用Optional包装:
java复制Object dbResult = userMapper.selectById(id);
caffeineCache.put(key, Optional.ofNullable(dbResult));
读取时校验Optional.isPresent()即可。这个细节我最初忽略了,结果压测时一遇到空结果就报NPE,排查半天才发现是Caffeine在拒绝空值。
StringRedisTemplate在示例里默认值序列化为String,真实的RuoYi-Cloud环境里RedisTemplate往往配置了JSON序列化,此时读取出来的对象已经是反序列化后的实体对象,存入Caffeine时无需再做二次转换。但如果不小心让StringRedisTemplate和Jackson序列化的RedisTemplate混用,读出来的对象类型就会错乱,遇到这种情况建议统一使用同一个RedisTemplate注入。
3.3 AOP注解模式:一个@MultiCache搞定业务侧接入
工具类层面对业务方来说还是太底层。我更推荐在RuoYi-Cloud里配合AOP做一个注解,让业务代码只加一个注解就能自动走多级缓存。自定义注解和切面的核心代码大概是这样:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiCache {
// 缓存键,支持SpEL表达式
String key();
// Redis键前缀
String redisKeyPrefix() default "ruoyi:cache:";
// 过期时间
long expire() default 60L;
}
java复制@Aspect
@Component
public class MultiCacheAspect {
@Around("@annotation(multiCache)")
public Object around(ProceedingJoinPoint joinPoint, MultiCache multiCache) throws Throwable {
// 解析SpEL表达式生成缓存key
String localKey = parseKey(multiCache.key(), joinPoint);
// 第一步:查本地缓存
Object localValue = multiCacheService.getLocal(localKey);
if (localValue != null) {
return localValue;
}
// 第二步:查Redis缓存
String redisKey = multiCache.redisKeyPrefix() + localKey;
Object remoteValue = multiCacheService.getRemote(redisKey);
if (remoteValue != null) {
multiCacheService.putLocal(localKey, remoteValue);
return remoteValue;
}
// 第三步:执行原方法查询数据库
Object methodResult = joinPoint.proceed();
// 第四步:两级缓存回填
multiCacheService.putRemote(redisKey, methodResult, multiCache.expire());
multiCacheService.putLocal(localKey, methodResult);
return methodResult;
}
}
实际在RuoYi-Cloud的某个Service实现里,用法是这样的:
java复制@MultiCache(key = "#userId", redisKeyPrefix = "ruoyi:user:info:")
public UserInfo getUserInfo(Long userId) {
return userMapper.selectById(userId);
}
调用方完全感知不到缓存的存在,切面会在方法执行前自动查两级缓存,如果没有命中才真正执行SQL查询。这里有一个非常重要的经验:切面拦截的方法返回值必须能安全地放进缓存,如果方法返回值是数据库自动生成的主键ID之类的可变对象,回填缓存时要复制一份,否则后面修改对象字段会不知不觉把缓存里的数据也改了。这个坑在微服务场景特别隐蔽,我踩过一次之后,统一在回填方法里加了ObjectUtils.clone逻辑。
3.4 失效广播:把本地缓存删除扩散到所有实例
既然每个微服务实例都有自己独立的Caffeine,更新数据时就必须把所有实例里的同一键清掉。我使用Redis Pub/Sub实现了一个简单的失效监听器。
发布端在更新数据库后执行:
java复制stringRedisTemplate.convertAndSend("ruoyi:cache:invalidation", key);
订阅端监听器:
java复制@Component
public class CacheInvalidationListener {
@Autowired
private MultiCacheService multiCacheService;
@EventListener
public void onMessage(Message message) {
String key = new String(message.getBody());
multiCacheService.evictLocal(key);
}
}
同时需要在Redis配置类里手动注册监听容器:
java复制@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory factory,
CacheInvalidationListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(listener, new ChannelTopic("ruoyi:cache:invalidation"));
return container;
}
这套方案实现很简单,RuoYi-Cloud本身有Redis依赖,不需要额外引入消息中间件。但如果团队已经接了RabbitMQ或RocketMQ,用MQ做失效广播会更可靠,因为MQ有持久化和重试机制,而Pub/Sub是即发即弃的。我的建议是:内部服务实例数量少于20个时用Redis Pub/Sub完全没有问题;超过50个实例才需要考虑MQ方案,避免高频失效消息导致Redis广播风暴。
4. 多级缓存最绕不开的坑:一致性、序列化与热点Key
4.1 Caffeine的引用类型与GC压力:默认别乱开weakKeys和weakValues
Caffeine构建时有一组比较冷门但影响巨大的配置项:weakKeys()、weakValues()、softValues()。官方文档说这些配置允许Cache中的键值被GC回收,适合缓存对象生命周期短、希望避免内存泄漏的场景。
但在多级缓存场景下,我强烈不建议开启这些弱引用选项。原因很简单:Caffeine本地缓存里的数据是Redis热数据的副本,希望它在TTL之内稳定存在。如果用weak引用,一旦发生GC,缓存里的数据可能被直接回收,下一次读取又要穿透到Redis,不仅没有提高命中率,反而增加缓存重建的CPU消耗。我在某个服务上试过开启weakValues(),线上接口P99直接翻了一倍,因为GC后大量缓存瞬间失效,Redis请求量暴增。
如果确实担心Caffeine内存占用过大,正确做法是限制maximumSize和maximumWeight,而不是依赖GC回收。Caffeine内部基于Window-TinyLFU算法,本身就会根据访问频率淘汰冷数据,只要maximumSize设置合理,内存占用是可控的。我一般按“实例内存的10%以内”作为Caffeine容量预算,比如4GB堆内存的服务,本地缓存条数控制在5000到10000之间,每条缓存对象控制在几KB以内,整体内存开销不超过500MB。
4.2 多实例本地缓存不一致:时间窗口内如何兜底
本地缓存最大的问题就是每个服务实例单独一份数据,必然会出现数据不一致。假设系统有4个RuoYi-Cloud服务实例,实例A上用户张三更新了手机号,通过广播把本地缓存清掉了,但广播消息到达实例B之前,实例B恰好被负载均衡分配了一个用户手机号查询请求,它读到的还是旧值。
这个问题无法彻底消除,只能通过三个方面来压缩窗口:
其一,失效消息要尽可能快。Redis Pub/Sub的发布订阅延迟通常控制在毫秒级,比轮询数据库变更高效得多。
其二,合理设置Caffeine的expireAfterWrite。哪怕失效消息全部丢失,本地缓存也会在几十秒内自动过期,保证最坏情况下的脏数据存活时间可控。我通常把字典类数据设置为60秒,用户配置信息设置为120秒,超过5分钟的业务数据就不建议放Caffeine了。
其三,对一致性要求高的接口可以走“强制旁路”。在注解上增加一个bypassLocal参数,比如金额查询、订单状态查询直接标记成@MultiCache(bypassLocal = true),这样的接口会跳过Caffeine缓存,只用Redis甚至直接查数据库。调用方可以根据业务宽容度自由选择。
4.3 缓存穿透与缓存雪崩在本地缓存层的连锁反应
多级缓存引入后,缓存穿透和雪崩的表现形态和单层Redis时代完全不同。
缓存穿透问题在本地缓存层反而更容易解决,因为本地缓存的查询成本极低,只要在DB查询为空时缓存一个空值(包装Optional),也能挡住后面的大流量,避免每个请求都打到数据库。这里需要注意本地缓存空值的TTL要比正常值短,我设置为30秒,因为空值本身是廉价信息,不需要长期持有。
缓存雪崩则更值得警惕。当Caffeine的expireAfterWrite统一设置为60秒时,所有热门Key会在同一时刻集体过期,此时如果DB查询逻辑没有加锁,等于缓存被一把清空,大量请求同时穿透到Redis,Redis又同时去查数据库,数据库瞬时QPS可能暴涨。解决方式很简单,给不同业务域的数据设置不同的过期时间,并在过期时间上加上随机偏移:
java复制long baseExpire = 60L;
long randomExpire = baseExpire + ThreadLocalRandom.current().nextLong(0, 30);
caffeineCache.policy().expireAfterWrite().ifPresent(policy -> policy.put(key, new Expiry... ));
如果用的是expireAfterWrite(long)这种固定方式,可以在构建不同业务域的Cache对象时传入不同的过期时间,而不是全项目共用一个构建参数。我在RuoYi-Cloud里按业务域拆分了多个Cache实例,比如userCache、dictCache、configCache,各自配置不同的maximumSize和expireAfterWrite,效果比一个万能缓存好得多。
本地缓存层的另一个雪崩连锁反应是:不管Caffeine如何设置,所有实例在同一时间都可能经历缓存过期,此时Redis和数据库会收到来自所有实例的穿透流量,流量是单实例的N倍。要防止这种集群级别的雪崩,比较有效的办法是给每个实例的Caffeine过期时间增加一个实例级别的偏移量,比如通过instanceId.hashCode() % 30生成不同偏移,这样四个实例的过期时刻彼此错开,Redis和数据库的压力就平滑很多。
5. 压测结果与参数调优:性能提升多少,参数怎么调
5.1 压测场景与方法:选一个高频读接口当样本
我建议把压测样本选一个业务上高频、读多写少、数据量小但查询热度高的接口,比如RuoYi-Cloud里的用户权限树查询或者数据字典翻译接口。我自己用的是“用户信息+角色列表+权限标识”聚合查询接口,这个接口在后台管理场景里几乎每个页面都要调用。
压测用JMeter开8线程组800并发,持续压测10分钟,统计平均耗时、P99、QPS和数据库QPS四个核心指标。对比场景分为三类:未接缓存、仅Redis缓存、Caffeine+Redis多级缓存。三个场景都使用相同的服务实例数和数据库数据量,确保结果可对比。
5.2 数据对比:多级缓存的性能提升到底有多大
我压测得到的典型数据趋势如下(不同的机器配置会带来差异,但相对关系一致):
| 压测场景 | 平均耗时 | P99耗时 | 接口QPS | 数据库查询QPS |
|---|---|---|---|---|
| 无缓存 | 28ms | 52ms | 约850 | 约850 |
| 仅Redis | 2.3ms | 8.7ms | 约2200 | 约80 |
| Caffeine+Redis | 0.4ms | 1.8ms | 约6400 | 约5 |
有两处数据值得细看。第一,仅Redis场景下数据库QPS还是有80左右,说明Redis未命中率仍然存在;引入Caffeine本地缓存后,数据库QPS降到了个位数,几乎等于没有数据库压力。第二,多级缓存的P99是1.8ms,这比单次Redis访问还要快,因为绝大多数请求直接命中本地缓存,走Redis的比例已经非常低。
不过我必须补充一句:这些数字展示的是趋势,不是绝对标准。如果你的接口查询SQL本身要join五六张表、单次查询耗时200ms以上,那么多级缓存的收益会更夸张;如果接口查询本身只有一次唯一索引查询、耗时0.5ms,多级缓存带来的收益就不会特别明显,因为网络开销占比已经很低。所以在动手改造前,先压测一下当前接口的性能,再决定需不需要上多级缓存。
5.3 参数调优思路:命中率、内存占用和GC的三角权衡
压测出性能数据后,参数调优才刚刚开始。我给团队定的调优顺序是:先看缓存命中率,再看内存占用,最后才看接口耗时的绝对值。
开启Caffeine的recordStats()后,可以通过caffeineCache.stats()获取hitRate()、missCount()、evictionCount()。命中率是最重要的指标。本地缓存命中率低于60%时,说明maximumSize设置偏小,热点数据经常被淘汰出去;命中率高于95%时,可以认为缓存容量充足,不需要再调整大小。
但高命中率不能盲目追求。Caffeine缓存本质上是用JVM堆内存换性能,缓存条目越多,GC扫描时间越长。我在一个服务里把maximumSize从5000提升到20000,命中率只从89%涨到92%,但Full GC次数明显增多,接口P99反而劣化了。后来把maximumSize回退到8000,并给对象体积较大的缓存域单独设置maximumWeight限制,整体效果最优。
还有几个调优参数值得根据线上监控动态调整:
expireAfterWrite:调整依据是数据变更频次。如果后台管理员修改字典信息的次数很少,可以放宽到120秒;如果数据是定时任务刷新的,可以设置成任务周期的1.5倍。initialCapacity:初始化容量设置得过大会白白占用内存,过小则会在缓存预热阶段频繁扩容。按预估热点条目数的一半设置即可,我记得2000条的缓存设initialCapacity=1024体验最好。- GC频率:本地缓存量大之后,老年代回收会更频繁,配合
-XX:+UseG1GC以及适当增大-XX:MaxGCPauseMillis,通常能保证接口P99稳定在个位数毫秒。
如果项目组有Prometheus+Grafana监控体系,建议把Caffeine的stats()指标暴露成Metric,比如通过Micrometer的CacheMeterBinder绑定Caffeine缓存实例,然后看“缓存命中率”和“缓存淘汰数”随接口峰值的变化曲线,调优会比拍脑袋准确得多。
我在RuoYi-Cloud里落地这套多级缓存差不多跑了大半年,最大的体会是本地缓存不是银弹,它解决的问题和引入的成本同样明显。设计多级缓存前,一定要先把业务数据分类想清楚,确认哪些接口能容忍最终一致,哪些必须强制读主库,不能一股脑全部塞进Caffeine。另一个经验是参数一定不要照搬网上的配置,不同业务域的热点规模差异很大,同一个maximumSize不可能既满足字典缓存又满足用户权限缓存,拆分多个缓存实例、按需调参会省掉线上很多突发问题。如果你的压测场景里热点接口的数据库查询QPS一直降不下来,不妨从这三级缓存链路入手试一试。
