首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Caffeine 缓存“不刷新”?90% 的人都用错了 refreshAfterWrite!--搞懂 Caffeine 的“惰性刷新”机制,避免线上缓存失效事故!

Caffeine 缓存“不刷新”?90% 的人都用错了 refreshAfterWrite!--搞懂 Caffeine 的“惰性刷新”机制,避免线上缓存失效事故!

作者头像
nobody-nobody
发布2026-03-16 21:13:37
发布2026-03-16 21:13:37
2340
举报
文章被收录于专栏:nobodynobody

引言:一个看似简单的缓存配置,为何“失灵”了?

在微服务架构中,缓存是提升性能、降低数据库压力的利器。Google 开源的 Caffeine 凭借高性能、易用性,成为 Java 生态中最受欢迎的本地缓存库之一。

但最近,一位同事向我求助:

“我明明设置了 .refreshAfterWrite(5, TimeUnit.MINUTES),为什么数据没有每 5 分钟自动刷新?

这个问题看似简单,实则触及了 Caffeine 设计哲学的核心——“懒加载 + 按需刷新”。今天,我们就从 源码剖析 + 实战案例 + 正确姿势 三个维度,彻底揭开 refreshAfterWrite 的神秘面纱!

一、问题复现:你以为的“定时刷新”,其实是“条件刷新”

1.1 常见错误写法

代码语言:javascript
复制
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,数据永远不会刷新!

1.2 真实案例:用户画像缓存“冻结”

某电商系统使用 Caffeine 缓存用户行为分析结果(如“近7天点击商品数”)。 配置了 refreshAfterWrite(5, MINUTES),希望每 5 分钟更新一次用户画像。

结果

  • 活跃用户的数据能及时更新(因为频繁访问)。
  • 沉默用户(如30分钟未登录)的数据永远停留在第一次加载的状态!
  • 运营活动基于过期数据推送,导致转化率暴跌。

根本原因:开发者误以为 refreshAfterWrite 是“定时任务”,而它其实是“触发式刷新”。

二、源码深挖:Caffeine 到底如何实现 refreshAfterWrite?

我们以 Caffeine 3.x 源码为基础(GitHub - ben-manes/caffeine),追踪关键路径。

2.1 缓存读取入口:LocalCache.get(K key)

当调用 cache.get("key") 时,最终会进入 BoundedLocalCachecomputeIfAbsent 方法。

关键逻辑如下(简化版):

代码语言:javascript
复制
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(); // ⚡️ 先返回旧值!
  }
  // 首次加载...
}

2.2 刷新判断:isExpiredForRefresh(Node)

代码语言:javascript
复制
boolean isExpiredForRefresh(Node<?, ?> node) {
  long now = ticker.read();
  return (now - node.getWriteTime()) >= refreshAfterWriteNanos;
}

重点

  • 刷新检查 只在 get() 时发生
  • 如果 node == null(即从未访问过或已驱逐),根本不会进入此逻辑

2.3 异步刷新:scheduleRefresh(...)

代码语言:javascript
复制
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);
      // 默认保留旧值,不删除!
    }
  });
}

结论

  • 刷新是异步的 → 不阻塞主线程。
  • 刷新依赖 get 触发 → 无访问=无刷新。
  • 刷新失败保留旧值 → 避免缓存穿透。

三、三大缓存策略对比:别再混淆 expire 和 refresh!

策略

方法

是否主动刷新

是否返回旧值

适用场景

写后过期

expireAfterWrite(5, MINUTES)

否(下次 get 会阻塞重载)

数据强一致性要求高

读后过期

expireAfterAccess(5, MINUTES)

会话类缓存(如 token)

写后刷新

refreshAfterWrite(5, MINUTES)

否(但异步)

是(先返回旧值)

容忍短暂不一致,追求低延迟

💡 记住:Caffeine 没有任何策略会启动后台线程主动刷新未访问的 key!

四、正确姿势:如何实现“真正的定时刷新”?

既然 Caffeine 本身不支持主动刷新,我们就“曲线救国”!

方案 1:定期 get() 触发刷新(推荐)

代码语言:javascript
复制
@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);
  }
}

优点

  • 利用 Caffeine 原生机制,安全可靠。
  • 返回旧值期间,用户体验无感知。

方案 2:直接调用 cache.refresh(key)

代码语言:javascript
复制
// 在定时任务中
MONITORED_KEYS.forEach(cache::refresh);

区别

  • refresh()不返回值,纯粹触发异步加载。
  • 不会因缓存未命中而同步加载(更轻量)。

方案 3:组合策略 —— refresh + expire 双保险

代码语言:javascript
复制
Caffeine.newBuilder()
    .refreshAfterWrite(5, TimeUnit.MINUTES)  // 5分钟后可异步刷新
    .expireAfterWrite(10, TimeUnit.MINUTES)  // 10分钟后强制过期
    .build(...);

行为

  • 5~10 分钟:访问时返回旧值 + 后台刷新。
  • 10 分钟:数据真正过期,下次访问同步重载。

适用于:既想低延迟,又怕数据无限陈旧的场景。

五、避坑指南:常见误区与最佳实践

误区 1:“refreshAfterWrite = 定时任务”

纠正:它是“访问触发的异步刷新”,不是 cron job!

误区 2:“不设置 expire,数据会永久缓存”

纠正maximumSize 会通过 LRU 驱逐冷数据!长期不访问的 key 会被淘汰。

最佳实践 1:关键数据必须主动维护

对业务核心 key(如 VIP 用户),务必用定时任务保活。

最佳实践 2:监控刷新日志

代码语言:javascript
复制
.build(key -> {
  long start = System.currentTimeMillis();
  ApiResultDO<?> result = loadHuangoAppData(key);
  log.info("Loaded cache for {} in {}ms", key, System.currentTimeMillis() - start);
  return result;
});

最佳实践 3:处理刷新异常

代码语言:javascript
复制
// 自定义 executor 捕获异常
.executor(Runnable::run) // 或提交到带监控的线程池

六、结语:理解设计哲学,才能用好工具

Caffeine 的 refreshAfterWrite 不是一个 bug,而是一种精妙的设计权衡

“只为需要的数据付出刷新成本” —— 这正是它高性能的秘诀!

作为开发者,我们必须:

  1. 认清机制本质:刷新 = 访问 + 超时。
  2. 主动管理关键数据:用定时任务“唤醒”沉默的缓存。
  3. 监控 + 日志:让缓存行为可观测。

下次当你再配置 refreshAfterWrite 时,请记住:它不会自己动,得你去“碰”它一下!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 认知科技技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:一个看似简单的缓存配置,为何“失灵”了?
  • 一、问题复现:你以为的“定时刷新”,其实是“条件刷新”
    • 1.1 常见错误写法
    • 1.2 真实案例:用户画像缓存“冻结”
  • 二、源码深挖:Caffeine 到底如何实现 refreshAfterWrite?
    • 2.1 缓存读取入口:LocalCache.get(K key)
    • 2.2 刷新判断:isExpiredForRefresh(Node)
    • 2.3 异步刷新:scheduleRefresh(...)
  • 三、三大缓存策略对比:别再混淆 expire 和 refresh!
  • 四、正确姿势:如何实现“真正的定时刷新”?
    • 方案 1:定期 get() 触发刷新(推荐)
    • 方案 2:直接调用 cache.refresh(key)
    • 方案 3:组合策略 —— refresh + expire 双保险
  • 五、避坑指南:常见误区与最佳实践
    • 误区 1:“refreshAfterWrite = 定时任务”
    • 误区 2:“不设置 expire,数据会永久缓存”
    • 最佳实践 1:关键数据必须主动维护
    • 最佳实践 2:监控刷新日志
    • 最佳实践 3:处理刷新异常
  • 六、结语:理解设计哲学,才能用好工具
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档