Guava Cache 在第三方接口异常时的优雅降级处理

269 阅读3分钟

一、背景

在实际开发中,我们经常使用缓存(如 Guava Cache)来存储从第三方接口获取的数据,以提高系统性能和响应速度。然而,第三方接口可能会出现故障或超时,导致缓存加载失败。如果直接使用 getUnchecked 方法,Guava Cache 会在 load 方法抛出异常时直接抛出 UncheckedExecutionException,这可能会导致系统中断或用户体验下降。对于一些非关键数据,我们希望即使第三方接口出错,系统也能继续运行,并且不缓存错误结果。

二、问题分析

1. Guava Cache 的默认行为

  • 当 CacheLoader.load 方法抛出异常时,Guava Cache 会包装异常并抛出 ExecutionException 或 UncheckedExecutionException。
  • 异常结果不会被缓存,但系统会中断当前操作。

2. 需求

  • 在第三方接口出错时,系统不应中断,而是返回一个默认值或降级结果。
  • 错误结果不应被缓存,以便下次请求可以重试加载。

三、解决方案

为了实现上述需求,我们可以通过以下两种方式对 Guava Cache 进行扩展:

1. 方案一:手动捕获异常 + 失效缓存键

在调用 getUnchecked 时,手动捕获异常并处理,同时使缓存键失效,确保下次请求可以重新加载数据。

实现步骤:
  • 使用 try-catch 捕获 UncheckedExecutionException
  • 在捕获异常后,调用 cache.invalidate(key) 使当前键失效。
  • 返回一个默认值或降级结果。
  • 代码示例:
public V getCachedValueSafely(K key) {
    try {
        return cache.getUnchecked(key);
    } catch (UncheckedExecutionException e) {
        // 记录日志
        logger.error("loading cache failed, key={}", key, e.getCause());
        // 使当前键失效,下次访问时重新加载
        cache.invalidate(key);
        // 返回默认值
        return defaultValue;
    }
}
优点:
  • 实现简单,逻辑清晰。
  • 精准控制异常处理和缓存失效。
缺点:
  • 需要在每个 get 调用处添加重复代码。

2. 方案二:自定义 CacheLoader + 空值包装

CacheLoader 内部捕获异常,并返回一个特殊标记(如 Optional.empty()),在外部调用时根据标记返回默认值。

实现步骤:
  • CacheLoader.load 方法中捕获异常,返回一个空值标记(如 Optional.empty())。
  • 在外部调用时,检查返回值是否为标记,如果是则返回默认值。
  • 代码示例:
LoadingCache<K, Optional<V>> cache = CacheBuilder.newBuilder()
    .build(new CacheLoader<K, Optional<V>>() {
        @Override
        public Optional<V> load(K key) {
            try {
                return Optional.of(thirdPartyAPI.loadData(key));
            } catch (Exception e) {
                // 返回空值标记
                return Optional.empty();
            }
        }
    });

public V getCachedValue(K key) {
    Optional<V> result = cache.getUnchecked(key);
    return result.isPresent() ? result.get() : defaultValue;
}
优点:
  • 逻辑集中,代码更简洁。
  • 避免在每个 get 调用处重复捕获异常。
缺点:
  • 需要处理 Optional 包装,可能对业务逻辑有一定侵入性。

四. 总结

在 Guava Cache 中处理第三方接口异常时,可以通过手动捕获异常 + 失效缓存键或自定义 CacheLoader + 空值包装的方式实现优雅降级。两种方案各有优劣,开发者可以根据具体场景选择合适的方式。对于非关键数据,这种设计能够有效提升系统的健壮性和用户体验,避免因第三方接口故障导致系统中断。

希望这个分析对大家有所帮助!另外延伸出另一个问题,如果这个第三方接口一直失败,你会如何解决这类缓存穿透的问题?欢迎一起讨论!