Spring中Caffeine缓存refreshAfterWrite异步刷新未触发问题排查
Spring中Caffeine缓存异步全量刷新问题解决
问题分析
你想要实现全量异步刷新缓存所有键,从外部服务获取数据后无缝替换旧值且无停机,但当前配置的AsyncLoadingCache中fetchCacheValues()从未被调用,核心原因是你误解了refreshAfterWrite()的行为:
refreshAfterWrite是被动触发机制:只有当某个缓存key被访问,且该key距离上次写入时间超过设定的5分钟时,才会异步触发对应的loader逻辑刷新这个key。它不会主动遍历所有key执行全量刷新。- 你的loader实现不符合设计预期:
buildAsync的loader函数是针对单个key的,应该根据传入的key获取对应值,但你现在不管什么key都调用全量获取的fetchCacheValues(),逻辑本身就不匹配。
另外你提到用定时任务+CachePut出现停机问题,大概率是因为刷新时先删除旧值再加载新值,或者同步加载新值导致缓存空窗期,用户访问无缓存可用。
解决方案
方案1:主动全量异步刷新(匹配你的全量刷新需求)
这种方式主动定时获取全量数据,批量更新缓存,完全避免空窗期:
- 缓存初始化时先加载全量数据
- 用Spring定时任务异步执行全量刷新,确保新数据就绪后再替换旧值
@Bean public AsyncLoadingCache<Object, Object> myCache() { // 启动时加载初始全量数据 Map<Object, Object> initialData = cacheInitializer.fetchCacheValues(); return Caffeine.newBuilder() .maximumSize(cacheProperties.getProp()) .recordStats() // 初始化loader,根据key返回对应初始值 .buildAsync((key, executor) -> CompletableFuture.completedFuture(initialData.get(key))); } // 每5分钟异步执行全量刷新 @Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES) @Async public void refreshAllCacheEntries() { // 先异步获取最新全量数据,确保数据就绪后再更新缓存 Map<Object, Object> latestData = cacheInitializer.fetchCacheValues(); // 获取缓存的并发安全视图,直接批量替换 ConcurrentMap<Object, Object> cacheMap = myCache().asMap(); // 批量更新现有key的新值 latestData.forEach(cacheMap::put); // 可选:删除缓存中存在但新数据已不存在的key(根据业务需求决定) cacheMap.keySet().removeIf(key -> !latestData.containsKey(key)); }
- 用
@Async保证刷新任务异步执行,不阻塞主线程 - 先加载完所有新数据再批量替换,用户访问始终能获取到旧值或新值,完全无空窗期
- 缓存的
asMap()是并发安全的,批量操作不会出现线程安全问题
方案2:修正refreshAfterWrite用法(适合单key被动刷新场景)
如果你的需求其实是单个key被访问时才触发刷新,而非全量刷新,那需要修正loader逻辑,让它根据key获取对应值:
@Bean public AsyncLoadingCache<Object, Object> myCache() { return Caffeine.newBuilder() .maximumSize(cacheProperties.getProp()) .refreshAfterWrite(5, TimeUnit.MINUTES) .recordStats() // 根据传入的key获取对应数据,而非全量获取 .buildAsync(key -> cacheInitializer.fetchValueByKey(key)); }
此时当某个key被访问且超过刷新时间时,会异步调用fetchValueByKey(key)获取新值,旧值会被继续返回直到新值加载完成,实现无缝替换。
内容的提问来源于stack exchange,提问作者ppk
相关产品推荐
相关产品推荐

