一、背景
在实际开发中,我们经常使用缓存(如 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 + 空值包装的方式实现优雅降级。两种方案各有优劣,开发者可以根据具体场景选择合适的方式。对于非关键数据,这种设计能够有效提升系统的健壮性和用户体验,避免因第三方接口故障导致系统中断。
希望这个分析对大家有所帮助!另外延伸出另一个问题,如果这个第三方接口一直失败,你会如何解决这类缓存穿透的问题?欢迎一起讨论!