如何用 Caffeine 构建 Java 高性能本地缓存?
介绍用 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 的底层实现,结果是注解就能用缓存。
- 引入
spring-boot-starter-cache和上面的 caffeine 依赖。 - 启动类加
@EnableCaching。 - 在
application.yml里配置:
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
- 方法上加
@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一行配置搞定。
转载请注明出处,版权归原作者所有。