Caffeine 的 expireAfterWrite 和 refreshAfterWrite 用途和行为不同,需要结合业务场景合理配置。以下是两者的区别和联合使用的实践建议:
1. 核心区别
| 特性 | expireAfterWrite | refreshAfterWrite |
|---|---|---|
| 触发条件 | 写入后固定时间强制过期,缓存条目被移除 | 写入后固定时间触发异步刷新,保留旧值直到刷新完成 |
| 数据可用性 | 过期后数据不可用,触发同步加载(可能阻塞请求) | 刷新期间旧数据仍然可用,新数据异步加载 |
| 适用场景 | 强一致性场景(如敏感配置) | 高可用性场景(如频繁访问的热点数据) |
| 对性能影响 | 过期后同步加载可能导致瞬时压力 | 异步刷新减少阻塞,但可能返回旧数据 |
2. 同时使用的场景
当需要同时满足以下需求时,可以联合使用两者:
- 保证数据新鲜度:通过
expireAfterWrite设置最终过期时间,防止因刷新失败导致数据长期不更新。 - 减少阻塞和穿透:通过
refreshAfterWrite提前异步刷新,避免过期后大量请求同步加载数据库。
示例配置
LoadingCache<Key, Value> cache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES) // 最终过期时间(兜底)
.refreshAfterWrite(10, TimeUnit.MINUTES) // 定期刷新(优化体验)
.build(key -> loadDataFromDB(key));
3. 行为解析
-
写入后 10 分钟:
- 下一次访问该 Key 时触发异步刷新,后台加载新值,但立即返回旧值。
- 旧值仍然保留在缓存中,直到刷新完成或过期。
-
写入后 30 分钟:
- 缓存条目强制过期,后续访问会同步加载新值(阻塞请求直到加载完成)。
-
刷新失败时:
- 如果异步刷新过程中发生异常,旧值会保留,直到过期时间到达后强制重新加载。
4. 注意事项
-
刷新与过期间隔:
通常设置refreshAfterWrite < expireAfterWrite,确保在过期前有机会刷新数据。
例如:refresh=10m+expire=30m。 -
资源消耗:
频繁刷新可能增加后台负载,需结合executor配置合理的线程池(默认使用ForkJoinPool)。 -
数据一致性:
刷新期间可能短暂返回旧值,不适用于强一致性场景。 -
异常处理:
在CacheLoader中需捕获异常,避免刷新失败导致缓存条目直接过期。
5. 完整示例
LoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.refreshAfterWrite(10, TimeUnit.MINUTES)
// 自定义线程池(可选)
.executor(Executors.newFixedThreadPool(4))
.build(new CacheLoader<>() {
@Override
public String load(String key) {
return fetchFromDB(key); // 同步加载(用于初始化或过期后)
}
@Override
public CompletableFuture<String> reload(String key, String oldValue) {
return CompletableFuture.supplyAsync(() -> fetchFromDB(key));
}
});
// 使用缓存
String value = cache.get("key1"); // 触发加载或刷新
6. 适用场景
- 高频访问的热点数据:通过定期刷新减少用户感知的延迟。
- 容忍短暂不一致:如商品价格、库存等非强一致性数据。
- 依赖外部服务:防止因外部服务抖动导致缓存雪崩。
总结
expireAfterWrite 和 refreshAfterWrite 的联合使用能平衡数据的新鲜度和可用性,但需注意:
- 设置合理的过期和刷新的时间间隔。
- 处理刷新时的异常和资源竞争。
- 根据业务容忍度选择是否接受短暂不一致。