如何实现Guava Suppliers.memoizeWithExpiration()的异步版本(优先Vert.x原生方案)
如何实现Guava Suppliers.memoizeWithExpiration()的异步版本(优先Vert.x原生方案)
嘿,这个问题我刚好研究过!你想把Guava那种带过期时间的同步缓存改成异步版本,还优先用Vert.x原生方案,对吧?先帮你理清几个关键问题,再给你具体的实现思路和代码。
首先得说清楚:直接缓存Future对象确实有坑,就像你担心的那样——Future一旦完成(不管成功还是失败),状态就固定了。如果缓存了一个失败的Future,那后续调用拿到的都是失败结果,而且Guava的同步锁机制也和Vert.x的异步非阻塞理念不搭,确实不太适合。
那Vert.x原生怎么实现呢?我给你两个方案:一个是纯原生自定义实现,另一个是Vert.x官方集成的Caffeine缓存(这个更推荐,成熟高效)。
方案一:纯Vert.x原生自定义实现
我们可以基于Vert.x的SharedData(本地缓存)+ 定时器来实现,同时处理并发请求避免重复加载的问题:
第一步:封装异步缓存工具类
import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.shareddata.LocalMap; import java.util.concurrent.TimeUnit; public class AsyncExpiringMemoizer<T> { private final Vertx vertx; private final String cacheKey; private final long expirationMs; private final AsyncSupplier<T> dataSupplier; private final LocalMap<String, Object> cacheStore; // 定义异步数据源接口 @FunctionalInterface public interface AsyncSupplier<T> { Future<T> fetch(); } // 缓存条目:存值和过期时间 private static class CacheEntry<T> { T value; long expireAt; CacheEntry(T value, long expireAt) { this.value = value; this.expireAt = expireAt; } } public AsyncExpiringMemoizer(Vertx vertx, String cacheKey, long duration, TimeUnit unit, AsyncSupplier<T> supplier) { this.vertx = vertx; this.cacheKey = cacheKey; this.expirationMs = unit.toMillis(duration); this.dataSupplier = supplier; this.cacheStore = vertx.sharedData().getLocalMap("async-memoizer-store"); } public Future<T> get() { // Verticle默认单线程,这里的同步锁不会阻塞事件循环(多实例部署可根据场景调整) synchronized (this) { // 先检查缓存是否有效 CacheEntry<T> cachedEntry = (CacheEntry<T>) cacheStore.get(cacheKey); if (cachedEntry != null && System.currentTimeMillis() < cachedEntry.expireAt) { return Future.succeededFuture(cachedEntry.value); } // 检查是否有正在加载的请求,避免重复调用数据源 String loadingFlagKey = cacheKey + "-loading"; Future<T> loadingFuture = (Future<T>) cacheStore.get(loadingFlagKey); if (loadingFuture != null) { return loadingFuture; } // 没有缓存,开始异步加载 Future<T> loadFuture = dataSupplier.fetch() .onSuccess(result -> { long expireTime = System.currentTimeMillis() + expirationMs; cacheStore.put(cacheKey, new CacheEntry<>(result, expireTime)); // 加载完成后移除加载标记 cacheStore.remove(loadingFlagKey); // 设置定时器自动清理过期缓存(也可下次获取时再清理) vertx.setTimer(expirationMs, timerId -> cacheStore.remove(cacheKey)); }) .onFailure(error -> { // 加载失败,移除加载标记,让后续请求可以重试 cacheStore.remove(loadingFlagKey); }); // 缓存正在加载的Future,让其他等待的请求复用 cacheStore.put(loadingFlagKey, loadFuture); return loadFuture; } } }
第二步:在你的代码里使用
// 在Verticle初始化时创建缓存实例 AsyncExpiringMemoizer<String> tokenMemoizer = new AsyncExpiringMemoizer<>( vertx, "auth-token-cache", 50, TimeUnit.MINUTES, this::provideTokenInternal ); // 对外提供的获取token方法 public Future<String> provideToken() { return tokenMemoizer.get(); } // 你的异步获取token的核心逻辑 private Future<String> provideTokenInternal() { // 这里写你的异步逻辑,比如调用第三方认证接口 return vertx.createHttpClient() .get(8080, "auth-service", "/api/token") .send() .map(response -> response.bodyAsString()); }
这个实现的好处:
- 完全基于Vert.x原生API,无第三方依赖
- 避免了缓存Future的问题,缓存的是实际的业务值
- 处理了并发请求:缓存失效时只有第一个请求触发加载,其他请求复用结果
- 自动清理过期缓存,无需手动维护
方案二:Vert.x集成Caffeine缓存(更推荐)
如果你能引入第三方依赖,Vert.x官方集成了Caffeine(一款高性能Java缓存库),用它实现异步过期缓存会更简洁,而且Caffeine本身已经处理了各种缓存边界问题(比如击穿、雪崩)。
第一步:引入依赖(Maven为例)
<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-caffeine-cache</artifactId> <version>你的Vert.x版本</version> </dependency>
第二步:实现缓存逻辑
import io.vertx.caffeine.cache.Cache; import io.vertx.caffeine.cache.CaffeineCache; import io.vertx.core.Vertx; import java.util.concurrent.TimeUnit; // 初始化Caffeine异步缓存 Cache<String, String> tokenCache = CaffeineCache.create(vertx, com.github.benmanes.caffeine.cache.Caffeine.newBuilder() .expireAfterWrite(50, TimeUnit.MINUTES) // 写入后50分钟过期 .buildAsync((cacheKey, executor) -> provideTokenInternal().toCompletionStage()) ); // 对外提供的获取token方法 public Future<String> provideToken() { return tokenCache.get("auth-token"); } // 你的异步获取token逻辑 private Future<String> provideTokenInternal() { return vertx.createHttpClient() .get(8080, "auth-service", "/api/token") .send() .map(response -> response.bodyAsString()); }
这个方案的优势:
- 代码更简洁,无需自己处理缓存过期、并发加载等细节
- Caffeine性能优异,支持多种缓存策略
- 完美兼容Vert.x的Future异步模型
总结一下:如果可以引入第三方依赖,优先用Vert.x+Caffeine的方案;如果必须纯原生,就用第一个自定义实现。两种方案都能满足你类似Guava memoizeWithExpiration的异步需求,而且符合Vert.x的非阻塞风格。
备注:内容来源于stack exchange,提问作者Jakub Bochenski
相关产品推荐
相关产品推荐

