找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2545

积分

0

好友

369

主题
发表于 17 小时前 | 查看: 2| 回复: 0

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

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

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

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

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

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

1.1 常见错误写法

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 方法。

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

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)

boolean isExpiredForRefresh(Node<?, ?> node) {
  long now = ticker.read();
  return (now - node.getWriteTime()) >= refreshAfterWriteNanos;
}

重点

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

2.3 异步刷新: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);
      // 默认保留旧值,不删除!
    }
  });
}

结论

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

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

策略 方法 是否主动刷新 是否返回旧值 适用场景
写后过期 expireAfterWrite(5, MINUTES) 否(下次 get 会阻塞重载) 数据强一致性要求高
读后过期 expireAfterAccess(5, MINUTES) 会话类缓存(如 token)
写后刷新 refreshAfterWrite(5, MINUTES) 否(但异步) 是(先返回旧值) 容忍短暂不一致,追求低延迟

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

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

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

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

@Component
public class CacheRefresher {

  private final LoadingCache<String, String> cache = Caffeine.newBuilder()
      .maximumSize(100)
      .refreshAfterWrite(5, TimeUnit.MINUTES)
      .build(this::loadData);

  private final 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)

// 在定时任务中
MONITORED_KEYS.forEach(cache::refresh);

区别

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

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

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:监控刷新日志

.build(key -> {
  long start = System.currentTimeMillis();
  ApiResultDO<?> result = loadHuangoAppData(key);
  log.info(“Loaded cache for {} in {}ms“, key, System.currentTimeMillis() - start);
  return result;
});

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

// 自定义 executor 捕获异常
.executor(Runnable::run) // 或提交到带监控的线程池

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

Caffeine 的 refreshAfterWrite 不是一个 bug,而是一种精妙的设计权衡,关于缓存策略的更深入讨论可以参考相关设计文档:

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

作为开发者,我们必须:

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

下次当你再配置 refreshAfterWrite 时,请记住:它不会自己动,得你去“碰”它一下! 更多关于数据库与中间件的实战技巧和案例,欢迎在云栈社区交流探讨。对于框架的深入理解,仔细阅读官方文档总是最可靠的第一步。




上一篇:2026年云服务器选购指南:99元起续费同价,个人与企业高性价比之选
下一篇:5G基站功率放大器效率提升技术:APT与ET结合DC/DC调节器降低40%能耗
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 18:39 , Processed in 0.300063 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表