
Spring Cache 默认缓存的是方法返回值对象本身(如 Flux),而非其订阅后产生的数据;若直接缓存 Flux,每次调用 toIterable() 或 block() 仍会触发底层 HTTP 请求,造成“缓存生效但后端仍高频调用”的假象。
spring cache 默认缓存的是方法返回值对象本身(如 flux),而非其订阅后产生的数据;若直接缓存 flux,每次调用 toiterable() 或 block() 仍会触发底层 http 请求,造成“缓存生效但后端仍高频调用”的假象。
在 Spring Boot 应用中使用声明式缓存(如 @Cacheable)时,一个常见却极易被忽视的陷阱是:缓存目标类型的选择直接影响缓存的实际效果。你可能已正确配置了 @EnableCaching、Caffeine 和 coffee-boots,并观察到日志中 CACHE MISS 每 15 分钟仅出现一次——这看似证明缓存工作正常。但与此同时,后端服务日志却显示 backend getting foobars 频繁发生(甚至每次页面刷新都触发)。问题根源并非缓存失效或代理失效,而在于你缓存了一个惰性计算的响应式流(Flux),而非其物化结果。
? 为什么 Flux 不该被直接缓存?
Flux 是 Reactor 中的冷序列(cold publisher):它不保存数据,只封装了“如何获取数据”的逻辑。当你对一个 Flux 调用 .toIterable()(如 Thymeleaf 模板中 @fooBarRepository.getFooBars().toIterable()),实际上是在重新订阅该流——这意味着 WebClient 会再次发起 HTTP 请求。即使 getFooBars() 方法本身被缓存(即同一个 Flux 实例被复用),这个被缓存的 Flux 每次被迭代时,仍会执行其内部的网络调用逻辑。
简言之:
✅ 缓存生效 → getFooBars() 方法体只执行一次/15分钟(所以 CACHE MISS 日志频率正常)
❌ 缓存失效 → Flux 的每次消费都会重放网络请求(所以后端日志高频刷屏)
✅ 正确做法:缓存物化结果
要使缓存真正减少后端负载,必须缓存已执行完毕、包含实际数据的容器,例如 List、Set 或 Optional。修改你的仓库方法如下:
@Repository("fooBarRepository")
public class WebAppFooBarRepositoryImpl implements WebAppFooBarRepository {
private final WebClient webClient;
public WebAppFooBarRepositoryImpl(WebClient webClient) {
this.webClient = webClient;
}
@Override
@Cacheable("foobar")
public List<foobar> getFooBars() {
System.out.println("CACHE MISS"); // 现在每15分钟打印一次,且后端调用同步减少
return webClient.get()
.uri("http://backend-service/foobars")
.retrieve()
.bodyToFlux(FooBar.class)
.collectList() // 物化为 List
.block(); // 同步阻塞获取(注意:仅适用于非响应式 WebMvc 场景)
}
}</foobar>
⚠️ 注意事项:
block()在 Spring MVC(Servlet 容器)中是可接受的,因其线程模型允许同步等待;但在 WebFlux 响应式栈中应避免block(),改用Mono+>
@Cacheable(需配合spring-boot-starter-cache对Mono的原生支持,Spring Boot 2.2+ 已内置)。- 若坚持使用响应式编程模型,推荐返回
Mono并确保缓存抽象能正确处理>
Mono(Spring Cache 默认支持Mono和Flux,但@Cacheable对Flux的缓存行为仍为缓存流对象本身,故仍需collectList()物化)。- Thymeleaf 模板无需改动:
${@fooBarRepository.getFooBars().toIterable()}仍可工作,因List的toIterable()是无副作用的轻量操作。
? 验证与进阶建议
-
验证方式:启动应用后连续刷新页面,观察两个日志:
- Web 应用日志:
CACHE MISS严格按配置周期(如 15m)出现; - 后端服务日志:
backend getting foobars出现频率与CACHE MISS完全一致。
- Web 应用日志:
-
进阶优化:
- 使用
@CacheEvict或 coffee-boots 的主动驱逐能力,在业务事件(如数据更新)发生时清理缓存; - 对高并发场景,考虑
@Cacheable(sync = true)防止缓存击穿; - 若需保留响应式链路,可将
Flux转为Mono后缓存,并在 Controller 层转回Flux响应客户端。
- 使用
缓存不是魔法,而是对数据生命周期的精确控制。选择缓存 List 而非 Flux,本质是明确告诉 Spring:“我要缓存结果,而不是缓存获取结果的动作”。这一细微差别,正是解决“缓存看似生效、后端却狂刷日志”这类疑难问题的关键所在。










