⭐ 推荐:社区规则条款 V1.0

如何用 Caffeine 构建 Java 高性能本地缓存?

GJ
GJ 资深会员 👑黑卡会员
发布于 2026-10-26 03:15 ·2 浏览 ·0 回复
内容摘要

介绍用 Caffeine 构建 Java 本地缓存的完整步骤,包括依赖选择、手动缓存、LoadingCache 自动回源、过期刷新与权重策略,并提示 null 值、异常处理及避免与 Guava 混用等注意事项。

学完这篇你能得到一个可以直接跑起来的 Caffeine 本地缓存:从加依赖、建缓存、配过期策略到接进 Spring Boot,每一步都有具体代码和参数。

Caffeine 是 Java 生态里目前综合表现最好的本地缓存库,它的淘汰算法基于 Window TinyLFU,命中率通常比 Guava Cache 更高。下面按步骤来。

第一步:确认 JDK 版本并引入依赖

这一步要拿到可用的 Caffeine jar 包,得到的结果是 pom.xml 里多一个能编译通过的依赖。

Caffeine 3.x 要求 JDK 11 及以上。如果你还在用 JDK 8,只能用 2.9.3 这个最后的 2.x 版本。

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

注意:不要同时引入 Guava 的 Cache 和 Caffeine 后混用,两者的 null 处理和并发语义不一样,容易排查半天。

第二步:建一个最基础的手动缓存

这一步要得到一个能 put / get 的 Cache 对象,结果是你能在业务代码里手动读写缓存。

Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build();

cache.put("k1", "v1");
String v = cache.getIfPresent("k1");     // 没有则返回 null
String v2 = cache.get("k2", k -> load(k)); // 没有则用函数加载

maximumSize(10_000) 表示最多存 1 万个条目,超了按算法淘汰。

注意:Caffeine 不允许 null 作为 key 或 value。缓存里要表达"不存在",用 Optional 包装,或者干脆不写进缓存。

第三步:改用 LoadingCache 自动回源

这一步要把"查不到就去数据库取"的逻辑交给缓存框架,结果是业务代码只写一行 get。

LoadingCache<Long, User> userCache = Caffeine.newBuilder()
        .maximumSize(5_000)
        .build(id -> userMapper.selectById(id));

User u = userCache.get(1001L);

同样的写法还有一个异步版本 buildAsync,返回 AsyncCache,配合 CompletableFuture 用在高并发接口里更合适。

注意:CacheLoader 里抛异常,get 会直接抛出 CompletionException,记得在业务层捕获,否则一个数据库抖动会把整个接口打挂。

第四步:配置过期与刷新策略

这一步要让缓存内容有生命周期,结果是数据不会永久陈旧。常用参数有四个:

  • expireAfterWrite(Duration.ofMinutes(10)):写入 10 分钟后过期,最常用。
  • expireAfterAccess(...):最后一次访问后多久过期,适合热点数据。
  • refreshAfterWrite(...):到期后异步刷新,访问时先返回旧值,不会阻塞读。
  • maximumWeight(...) + weigher(...):按内存权重限制,适合 value 大小差异大的场景。
Caffeine.newBuilder()
        .maximumSize(5_000)
        .expireAfterWrite(Duration.ofMinutes(10))
        .refreshAfterWrite(Duration.ofMinutes(2))
        .build(id -> userMapper.selectById(id));

注意:maximumSize 和 maximumWeight 只能二选一,同时设置启动时会直接报 IllegalStateException。

第五步:打开统计,看命中率

这一步要拿到命中率数据,结果是你能判断缓存到底有没有起作用。在 build() 之前加 recordStats():

Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .recordStats()
        .build();
CacheStats stats = cache.stats();
System.out.println(stats.hitRate());     // 命中率
System.out.println(stats.evictionCount()); // 淘汰数

注意:recordStats() 有额外开销,压测环境或生产排查时再开,长期全量开启要评估一下。

第六步:接入 Spring Boot

这一步要把 Caffeine 变成 @Cacheable 的底层实现,结果是注解就能用缓存。

  1. 引入 spring-boot-starter-cache 和上面的 caffeine 依赖。
  2. 启动类加 @EnableCaching。
  3. 在 application.yml 里配置:
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=10m
  1. 方法上加 @Cacheable(cacheNames = "user", key = "#id")。

如果需要每 2 分钟刷新一次这种 spec 表达不了的配置,就自己写一个 @Bean CaffeineCacheManager,用 Caffeine.newBuilder() 逐项配置。

第七步:几个容易翻车的点

  • 缓存对象要不可变。Caffeine 存的是引用,外面改了字段,缓存里就跟着变,排查起来非常痛苦。
  • 本地缓存不跨 JVM。多实例部署时各存各的,数据一致性要靠过期时间或消息广播兜底。
  • 别拿它当分布式缓存。跨节点共享数据请用 Redis,Caffeine 的定位是进程内的一级缓存。

小结

  • 依赖坐标 com.github.ben-manes.caffeine:caffeine:3.1.8,3.x 需 JDK 11+。
  • 手动缓存用 build(),自动回源用 build(CacheLoader),异步用 buildAsync。
  • 过期用 expireAfterWrite,异步刷新用 refreshAfterWrite,二者可叠加。
  • maximumSize 与 maximumWeight 互斥,null key/value 不被允许。
  • 加 recordStats() 才能看命中率,Spring Boot 用 spring.cache.caffeine.spec 一行配置搞定。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2561.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~