目录

    一、Caffeine 是什么?

    二、Caffeine核心特性

    三、Caffeine 基本使用

    核心使用场景示例

    场景 1:基础缓存(Cache,手动存 / 取)

    场景 2:加载式缓存(LoadingCache)

    场景 3:异步缓存(AsyncLoadingCache)

    四、Caffeine 核心原理(W-TinyLFU 算法)

    传统算法的问题:

    LRU 的淘汰逻辑:优先删除 “最久没有被访问过” 的数据

    LFU 的淘汰逻辑:优先删除 “使用次数最少” 的数据

    问题 1:缓存 “历史高频但当前无用” 的数据

    问题 2:对新数据不友好

    W-TinyLFU 的优势

    频率草图

    计数最小草图的工作原理

    数据结构:固定大小的二维数组 + 多个独立哈希函数

    写入频率(Key 被访问时):多哈希映射 + 计数累加

    查询频率(淘汰时判断 Key 热度):取最小值减少误差

    为什么取最小值?

    关键优化:频率衰减(解决 LFU “历史高频数据占坑” 问题)

    为什么高效

    窗口缓存(Window Cache)

    底层实现

    为什么能保护新数据

    分层迁移规则(数据在三层缓存间的流动)


      一、Caffeine 是什么?

      Caffeine 是基于 Java 8 开发的高性能本地缓存库,是目前性能最优的 Java 本地缓存实现(远超 Guava Cache、Ehcache 等)。它借鉴了 Guava Cache 的 API 设计,同时优化了底层的缓存淘汰算法,成为 Spring Boot、MyBatis 等主流框架的默认本地缓存选择。


      二、Caffeine核心特性

      极致性能:基于 W-TinyLFU 淘汰算法,命中率远超传统的 LRU、LFU 算法;

      丰富的缓存策略:支持基于大小、时间(访问后 / 写入后)、引用类型(强 / 软 / 弱引用)的过期 / 淘汰策略;

      异步支持:提供异步加载、异步刷新 API,适配高并发场景;

      兼容 Guava Cache:API 几乎和 Guava Cache 一致,迁移成本极低;

      灵活的加载机制:支持手动加载、自动加载(CacheLoader)、异步加载(AsyncCacheLoader);

      事件监听:支持缓存淘汰、过期、移除的事件监听,便于问题排查。


      三、Caffeine 基本使用

      核心使用场景示例

      场景 1:基础缓存(Cache,手动存 / 取)

       // 1. 创建缓存实例
              Cache<String, String> cache = Caffeine.newBuilder()
                      .maximumSize(100) // 缓存最大容量(超过则触发淘汰)
                      .expireAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟过期
                      .expireAfterAccess(2, TimeUnit.MINUTES) // 访问后2分钟过期(优先级低于write)
                      .build();
      
              // 2. 存入缓存
              cache.put("user:1", "张三");
              cache.put("user:2", "李四");
      
              // 3. 简单获取(不存在返回null)
              String user1 = cache.getIfPresent("user:1");
              System.out.println("获取user:1:" + user1); // 输出:张三
      
              // 4. 智能获取(不存在则执行函数并自动存入)
              String user3 = cache.get("user:3", key -> {
                  System.out.println("查询数据库获取" + key); // 模拟查库
                  return "王五";
              });
              System.out.println("获取user:3:" + user3); // 输出:王五
      
              // 5. 删除缓存
              cache.invalidate("user:2");
              System.out.println("删除后user:2:" + cache.getIfPresent("user:2")); // 输出:null

      get(key, function):Caffeine 最核心的方法,既简化 “查缓存 - 无则查源 - 存缓存” 的逻辑,又能避免并发下的重复查库问题


      场景 2:加载式缓存(LoadingCache)

      // 1. 创建加载式缓存
              LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
                      .maximumSize(1000)
                      .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入后1分钟异步刷新(非过期)
                      .build(key -> {
                          // 缓存不存在时自动执行的加载逻辑
                          System.out.println("从数据库加载:" + key);
                          return "用户-" + key.substring(5); // 如 key=user:10 → 用户-10
                      });
      
              // 2. 获取缓存(自动触发加载)
              String user10 = loadingCache.get("user:10");
              System.out.println(user10); // 输出:用户-10
      
              // 3. 再次获取(直接读缓存,不执行加载逻辑)
              String user10Again = loadingCache.get("user:10");
              System.out.println(user10Again); // 输出:用户-10

      场景 3:异步缓存(AsyncLoadingCache)

      // 1. 创建异步加载缓存
              AsyncLoadingCache<String, String> asyncCache = Caffeine.newBuilder()
                      .maximumSize(1000)
                      .expireAfterWrite(10, TimeUnit.MINUTES)
                      .buildAsync(key -> {
                          // 异步加载逻辑(模拟耗时IO操作)
                          TimeUnit.MILLISECONDS.sleep(100);
                          return "异步用户-" + key.substring(5);
                      });
      
              // 2. 异步获取(返回CompletableFuture)
              CompletableFuture<String> user20Future = asyncCache.get("user:20");
              // 业务中建议用 thenAccept 等非阻塞方式处理结果
              String user20 = user20Future.get();
              System.out.println(user20); // 输出:异步用户-20

      四、Caffeine 核心原理(W-TinyLFU 算法)

      Caffeine 性能优异的核心是它的缓存淘汰算法 ——W-TinyLFU(Weighted TinyLFU),这是对传统 LRU/LFU 的优化:

      传统算法的问题:

      LRU 的淘汰逻辑:优先删除 “最久没有被访问过” 的数据

      缓存 “突发流量的临时数据”,挤占常用数据
      用「秒杀活动」这个典型场景举例:
      正常情况下,你的缓存里存的是用户的 “个人信息、常用商品列表” 这些高频访问的 “常用数据”(就像抽屉里的身份证、常用合同);

      突然来了一场秒杀活动,几百万用户访问 “秒杀商品 A”(突发流量),这个 “秒杀商品 A” 的数据会被频繁访问,挤入缓存;秒杀活动只持续 10 分钟,结束后再也没人访问 “秒杀商品 A” 了,但因为它是 “最近刚被访问过” 的,LRU 不会优先删它;

      当后续有新的常用数据要进缓存时(比如用户的 “收货地址”),缓存已经满了,LRU 只能删掉那些 “很久没访问” 的常用数据(比如用户的个人信息),而 “秒杀商品 A” 这个临时数据却一直占着缓存空间;

      结果就是:常用数据被挤走,用户再访问个人信息时,缓存里没有了,只能重新查数据库,性能下降。

      简单说:LRU 会把 “昙花一现的临时数据” 当成 “重要数据” 留着,反而把真正常用的、只是暂时没被访问的数挤走了。


      LFU 的淘汰逻辑:优先删除 “使用次数最少” 的数据

      问题 1:缓存 “历史高频但当前无用” 的数据

      你公司上个月办了一场大型活动,活动期间 “活动规则文件” 被你翻了 100 次(累计次数极高),是缓存里的 “高频数据”;

      活动结束后,这个文件再也没用了,但因为它的 “累计使用次数” 还是最高的,LFU 不会删它;
      缓存满了之后,哪怕是现在每天都要用的 “日报模板”(累计次数只有 50 次,比活动文件少),也会被 LFU 优先删掉 —— 因为 LFU 只看 “总次数”,不管 “数据现在还有没有用”。

      问题 2:对新数据不友好

      新数据刚进入缓存时,它的 “累计使用次数” 是 0 或极少,很容易被淘汰:

      W-TinyLFU 的优势

      结合 LRU 的 “时间局部性” 和 LFU 的 “频率局部性”;

      用 “频率草图” 统计访问频率(占用内存极少);

      用 “窗口缓存” 接纳新数据,避免新数据被直接淘汰;

      最终通过加权平衡新数据和高频旧数据,最大化命中率。


      频率草图

      解决 LFU 的 “频率统计内存开销大” 问题

      传统 LFU 要为每个 Key 维护一个频率计数器,如果缓存有 100 万个 Key,就需要 100 万个计数器,内存开销巨大;而且 Key 淘汰后计数器还得清理,维护复杂。

      W-TinyLFU 的解决方案:用 “近似统计” 替代 “精确统计”,用极小的内存(默认几 KB)统计所有 Key 的访问频率,允许微小误差。这个 “近似统计工具” 就是(Count-Min Sketch)计数最小草图

      计数最小草图的工作原理

      Count-Min Sketch 是一种 “概率数据结构”,核心是「用多个哈希函数 + 二维数组」实现高效、低内存的频率统计


      数据结构:固定大小的二维数组 + 多个独立哈希函数

      二维数组(称为 “频率矩阵”):int[][] frequencyMatrix,行数 = 哈希函数个数(默认 4 个),列数 = 固定值(比如 1024),数组初始值全为 0;

      哈希函数:4 个独立的哈希函数(h1、h2、h3、h4),输入一个 Key,输出一个数组列索引(0~1023)


      写入频率(Key 被访问时):多哈希映射 + 计数累加

      当一个 Key 被访问时:用 4 个哈希函数分别计算 Key 的哈希值,得到 4 个不同的列索引(比如 h1→100、h2→300、h3→500、h4→700);

      把频率矩阵中这 4 个位置的计数值都 +1

      结束。整个过程是 O (1) 时间复杂度(不管 Key 多少,都只做 4 次哈希 + 4 次累加)。


      查询频率(淘汰时判断 Key 热度):取最小值减少误差

      当需要查询一个 Key 的访问频率时:同样用 4 个哈希函数计算 Key 的 4 个列索引;

      读取频率矩阵中这 4 个位置的计数值(比如分别是 5、3、7、4);

      取这 4 个值的 最小值 作为 Key 的 “近似频率”(这里就是 3)


      为什么取最小值?

      因为不同 Key 可能通过哈希映射到同一个位置(哈希冲突),导致计数值偏高,取最小值能最大程度减少冲突带来的误差。
      (比如 KeyA 和 KeyB 都映射到了位置 100,KeyA 访问 3 次,KeyB 访问 2 次,位置 100 的值是 5)


      关键优化:频率衰减(解决 LFU “历史高频数据占坑” 问题)

      比如活动结束后的旧数据,累计频率高,一直不被淘汰。W-TinyLFU 通过「定期频率衰减」解决:

      每隔一段时间(比如 1 分钟),对频率矩阵中所有计数值执行「右移 1 位」操作(相当于除以 2,向下取整);
      例如:一个 Key 的近似频率是 8,衰减后变成 4;再衰减一次变成 2,再衰减变成 1,最后变成 0;

      效果:历史高频数据的频率会逐渐 “降温”,即使后续不被访问,最终会变成低频数据,被淘汰出局,给新数据和当前高频数据腾空间。


      为什么高效

      内存开销极小:4 行 × 1024 列的 int 数组,仅占用 4×1024×4Byte=16KB 内存,与缓存中 Key 的数量无关(哪怕缓存有 100 万 Key,还是 16KB)

      时间复杂度 O (1):写入和查询频率都只需要执行固定次数的哈希和数组操作,无锁且高效;

      误差可接受:缓存的核心目标是 “提升命中率”,不是 “精确统计频率”,微小的频率误差对命中率影响可忽略。


      窗口缓存(Window Cache)

      设计目标:解决 LFU 的 “新数据歧视” 问题

      W-TinyLFU 的解决方案:单独开辟一个 “窗口缓存”,专门接纳新数据,给新数据一个 “观察期”,避免刚进来就被淘汰


      底层实现

      W-TinyLFU 分层存储

      容量设计:占总缓存的 1%~5%(默认 1%)

      容量小的原因:窗口缓存只需要 “临时接纳新数据”,不需要长期存储,避免占用过多内存

      核心规则:新数据优先进入窗口缓存,满了才迁移

      新数据写入缓存时,首先进入窗口缓存(不管频率高低);

      当窗口缓存满了,再写入新数据时,会把窗口缓存中「最久没访问」的旧数据迁移到下一层(Probationary Cache,试用缓存);

      窗口缓存中的数据被访问时,不会直接升级到 Protected Cache(受保护缓存),而是继续留在窗口缓存,直到被迁移到 Probationary Cache 后,再通过访问升级。


      为什么能保护新数据

      新数据进入窗口缓存后,不会因为频率低被淘汰(淘汰只发生在试用缓存);

      窗口缓存的淘汰规则是「LRU」(最久没访问),保证新数据至少有 “一段时间” 留在缓存中,有机会被多次访问,积累频率;
      例如:新业务流程文件进入窗口缓存后,你每天都访问它,窗口缓存满了之后,它会被迁移到试用缓存,此时它的频率已经积累到一定程度,不会被轻易淘汰,甚至能升级到受保护缓存。

      加权平衡新数据与高频旧数据

      通过「分层迁移规则」和「淘汰决策规则」实现


      分层迁移规则(数据在三层缓存间的流动)
      数据状态 迁移触发条件 迁移/淘汰规则 迁移方向
      新数据 写入缓存 直接进入 直接进入窗口缓存
      窗口缓存数据 窗口缓存满,写入新数据 最久未访问 窗口缓存 → 试用缓存
      试用缓存数据 被访问一次 直接进入 试用缓存 → 受保护缓存
      受保护缓存数据 保护缓存满,需腾出空间 最久未访问 受保护缓存 → 试用缓存
      试用缓存数据 缓存总容量满,需淘汰 低频优先淘汰,频率一样,最久未访问 直接淘汰

      Logo

      汇聚全球AI编程工具,助力开发者即刻编程。

      更多推荐