
本文介绍在 spring webflux 中利用 reactor 的 mono 与 webclient 进行响应缓存的两种实用方案:基于值的简单缓存(适合低并发、结果稳定场景)和基于 mono 实例的去重缓存(避免重复订阅导致的并发请求),并提供线程安全实现与关键注意事项。
本文介绍在 spring webflux 中利用 reactor 的 mono 与 webclient 进行响应缓存的两种实用方案:基于值的简单缓存(适合低并发、结果稳定场景)和基于 mono 实例的去重缓存(避免重复订阅导致的并发请求),并提供线程安全实现与关键注意事项。
在响应式编程中,WebClient 返回的 Mono 是冷序列(cold publisher)——每次订阅都会触发一次新请求。若对相同参数(如 fileName)频繁调用 getExtension(),默认行为将造成冗余 HTTP 调用与资源浪费。Reactor 本身不内置缓存机制,但可通过合理组合 ConcurrentHashMap、Mono.cache() 和生命周期钩子实现高效、线程安全的缓存策略。
✅ 方案一:基于响应值的轻量缓存(推荐用于读多写少、结果长期稳定场景)
适用于缓存结果极少变更、且无需处理并发请求竞争的场景。核心思路是缓存最终解析后的字符串值,并在命中时直接返回 Mono.just(value):
private final Map<string string> cache = new ConcurrentHashMap();
public Mono<string> getExtension(String fileName) {
// 先查缓存(O(1))
String cached = cache.get(fileName);
if (cached != null) {
return Mono.just(cached);
}
// 未命中则发起请求,并在成功后写入缓存
return webClient.get()
.uri(extensionUrl + "?file=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8))
.retrieve()
.bodyToMono(String[].class)
.map(extensions -> {
System.out.println("finished retrieving for: " + fileName);
return extensions.length > 0 ? extensions[0] : "";
})
.doOnNext(result -> cache.put(fileName, result)) // 成功后缓存
.doOnError(error -> System.err.println("Cache miss failed for " + fileName));
}</string></string>
⚠️ 注意事项:
- 务必对
fileName进行 URL 编码(如使用URLEncoder.encode(...)),避免特殊字符引发 URI 解析错误;- 此方案不防止并发请求:若两个线程同时首次访问同一
fileName,仍会触发两次 HTTP 请求;- 缓存无过期机制,适合静态或低频更新数据;如需 TTL,可考虑
Caffeine或Spring Cache集成。
✅ 方案二:基于 Mono 实例的“请求去重”缓存(推荐用于高并发、避免重复调用)
当多个订阅者几乎同时请求相同 fileName 时,更优策略是缓存 Mono<string></string> 本身——让后续订阅复用同一个 Mono 实例,从而共享其内部执行逻辑与结果(含错误)。配合 .cache() 可确保多次订阅不会重复触发下游请求:
private final Map<string mono>> monoCache = new ConcurrentHashMap();
public Mono<string> getExtension(String fileName) {
return monoCache.computeIfAbsent(fileName, key ->
webClient.get()
.uri(extensionUrl + "?file=" + URLEncoder.encode(key, StandardCharsets.UTF_8))
.retrieve()
.bodyToMono(String[].class)
.map(extensions -> extensions.length > 0 ? extensions[0] : "")
.doOnNext(result -> System.out.println("Cached result for: " + key))
.doOnError(error -> {
System.err.println("Failed to fetch extension for " + key + ": " + error.getMessage());
monoCache.remove(key); // 失败时不缓存,避免污染
})
.cache() // 关键:使 Mono 可被多次安全订阅
);
}</string></string>
✅ 优势说明:
- 天然防并发重复请求:首个订阅触发执行,其余订阅自动等待并共享结果(成功/失败均被广播);
.cache()确保 Mono 的“热化”,避免重复订阅导致的重复 HTTP 调用;computeIfAbsent是原子操作,线程安全,无需额外同步。
? 总结与选型建议
| 维度 | 值缓存(方案一) | Mono 缓存(方案二) |
|---|---|---|
| 并发安全性 | ❌ 可能重复请求 | ✅ 自动去重,强一致性 |
| 内存占用 | 低(仅存储 String) | 略高(存储 Mono 实例及内部状态) |
| 错误处理 | 需手动清理缓存 | 可通过 doOnError + remove 清理失效项 |
| 扩展性 | 易集成外部缓存(如 Caffeine) | 更适合纯 Reactive 场景,扩展需谨慎 |
实际项目中,若业务允许短暂重复请求,优先选用方案一以降低复杂度;若对性能与一致性要求严格(如高频文件元数据查询),强烈推荐方案二。两者均应搭配 ConcurrentHashMap 保证线程安全,并始终对 URI 参数做严格编码,确保缓存键的准确性与安全性。










