
在微服务架构中,缓存是提升性能、降低数据库压力的利器。Google 开源的 Caffeine 凭借高性能、易用性,成为 Java 生态中最受欢迎的本地缓存库之一。
但最近,一位同事向我求助:
“我明明设置了
.refreshAfterWrite(5, TimeUnit.MINUTES),为什么数据没有每 5 分钟自动刷新?
这个问题看似简单,实则触及了 Caffeine 设计哲学的核心——“懒加载 + 按需刷新”。今天,我们就从 源码剖析 + 实战案例 + 正确姿势 三个维度,彻底揭开 refreshAfterWrite 的神秘面纱!
private final LoadingCache<String, ApiResultDO<BehaviorReqAnalysisListResp>> cache =
Caffeine.newBuilder()
.maximumSize(10)
.refreshAfterWrite(5, TimeUnit.MINUTES) // ⚠️ 关键配置
.build(key -> {
log.info("LoadingCache:{}", key);
return LoadingCache(key); // 耗时远程调用
});
开发者期望:每 5 分钟自动重新加载数据,保持缓存新鲜。
实际行为:只要没人访问这个 key,数据永远不会刷新!
某电商系统使用 Caffeine 缓存用户行为分析结果(如“近7天点击商品数”)。
配置了 refreshAfterWrite(5, MINUTES),希望每 5 分钟更新一次用户画像。
结果:
根本原因:开发者误以为 refreshAfterWrite 是“定时任务”,而它其实是“触发式刷新”。
我们以 Caffeine 3.x 源码为基础(GitHub - ben-manes/caffeine),追踪关键路径。
LocalCache.get(K key)当调用 cache.get("key") 时,最终会进入 BoundedLocalCache 的 computeIfAbsent 方法。
关键逻辑如下(简化版):
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
Node<K, V> node = data.get(key);
if (node != null) {
// 检查是否需要刷新
if (refreshAfterWrite() && isExpiredForRefresh(node)) {
// 异步刷新(不阻塞当前线程)
scheduleRefresh(node, key, ...);
}
return node.getValue(); // ⚡️ 先返回旧值!
}
// 首次加载...
}
isExpiredForRefresh(Node)boolean isExpiredForRefresh(Node<?, ?> node) {
long now = ticker.read();
return (now - node.getWriteTime()) >= refreshAfterWriteNanos;
}
重点:
get() 时发生。node == null(即从未访问过或已驱逐),根本不会进入此逻辑!scheduleRefresh(...)void scheduleRefresh(Node<K, V> node, K key, ...) {
executor.execute(() -> {
try {
V newValue = loader.apply(key); // 调用你的 build() 中的 lambda
if (newValue != null) {
put(key, newValue); // 更新缓存,重置 writeTime
}
} catch (Exception e) {
logger.warn("Refresh failed for key: " + key, e);
// 默认保留旧值,不删除!
}
});
}
结论:
策略 | 方法 | 是否主动刷新 | 是否返回旧值 | 适用场景 |
|---|---|---|---|---|
写后过期 | expireAfterWrite(5, MINUTES) | 否 | 否(下次 get 会阻塞重载) | 数据强一致性要求高 |
读后过期 | expireAfterAccess(5, MINUTES) | 否 | 否 | 会话类缓存(如 token) |
写后刷新 | refreshAfterWrite(5, MINUTES) | 否(但异步) | 是(先返回旧值) | 容忍短暂不一致,追求低延迟 |
💡 记住:Caffeine 没有任何策略会启动后台线程主动刷新未访问的 key!
既然 Caffeine 本身不支持主动刷新,我们就“曲线救国”!
get() 触发刷新(推荐)@Component
publicclass CacheRefresher {
privatefinal LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(this::loadData);
privatefinal List<String> MONITORED_KEYS = Arrays.asList("user_1001", "user_1002");
@PostConstruct
public void startRefresher() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
() -> MONITORED_KEYS.forEach(cache::get), // 触发 refresh 检查
0,
4, // 间隔 < 5 分钟,确保及时
TimeUnit.MINUTES
);
}
private String loadData(String key) {
log.info("Loading data for key: {}", key);
return fetchDataFromRemote(key);
}
}
优点:
cache.refresh(key)// 在定时任务中
MONITORED_KEYS.forEach(cache::refresh);
区别:
refresh()不返回值,纯粹触发异步加载。Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES) // 5分钟后可异步刷新
.expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟后强制过期
.build(...);
行为:
适用于:既想低延迟,又怕数据无限陈旧的场景。
纠正:它是“访问触发的异步刷新”,不是 cron job!
纠正:
maximumSize会通过 LRU 驱逐冷数据!长期不访问的 key 会被淘汰。
对业务核心 key(如 VIP 用户),务必用定时任务保活。
.build(key -> {
long start = System.currentTimeMillis();
ApiResultDO<?> result = loadHuangoAppData(key);
log.info("Loaded cache for {} in {}ms", key, System.currentTimeMillis() - start);
return result;
});
// 自定义 executor 捕获异常
.executor(Runnable::run) // 或提交到带监控的线程池
Caffeine 的 refreshAfterWrite 不是一个 bug,而是一种精妙的设计权衡:
“只为需要的数据付出刷新成本” —— 这正是它高性能的秘诀!
作为开发者,我们必须:
下次当你再配置 refreshAfterWrite 时,请记住:它不会自己动,得你去“碰”它一下!