是什么
Caffeine 是 Java 的高性能本地缓存库。它本质上是JVM 内存中的 Key-Value 缓存,类似 Map<K, V>。
Map<Long, User> map = new HashMap<>(); 有哪些不足
- 不会自动过期,数据永远存在。
- 没有容量控制,内存可能爆炸
- 多线程问题,并发不安全
- 热点数据和冷数据混在一起,没有淘汰机制
- 无法自动加载。每次都要手写逻辑:
if (map.get(id) == null)
最核心的三个类型:
| 类型 | 作用 |
|---|---|
| Cache<K,V> | 手动缓存 |
| LoadingCache<K,V> | 自动加载缓存 |
| AsyncLoadingCache<K,V> | 异步缓存 |
引入依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
第一个 Caffeine 示例
package com.example.java_learn.Caffeine;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@SpringBootTest
public class caffeine1 {
@Test
public void testCaffeine() {
Cache<String, String> cache =
Caffeine.newBuilder()
.build();
// put 放入缓存
cache.put("name", "Tom");
// get 从缓存中获取
String value = cache.getIfPresent("name");
// 打印缓存值
System.out.println(value); // Tom
}
}
Caffeine的创建
Caffeine 的 API 设计非常统一,核心流程永远是:
Caffeine.newBuilder()
.配置各种策略()
.build();
// 或者
.build(loader)
Cache手动缓存
常见API
/*
Caffeine.newBuilder() : 所有配置都从这里开始。它返回Caffeine<Object, Object>,然后链式调用。
build(): 真正创建缓存对象
*/
Cache<String, String> cache =Caffeine.newBuilder().build();
cache.put("name", "Tom"); // 写入缓存:key -> value
String value = cache.getIfPresent("name"); // 读取缓存。如果不存在返回 null
cache.invalidate("name"); // 删除缓存。
cache.invalidateAll();// 清空缓存。
long size = cache.estimatedSize(); // 估算缓存大小。
get(key, mappingFunction)
在 Caffeine 中 cache.get(...) 不是普通Map.get(),它实际上是 缓存读取 + 缓存加载 + 并发控制 三者合一。
| 方法 | 含义 |
|---|---|
getIfPresent(key) |
只查缓存 |
get(key, loader) |
不存在则自动加载 |
语法结构如下:
V value = cache.get(
key,
k -> loadData(k)
);
| 部分 | 含义 |
|---|---|
key |
缓存 key |
k -> xxx |
数据加载函数mappingFunction |
返回值 |
最终缓存值 |
Caffeine 内部逻辑:
先查缓存
↓
存在 -> 直接返回
不存在 -> 执行 lambda
↓
返回数据
↓
自动写入缓存
↓
返回结果
@Test
public void testCaffeine5() throws InterruptedException {
Cache<Long, String> cache =Caffeine.newBuilder().build();
// 第一次
String v1 = cache.get(
1L,
key -> {
System.out.println("查询数据库");
return "Tom";
});
System.out.println(v1);
// 第二次
String v2 = cache.get(
1L,
key -> {
System.out.println("查询数据库");
return "Tom";
});
System.out.println(v2);
}
/*
查询数据库
Tom
Tom : 第二次没有查询数据库,直接从缓存中获取
*/
为什么必须推荐 get(key, loader):
因为它解决缓存击穿问题
突然1000 个请求同时进来,这个时候缓存过期
String value = cache.getIfPresent(key); if (value == null) { value = queryDb(); // 查询数据库 cache.put(key, value); }这个时候,1000 个线程同时查数据库,数据库直接被打爆。
cache.get(key, k -> queryDb());但是Caffeine 内部保证同一个 key 只加载一次。内部是只有1个线程查询数据库,其他线程等待。
loader 不可以返回 null
cache.get( 1L, k -> null );上面代码会抛异常。
因为 Caffeine 默认不允许缓存 null,否则无法区分是真没数据,还是缓存了 null
getAll()
getAll():批量获取。
Map<K, V> map = cache.getAll(keys, mappingFunction);
getAll()可以减少数据库的查询
@Test
public void testCaffeine6() throws InterruptedException {
Cache<Long, String> cache =Caffeine.newBuilder().build();
List<Long> ids =
Arrays.asList(1L, 2L, 3L);
Map<Long, String> result = cache.getAll(ids, keys -> {
System.out.println("批量查询数据库");
Map<Long, String> map =
new HashMap<>();
map.put(1L, "Tom");
map.put(2L, "Jack");
map.put(3L, "Lucy");
return map;
});
System.out.println(result); // {1=Tom, 2=Jack, 3=Lucy}
}
配置
maximumSize
maximumSize():设置最多多少个元素,防止缓存无限增长,最终OOM
@Test
public void testCaffeine2() throws InterruptedException {
Cache<Integer, String> cache =
Caffeine.newBuilder()
.maximumSize(3)
.build();
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
cache.put(4, "D");
/*
缓存淘汰的时机 :Caffeine的淘汰策略可能不是立即执行的。淘汰可能发生在后续操作中,或者有延迟。
等1秒,让缓存淘汰的时机可以有时间执行
在实际项目中不需要等待1秒,因为会有足够的时间让缓存淘汰的时机执行。
*/
Thread.sleep(1000);
// 打印缓存大小
System.out.println(cache.estimatedSize()); // 3
// 遍历打印所有缓存值
cache.asMap().forEach((key, value) -> System.out.println(key + " -> " + value));
/*
2 -> B
3 -> C
4 -> D
*/
}
expireAfterWrite
expireAfterWrite()写入后过期。
.expireAfterWrite(10, TimeUnit.MINUTES):写入 10 分钟后失效
@Test
public void testCaffeine3() throws InterruptedException {
// 5秒后缓存过期
Cache<String, String> cache =
Caffeine.newBuilder()
.expireAfterWrite(
5,
TimeUnit.SECONDS
)
.build();
cache.put("name", "Tom");
Thread.sleep(6000);
System.out.println(
cache.getIfPresent("name")
); // null
}
expireAfterAccess
expireAfterAccess():访问后过期。
.expireAfterAccess(5, TimeUnit.MINUTES) :5 分钟没人访问 -> 过期
@Test
public void testCaffeine4() throws InterruptedException {
// 5秒后缓存过期
Cache<String, String> cache =
Caffeine.newBuilder()
.expireAfterAccess(
3,
TimeUnit.SECONDS
)
.build();
cache.put("name", "Tom");
Thread.sleep(2000);
System.out.println(cache.getIfPresent("name")); // Tom
Thread.sleep(2000);
// 4秒后,缓存还没有过去,因为前2秒内有访问
System.out.println(cache.getIfPresent("name")); // Tom
Thread.sleep(4000);
// 再过去4秒后,缓存过期
System.out.println(cache.getIfPresent("name")); // null
}
| 策略 | 含义 |
|---|---|
| expireAfterWrite | 写入后固定时间失效 |
| expireAfterAccess | 最后一次访问后失效 |
refreshAfterWrite
refreshAfterWrite():后台刷新。
expire:过期后数据不存在,用户可能等待加载。refresh:缓存写入一段时间后,自动刷新。是“刷新”,不是“立即过期”。- refresh 的核心特点:不会阻塞当前请求
- 先返回旧值,后台异步刷新
.refreshAfterWrite(5, TimeUnit.SECONDS):5 秒后,下次访问时触发后台刷新。并且旧值仍然可以返回。
@Test
public void testCaffeine9() throws InterruptedException {
Cache<Integer, String> cache =
Caffeine.newBuilder()
.maximumSize(1)
.removalListener(
(key, value, cause) -> {
System.out.println(
key + " -> " + value + " 被删除: " + cause
);
}
)
.build();
cache.put(1, "A");
cache.put(2, "B"); // 1 -> A 被删除: SIZE
}
@Test
public void testCaffeine10() throws InterruptedException {
LoadingCache<Long, String> cache =
Caffeine.newBuilder()
// 5秒后允许刷新
.refreshAfterWrite(
5,
TimeUnit.SECONDS
)
.build(id ->{
System.out.println("查询数据库");
Thread.sleep(500); // 模拟数据库查询时间
return "Tom-" + System.currentTimeMillis();
});
// 第一次
System.out.println(cache.get(1L));
Thread.sleep(2000);
// 第二次
System.out.println(cache.get(1L));
Thread.sleep(5000); // 5秒后,缓存过期
// 第三次 :线程不会阻塞,直接从缓存中获取数据(旧值),同时异步更新缓存数据
System.out.println(cache.get(1L));
Thread.sleep(1000);
// 第四次
System.out.println(cache.get(1L));
}
完整时间线如下:
t=0
第一次访问
→ 查数据库
→ 缓存 Tom-1
t=2
第二次访问
→ 直接返回 Tom-1
t=3
第三次访问
→ 返回旧值 Tom-1
→ 后台刷新
t=8
第四次访问
→ 返回新值 Tom-2
recordStats
recordStats():开启统计。
Cache<String, String> cache =
Caffeine.newBuilder()
.recordStats()
.build();
查看统计
System.out.println(cache.stats());
会有类似输出:
CacheStats{
hitCount=10,
missCount=2
}
命中率 :$\text{Hit Rate}=\frac{hitCount}{hitCount+missCount}$
例如95% 意味着 95% 请求不访问数据库
@Test
public void testCaffeine11() throws InterruptedException {
LoadingCache<Long, String> cache =
Caffeine.newBuilder()
// 最大缓存数量
.maximumSize(100)
// 开启统计
.recordStats()
// 10分钟过期
.expireAfterWrite(
10,
TimeUnit.MINUTES
)
// loader
.build(id -> {
System.out.println("查询数据库: " + id);
return "User-" + id;
});
// 第一次访问: 查询数据库,缓存中没有,所以查询数据库,缓存中没有,所以查询数据库
System.out.println(cache.get(1L));
// 第二次访问(命中缓存)
System.out.println(cache.get(1L));
// 第三次访问(新key):查询数据库
System.out.println(cache.get(2L));
// 第四次访问(命中缓存)
System.out.println(cache.get(2L));
// 输出统计信息
System.out.println(cache.stats());
/*
CacheStats{hitCount=2,
missCount=2,
loadSuccessCount=2,
loadFailureCount=0,
totalLoadTime=327865,
evictionCount=0,
evictionWeight=0}
*/
}
hitCount:缓存命中次数,即没查数据库的次数missCount:缓存未命中次数,即需要加载数据的次数loadSuccessCount:成功加载次数,即loader 成功执行的次数loadFailureCount:加载失败次数,例如throw new RuntimeException();totalLoadTime:缓存淘汰次数,例如.maximumSize(100)超限后,导致淘汰缓存的次数evictionWeight:因缓存淘汰(eviction)而被移除的数据“总权重”
removalListener
removalListener():监听被删除的缓存。
语法:
.removalListener(
(key, value, cause) -> {}
)
被删除的原因RemovalCause 有:
| 原因 | 含义 |
|---|---|
| SIZE | 超大小淘汰 |
| EXPIRED | 过期 |
| EXPLICIT | 手动删除 |
| REPLACED | 被替换 |
@Test
public void testCaffeine9() throws InterruptedException {
Cache<Integer, String> cache =
Caffeine.newBuilder()
.maximumSize(1)
.removalListener(
(key, value, cause) -> {
System.out.println(
key + " -> " + value + " 被删除: " + cause
);
}
)
.build();
cache.put(1, "A"); // 1 -> A 被删除: SIZE (由于超过 maximumSize 被删除)
cache.put(2, "B"); // 触发 removalListener
}
LoadingCache
LoadingCache:自动加载缓存。
LoadingCache<Long, User> cache =Caffeine.newBuilder().build(id -> loadUser(id));
cache.get(key, loader):每次 get 时写 loader。
LoadingCache:loader 在 build 时固定。之后cache.get(key)即可。
@Test
public void testCaffeine7() throws InterruptedException {
LoadingCache<Long, String> cache =Caffeine.newBuilder()
.maximumSize(100)
.build(id -> {
System.out.println("查询数据库");
return "User-" + id;
});
// User-1: 第一次查询数据库,缓存中没有,所以查询数据库
System.out.println(cache.get(1L));
// User-1: 第二次查询数据库,缓存中有,所以直接从缓存中获取
System.out.println(cache.get(1L));
}
AsyncLoadingCache
AsyncLoadingCache 异步缓存。
为什么需要异步缓存:因为数据库 / RPC / Redis 查询是 IO,同步等待会阻塞线程
@Test
public void testCaffeine12() throws Exception {
AsyncLoadingCache<Long, String> cache =Caffeine.newBuilder()
.maximumSize(100)
// 创建异步加载器,用于查询数据库
// 要求 return CompletableFuture<String>
.buildAsync(
// id : 缓存key
// executor : Caffeine 提供的线程池,用于执行异步任务
(id, executor) ->
// CompletableFuture :是Java8中引入的异步任务框架,用于处理异步任务的结果和异常
CompletableFuture
// 异步执行工具,用于执行异步任务
/*
CompletableFuture.supplyAsync(
supplier,
executor
)
含义:在线程池里异步执行 supplier 方法,返回 CompletableFuture 对象
*/
.supplyAsync(() -> {
return queryUser(id);
}, executor)
);
System.out.println("开始查询");
// “发起异步查询” : 这里只是发起,不是拿到结果
// 返回 CompletableFuture<String>,表示“未来会完成的结果”
// 这里的流程:查缓存 --> 未命中 --> 提交异步任务, 后台线程池里执行异步任务 --> 立刻返回 Future ,立刻返回 Future
CompletableFuture<String> future = cache.get(1L);
System.out.println("主线程没阻塞");
// “等待异步结果”,直到异步查询完成,才会返回结果
// 可以理解为:“把 Future 里的结果拿出来”
// 也就是等到要使用结果时,才会返回结果
// 如果这个时候异步查询还没有完成,就会阻塞主线程,等待异步查询完成,才会返回结果
String user = future.get();
System.out.println(user); // User-1
}
最常用的实际配置
LoadingCache<Long, User> cache =
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(
10,
TimeUnit.MINUTES
)
.recordStats()
.build(this::queryUser);
| 配置 | 作用 |
|---|---|
| maximumSize | 防 OOM |
| expireAfterWrite | TTL |
| recordStats | 监控 |
| build(loader) | 自动加载 |