如何让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
相关产品推荐
相关产品推荐

