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

如何让Spring Cache缓存未命中时返回null并异步加载缓存?

CaffeineCache 缓存未命中时返回null并异步加载新值的实现需求

我目前用CaffeineCache搭配refreshAfterWrite和CacheLoader做缓存,现在的逻辑是缓存里有key时返回旧值,同时异步加载新值更新缓存。但我想改成:缓存没命中(key不存在)时直接返回null,同时异步加载新值存到缓存里给后续请求用。

当前的缓存配置代码如下:

private CaffeineCache buildCache(String cacheName, Ticker ticker, Long maxSize, Long ttl, TimeUnit ttlUnit,
                                 CacheLoader<Object, Object> cacheLoader){

    Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();

    // TTL
    if (ttl != null && ttl > 0 && ttlUnit != null) {
        if (cacheLoader != null) {
            cacheBuilder.refreshAfterWrite(ttl, ttlUnit);
            cacheBuilder.expireAfterWrite(180, TimeUnit.MINUTES);
        } else {
            cacheBuilder.expireAfterWrite(ttl, ttlUnit);
        }
    }

    // Max size
    if (maxSize != null && maxSize > 0){
        cacheBuilder.maximumSize(maxSize);
    }

    // Ticker
    cacheBuilder.ticker(ticker);

    if (cacheLoader != null) {
        return new CaffeineCache(cacheName, cacheBuilder.build(cacheLoader));
    }

    return new CaffeineCache(cacheName, cacheBuilder.build());
}

注:当前配置中refreshAfterWrite的时长远小于expireAfterWrite。


实现方案

要达成这个需求,核心是自定义CacheLoader的load方法,让它不阻塞请求,直接返回null,同时后台异步加载数据并更新缓存。具体操作如下:

1. 自定义支持异步加载的CacheLoader

重写load方法,在方法里提交异步任务去加载真实数据,方法本身立刻返回null。另外要加个机制避免同一key被重复触发异步加载,比如用ConcurrentHashMap记录正在加载的key:

public class AsyncNullCacheLoader<K, V> extends CacheLoader<K, V> {
    private final Function<K, V> dataLoader;
    private final ConcurrentHashMap<K, CompletableFuture<V>> loadingFlagMap = new ConcurrentHashMap<>();
    private final Cache<K, V> cache;

    public AsyncNullCacheLoader(Function<K, V> dataLoader, Cache<K, V> cache) {
        this.dataLoader = dataLoader;
        this.cache = cache;
    }

    @Override
    public V load(K key) throws Exception {
        // 检查是否已有加载任务,避免重复执行
        CompletableFuture<V> loadFuture = loadingFlagMap.computeIfAbsent(key, k ->
            CompletableFuture.supplyAsync(() -> dataLoader.apply(k))
                .whenComplete((result, throwable) -> {
                    // 加载完成后更新缓存,并移除加载标记
                    if (result != null && throwable == null) {
                        cache.put(key, result);
                    }
                    loadingFlagMap.remove(key);
                })
        );
        // 直接返回null,不等待加载完成
        return null;
    }

    @Override
    public CompletableFuture<V> reload(K key, V oldValue) throws Exception {
        // 刷新逻辑和首次加载保持一致,异步加载新值并更新缓存
        return CompletableFuture.supplyAsync(() -> dataLoader.apply(key))
            .whenComplete((newValue, throwable) -> {
                if (newValue != null && throwable == null) {
                    cache.put(key, newValue);
                }
            });
    }
}

2. 调整缓存构建逻辑

在buildCache方法中使用这个自定义的CacheLoader。注意要先构建缓存实例,再把它传入AsyncNullCacheLoader,这样加载完成后能直接更新缓存:

// 修改buildCache中创建带CacheLoader的缓存部分
if (cacheLoader != null) {
    // 先构建基础缓存实例
    Cache<Object, Object> cache = cacheBuilder.build();
    // 包装成自定义的异步加载CacheLoader
    AsyncNullCacheLoader<Object, Object> asyncLoader = new AsyncNullCacheLoader<>(
        key -> {
            // 这里替换成你的真实数据加载逻辑,比如调用DAO或者远程接口
            return yourActualDataLoadingLogic(key);
        },
        cache
    );
    // 重新构建带自定义Loader的缓存
    return new CaffeineCache(cacheName, cacheBuilder.build(asyncLoader));
}

3. 关键注意点

  • 异步加载的异常要处理,避免任务失败后缓存一直没有有效值
  • 用ConcurrentHashMap做加载标记,防止同一key被并发请求重复触发多次异步加载
  • 保留refreshAfterWrite的原有逻辑,确保缓存过期后能自动异步刷新,不影响现有请求

内容的提问来源于stack exchange,提问作者saket agarwal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 07:50:25