Caffeine Java缓存:refreshAfterWrite到期后如何优先返回新值
嘿,针对你遇到的Caffeine缓存刷新需求,我来帮你梳理下可行的方案——先拆解你的核心诉求,再评估你给出的方案,最后给出简洁、无临时workaround的实现~
问题核心
你当前使用refreshAfterWrite(5, TimeUnit.MINUTES)配置的是Caffeine的异步刷新策略:当缓存条目超过刷新时间后,第一次查询会先返回旧值,后台线程异步加载新值更新缓存。但你需要的是同步优先刷新逻辑:优先尝试加载新值,成功则返回新值;若加载失败,再回退返回旧值。
你的现有方案评估
你给出的方案思路是先判断是否需要同步刷新,再通过get+computeIfPresent尝试更新,但存在几个明显的问题:
- 竞态条件:
cache.get(key)和computeIfPresent之间是非原子操作,若多个线程同时触发该逻辑,可能导致重复加载,或者因为其他线程已更新缓存,导致oldValue.equals(value)判断失效,跳过本该执行的刷新。 - 逻辑冗余与硬编码:手动判断
age.toMinutes() > REFRESH_AFTER_MINUTES需要和缓存构建时的refreshAfterWrite参数保持一致,后期维护容易出错;且age.toMinutes() < 0的判断无意义(ageOf(key)返回的是缓存条目已存在的时长,不可能为负)。 - 异常处理不完整:
loader(key)抛出异常时,仅注释了//handle error,若未返回旧值,会导致缓存条目被置为null,不符合“失败返回旧值”的诉求。
推荐的简洁可扩展方案
下面给出基于Caffeine原生API的原子性实现,解决竞态问题,同时保持逻辑简洁、可扩展:
原子性同步刷新方案(最优)
利用Cache.asMap().compute()的原子特性,实现“优先同步刷新,失败返回旧值”的逻辑,同时自动从缓存策略中读取刷新配置,避免硬编码:
import com.github.benmanes.caffeine.cache.LoadingCache; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; public class CacheUtils { // 定义缓存加载逻辑(和你原有的loader一致) private static <K, V> V loader(K key) throws Exception { // 你的原加载实现,比如DB查询、远程调用等 return null; } /** * 优先同步刷新缓存,成功返回新值;失败返回旧值(若存在) * @param cache 目标LoadingCache实例 * @param key 缓存键 * @param <K> 键类型 * @param <V> 值类型 * @return 缓存值(新值优先,失败则旧值) */ public static <K, V> V getWithSyncRefreshFirst(LoadingCache<K, V> cache, K key) { // 从缓存策略中自动获取refreshAfter配置,避免硬编码 long refreshAfterMinutes = cache.policy().refreshAfterWrite() .map(policy -> policy.refreshAfter(TimeUnit.MINUTES)) .orElse(5L); return cache.asMap().compute(key, (k, oldValue) -> { // 判断是否需要刷新:缓存为空,或已超过刷新时长 boolean needsRefresh = oldValue == null || cache.policy().refreshAfterWrite() .flatMap(policy -> policy.ageOf(k)) .map(age -> age.toMinutes() >= refreshAfterMinutes) .orElse(true); if (!needsRefresh) { return oldValue; } try { // 同步加载新值,成功则返回(自动更新缓存) return loader(k); } catch (Exception e) { // 加载失败:若旧值存在则返回旧值,否则抛出异常(根据业务调整) if (oldValue != null) { return oldValue; } throw new CompletionException("Failed to load new value and no old value exists", e); } }); } }
方案优势
- 原子性:
compute方法是原子操作,避免多线程下的竞态条件,不会出现重复加载或缓存值不一致的问题。 - 无硬编码:自动从缓存的
refreshAfterWrite策略中读取刷新时长,和缓存构建配置保持一致,后期修改无需同步调整两处代码。 - 优雅的异常处理:加载失败时优先返回旧值,若旧值也不存在则抛出明确异常,符合业务预期。
- 可扩展性:可以轻松扩展为通用工具方法,适配不同类型的
LoadingCache,无需重复编写逻辑。
总结
你的原有方案思路方向正确,但存在竞态和维护性问题。推荐使用上述基于compute的原子性方案,完全利用Caffeine原生API实现,没有临时workaround,同时满足“优先刷新返回新值,失败返回旧值”的核心诉求。
内容的提问来源于stack exchange,提问作者Vytautas Šerėnas
相关产品推荐
相关产品推荐

