You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 15:30:28