Caffeine Cache基础入门:Java本地缓存之王完全指南
·
你还在用Guava Cache做本地缓存吗?Spring Boot 2.x已经默认使用Caffeine作为缓存组件!作为号称**“本地缓存之王”**的高性能Java缓存库,Caffeine在性能上比Guava Cache提升数倍。本文将从基础入门到高级特性,手把手教你掌握Caffeine的使用,包括同步/异步加载、多种淘汰策略、引用驱逐、状态统计等核心功能,附带完整实战代码!
📋 文章目录
一、Caffeine简介
1.1 什么是Caffeine?
Caffeine Cache 是一个高性能的Java本地缓存库,以其卓越的性能和可扩展性赢得了 “本地缓存之王” 的称号。
1.2 与Spring Boot的关系
- Spring Boot 1.x:默认本地缓存是 Guava Cache
- Spring Boot 2.x:官方放弃Guava Cache,改用性能更优秀的 Caffeine 作为默认缓存组件
1.3 性能对比
官方测试报告:https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN

二、Caffeine八大核心特点
- 自动加载:自动将数据加载到缓存中,支持异步加载
- 灵活淘汰策略:基于频次、最近访问、最大容量
- 智能过期:根据上次访问/写入决定过期设置
- 异步清理:缓存过期后自动异步清理
- 内存友好:考虑JVM内存管理,支持弱引用、软引用
- 清除通知:缓存被清理后收到相关通知
- 外部存储:缓存写入可传播到外部存储
- 统计功能:访问次数、命中率、清理个数等统计数据
三、快速入门
3.1 Maven依赖
<!-- Spring Boot Cache Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
3.2 基础使用
Cache是核心接口,通过Caffeine类来构建实现:
public static void basicCache() throws Exception {
// 构建Caffeine实例
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100) // 设置最大缓存数量
.expireAfterAccess(3L, TimeUnit.SECONDS) // 3秒无访问失效
.build(); // 构建Cache实例
// 写入缓存
cache.put("mca", "www.mashibing.com");
cache.put("baidu", "www.baidu.com");
cache.put("spring", "www.spring.io");
// 获取缓存
log.info("获取缓存[getIfPresent]: mca={}", cache.getIfPresent("mca"));
TimeUnit.SECONDS.sleep(5); // 休眠5秒
// 再次获取(已过期)
log.info("获取缓存[getIfPresent]: mca={}", cache.getIfPresent("mca"));
}
运行效果:
获取缓存[getIfPresent]: mca=www.mashibing.com
获取缓存[getIfPresent]: mca=null
注意:
getIfPresent()对于不存在的key立即返回null,不会阻塞。
四、缓存加载机制
4.1 同步加载(CacheLoader)
当缓存失效时,自动同步加载数据:
public static void loadingCache() throws Exception {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(3L, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
log.info("正在重新加载数据... key={}", key);
TimeUnit.SECONDS.sleep(1);
return key.toUpperCase();
}
});
// 写入缓存
cache.put("mca", "www.mashibing.com");
cache.put("baidu", "www.baidu.com");
TimeUnit.SECONDS.sleep(5); // 等待缓存过期
// 获取所有key对应的值(触发自动加载)
List<String> keys = Arrays.asList("mca", "baidu");
Map<String, String> map = cache.getAll(keys);
map.forEach((k, v) ->
log.info("缓存的键: {}, 值: {}", k, v));
}
特点:
- 使用
CacheLoader接口标准化加载流程 - 支持
getAll()批量加载 - 加载过程是同步阻塞的
4.2 异步加载(AsyncLoadingCache)
当多个缓存同时失效时,使用异步加载提高效率:
public static void asyncLoadingCache() throws Exception {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(3L, TimeUnit.SECONDS)
.buildAsync(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
log.info("正在重新加载数据... key={}", key);
TimeUnit.SECONDS.sleep(1);
return key.toUpperCase();
}
});
// 异步缓存的值被CompletableFuture包裹
cache.put("mca", CompletableFuture.completedFuture("www.mashibing.com"));
cache.put("baidu", CompletableFuture.completedFuture("www.baidu.com"));
TimeUnit.SECONDS.sleep(5); // 等待缓存过期
// 批量获取(异步执行,并行加载)
List<String> keys = Arrays.asList("mca", "baidu");
Map<String, String> map = cache.getAll(keys).get();
map.forEach((k, v) ->
log.info("缓存的键: {}, 值: {}", k, v));
}
特点:
- 返回
CompletableFuture包裹的结果 - 多个key并行加载,提高效率
- 接口层级:
AsyncCache与Cache同级
五、缓存淘汰策略
Caffeine提供三类驱逐策略:基于大小、基于时间、基于引用。
5.1 基于大小淘汰
最大容量(maximumSize)
public static void expireMaxType() throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1) // 最大容量为1
.expireAfterAccess(3L, TimeUnit.SECONDS)
.build();
cache.put("name", "张三");
cache.put("age", "18"); // 触发淘汰,"name"被清除
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(cache.getIfPresent("name")); // null
System.out.println(cache.getIfPresent("age")); // 18
}
最大权重(maximumWeight)
public static void expireWeigherType() throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumWeight(100) // 最大权重100
.weigher((key, value) -> {
// 自定义权重计算
if (key.equals("age")) {
return 30;
}
return 50;
})
.build();
cache.put("name", "张三"); // 权重50
cache.put("age", "18"); // 权重30
cache.put("sex", "男"); // 权重50,总权重130>100,触发淘汰
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(cache.getIfPresent("name")); // null(被清除)
System.out.println(cache.getIfPresent("age")); // 18
System.out.println(cache.getIfPresent("sex")); // 男
}
注意:
maximumSize和maximumWeight只能二选一。
5.2 基于时间淘汰
最后一次访问后过期
public static void expireAfterAccess() throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(1L, TimeUnit.SECONDS) // 1秒无访问过期
.build();
cache.put("name", "张三");
for (int i = 0; i < 10; i++) {
System.out.println("第" + i + "次读:" + cache.getIfPresent("name"));
TimeUnit.SECONDS.sleep(2); // 每次间隔2秒,会过期
}
}
最后一次写入后过期
public static void expireAfterWrite() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1L, TimeUnit.SECONDS) // 写入后1秒过期
.build();
cache.put("name", "张三");
for (int i = 0; i < 10; i++) {
System.out.println("第" + i + "次读:" + cache.getIfPresent("name"));
TimeUnit.SECONDS.sleep(1);
}
}
自定义过期策略
public static void customExpire() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(String key, String value, long currentTime) {
// 创建后2秒失效
return TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS);
}
@Override
public long expireAfterUpdate(String key, String value,
long currentTime, long currentDuration) {
// 更新后5秒失效
return TimeUnit.NANOSECONDS.convert(5, TimeUnit.SECONDS);
}
@Override
public long expireAfterRead(String key, String value,
long currentTime, long currentDuration) {
// 读取后100秒失效
return TimeUnit.NANOSECONDS.convert(100, TimeUnit.SECONDS);
}
})
.build();
cache.put("name", "张三");
// ...
}
5.3 基于引用淘汰
软引用(Soft Reference)
public static void expireSoft() throws InterruptedException {
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.softValues() // 使用软引用
.build();
cache.put("name", new byte[1024 * 1024 * 5]); // 5MB数据
System.out.println("第1次读:" + cache.getIfPresent("name"));
// 模拟OOM
List<byte[]> list = new LinkedList<>();
try {
for (int i = 0; i < 100; i++) {
list.add(new byte[1024 * 1024 * 1]); // 1M对象
}
} catch (Throwable e) {
TimeUnit.SECONDS.sleep(1);
System.out.println("OOM时读:" + cache.getIfPresent("name")); // null
}
}
JVM参数:
-Xms20m -Xmx20m
弱引用(Weak Reference)
public static void expireWeak() throws InterruptedException {
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(100)
.weakValues() // 使用弱引用
.build();
cache.put("name", new WeakReference<>("张三"));
System.out.println("第1次读:" + cache.getIfPresent("name"));
System.gc(); // 触发GC
System.out.println("GC后读:" + cache.getIfPresent("name")); // null
}
六、状态统计与监控
6.1 开启统计功能
public static void cacheStats() throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(2)
.recordStats() // 开启统计功能
.expireAfterAccess(200L, TimeUnit.SECONDS)
.build();
cache.put("name", "张三");
cache.put("sex", "男");
cache.put("age", "18");
// 模拟查询
String[] keys = new String[]{"name", "age", "sex", "phone", "school"};
for (int i = 0; i < 1000; i++) {
cache.getIfPresent(keys[new Random().nextInt(keys.length)]);
}
// 获取统计数据
CacheStats stats = cache.stats();
System.out.println("用户请求查询总次数:" + stats.requestCount());
System.out.println("命中个数:" + stats.hitCount());
System.out.println("命中率:" + stats.hitRate());
System.out.println("未命中次数:" + stats.missCount());
System.out.println("未命中率:" + stats.missRate());
System.out.println("加载次数:" + stats.loadCount());
System.out.println("总共加载时间:" + stats.totalLoadTime());
System.out.println("平均加载时间(纳秒):" + stats.averageLoadPenalty());
System.out.println("加载失败率:" + stats.loadFailureRate());
System.out.println("加载失败次数:" + stats.loadFailureCount());
System.out.println("加载成功次数:" + stats.loadSuccessCount());
System.out.println("被淘汰数据总个数:" + stats.evictionCount());
System.out.println("被淘汰数据总权重:" + stats.evictionWeight());
}
6.2 自定义状态收集器
public class MyStatsCounter implements StatsCounter {
@Override
public void recordHits(int count) {
System.out.println("命中次数:" + count);
}
@Override
public void recordMisses(int count) {
System.out.println("未命中次数:" + count);
}
@Override
public void recordLoadSuccess(long loadTime) {
System.out.println("加载成功,耗时:" + loadTime);
}
@Override
public void recordLoadFailure(long loadTime) {
System.out.println("加载失败,耗时:" + loadTime);
}
@Override
public void recordEviction() {
System.out.println("缓存被淘汰");
}
@Override
public CacheStats snapshot() {
return null;
}
}
七、清除与更新监听
缓存数据更新或被清除时触发监听器(异步执行):
public static void removalListener() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(2)
.removalListener((key, value, cause) -> {
System.out.println("键:" + key +
" 值:" + value +
" 清除原因:" + cause);
})
.expireAfterAccess(1, TimeUnit.SECONDS)
.build();
cache.put("name", "张三");
cache.put("sex", "男");
cache.put("age", "18"); // 触发淘汰,"name"被清除
TimeUnit.SECONDS.sleep(2);
cache.put("name2", "张三");
cache.put("age2", "18");
cache.invalidate("age2"); // 手动清除
TimeUnit.SECONDS.sleep(10);
}
清除原因(RemovalCause):
EXPLICIT:手动清除(调用invalidate())REPLACED:被替换(put相同key)COLLECTED:被垃圾收集器回收EXPIRED:过期SIZE:超过容量限制
八、完整配置速查表
| 配置项 | 说明 | 示例 |
|---|---|---|
initialCapacity |
初始缓存空间大小 | .initialCapacity(100) |
maximumSize |
最大缓存条数 | .maximumSize(1000) |
maximumWeight |
最大权重 | .maximumWeight(10000) |
expireAfterAccess |
最后一次访问后过期 | .expireAfterAccess(5, TimeUnit.MINUTES) |
expireAfterWrite |
最后一次写入后过期 | .expireAfterWrite(10, TimeUnit.MINUTES) |
refreshAfterWrite |
定时刷新 | .refreshAfterWrite(1, TimeUnit.MINUTES) |
weakKeys |
Key使用弱引用 | .weakKeys() |
weakValues |
Value使用弱引用 | .weakValues() |
softValues |
Value使用软引用 | .softValues() |
recordStats |
开启统计 | .recordStats() |
removalListener |
清除监听器 | .removalListener((k, v, c) -> {...}) |
关键词:Caffeine Cache, Java本地缓存, Spring Boot缓存, 缓存淘汰策略, 软引用, 弱引用, 缓存统计, 异步加载, 同步加载, CacheLoader
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何Caffeine使用问题,欢迎在评论区留言讨论。
更多推荐
所有评论(0)