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

Spring @Cacheable如何忽略Feign异常返回上次有效缓存结果

问题根因

原有方案失效的核心逻辑偏差:

  • Spring @Cacheable的执行逻辑是固定的:缓存命中且未过期时直接返回缓存值,完全不执行目标方法;仅当缓存不存在、已过期时才会执行目标方法,执行完成后再根据unless/condition规则判断是否将返回值写入缓存。
  • 现有缓存配置使用expireAfterWrite(5, TimeUnit.MINUTES)写后过期策略,缓存到期后会直接删除旧条目,此时请求进入方法执行Feign调用,若调用失败返回null,缓存中已经没有旧值可供返回。unless = "#result == null"仅能阻止null被写入缓存,无法恢复已被删除的历史值。
  • 该策略从设计上就无法满足「刷新失败回退旧值」的需求,回退的核心前提是旧值在刷新前不会被提前删除。
推荐方案(基于Spring缓存抽象+Caffeine,无额外技术栈引入)

使用Caffeine的refreshAfterWrite刷新策略替代expireAfterWrite过期策略,配合自定义CacheLoader实现异常回退逻辑,完全匹配需求:

  • refreshAfterWrite策略:缓存写入达到配置时长后不会删除旧值,第一次访问触发异步刷新,刷新过程中所有请求直接返回旧值;刷新成功则替换为新值,刷新失败则保留旧值,等待下一个刷新周期重试。
  • 自定义CacheLoader拆分两种加载场景:首次无缓存时加载失败返回空列表,刷新时加载失败返回旧值。

代码实现

1. 修改缓存配置

@Configuration
@EnableCaching
class ClientConfig {

    @Bean
    @Qualifier("itemClientCacheManager")
    fun itemClientCacheManager(itemFeignClient: ItemFeignClient): CaffeineCacheManager {
        return object : CaffeineCacheManager("get_items") {
            override fun createCaffeineCache(cacheName: String): Cache<Any, Any> {
                val cacheLoader = object : CacheLoader<String, List<String>> {
                    // 首次无缓存时的加载逻辑
                    override fun load(key: String): List<String> {
                        return try {
                            itemFeignClient.getItems()
                        } catch (e: Exception) {
                            // 首次加载失败返回空列表
                            emptyList()
                        }
                    }

                    // 到期刷新时的加载逻辑
                    override fun reload(key: String, oldValue: List<String>): List<String> {
                        return try {
                            itemFeignClient.getItems()
                        } catch (e: Exception) {
                            // 刷新失败直接返回旧值,不更新缓存
                            oldValue
                        }
                    }
                }

                return Caffeine.newBuilder()
                    .maximumSize(10)
                    // 配置5分钟自动刷新,刷新期间保留旧值
                    .refreshAfterWrite(5, TimeUnit.MINUTES)
                    // 可选配置:30分钟写后硬过期,避免旧值无限期留存
                    .expireAfterWrite(30, TimeUnit.MINUTES)
                    .build(cacheLoader) as Cache<Any, Any>
            }
        }
    }
}

2. 简化Service层代码

移除方法内手动捕获异常返回null的逻辑,指定固定缓存key(无参方法使用固定key即可):

@Service
class ItemService(
    private val itemFeignClient: ItemFeignClient,
) {
    @Cacheable(
        cacheManager = "itemClientCacheManager",
        cacheNames = ["get_items"],
        key = "'all_items'"
    )
    fun getItems(): List<String> {
        // 兜底逻辑,正常加载/刷新流程优先走CacheLoader
        return try {
            itemFeignClient.getItems()
        } catch (e: Exception) {
            emptyList()
        }
    }
}

逻辑说明

  • 首次调用:缓存无值,同步执行Feign调用,成功则缓存结果并返回,失败返回空列表。
  • 缓存写入后5分钟内:所有请求直接返回缓存值,不触发Feign调用,与原有正常缓存逻辑一致。
  • 写入满5分钟后:第一个请求触发异步刷新,立即返回旧值;刷新成功则替换为新值,后续请求返回新结果;刷新失败则保留旧值,后续请求继续返回旧值,直到下一个5分钟周期再次尝试拉取。
  • 若配置硬过期时间,达到时间后缓存会被删除,逻辑回到首次调用的处理流程。
可选替代方案(Feign层面实现)

如果不便调整现有缓存配置,可通过Feign Fallback机制实现异常回退:

  1. 为ItemFeignClient自定义Fallback实现类,Feign调用抛出异常时自动进入Fallback逻辑。
  2. 在Fallback类中注入itemClientCacheManager,手动读取get_items缓存中已存在的值,有值则直接返回,无值则返回空列表。
    该方案需要手动操作缓存API,逻辑耦合度更高,仅推荐在缓存配置无法调整时使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 00:18:20