一、Caffeine 简介
Caffeine 是一个高性能的 Java 缓存库,提供了接近最佳的命中率。它是 Google Guava Cache 的改进版本,广泛应用于需要高效缓存的场景。Caffeine 的设计目标是提供灵活、线程安全的缓存机制,支持多种淘汰策略和配置。
主要特性
- 高性能:基于 ConcurrentHashMap 实现,提供了极高的并发性能。
- 多种淘汰策略:
- 基于大小的淘汰(maximum size)。
- 基于时间的淘汰(expire after access/write)。
- 基于引用(弱引用、软引用)。
- 异步加载:支持异步缓存加载,适合高并发场景。
- 统计功能:提供缓存命中率、加载时间等统计信息。
- 轻量级:API 简洁,易于集成。
二、Caffeine 的核心概念
- Cache:基本的缓存接口,支持手动加载数据。
- LoadingCache:自动加载缓存,当缓存未命中时自动调用加载函数。
- AsyncCache 和 AsyncLoadingCache:异步版本的缓存,适合需要非阻塞操作的场景。
- Eviction(淘汰):
- Size-based:当缓存大小超过限制时,移除最少使用的条目(LRU)。
- Time-based:基于访问时间或写入时间设置过期。
- Reference-based:使用弱引用或软引用进行垃圾回收。
- Refresh:支持在指定时间后自动刷新缓存条目。
三、Caffeine 的主要 API
-
构建缓存:
- 使用
Caffeine.newBuilder()
创建缓存配置。 - 配置项包括:
maximumSize(long)
:最大缓存条目数。expireAfterAccess(Duration)
:最后访问后多久过期。expireAfterWrite(Duration)
:写入后多久过期。refreshAfterWrite(Duration)
:写入后多久刷新。weakKeys()
/weakValues()
:使用弱引用。recordStats()
:启用统计功能。
- 使用
-
操作缓存:
cache.get(key, mappingFunction)
:获取或加载值。cache.put(key, value)
:手动放入值。cache.invalidate(key)
:移除缓存条目。cache.cleanUp()
:手动触发清理过期条目。
-
统计:
cache.stats()
:获取缓存统计信息(如命中率、平均加载时间)。
四、代码示例
1. 基本缓存使用
以下示例展示如何创建一个简单的 Caffeine 缓存,设置最大大小为 100,访问后 10 秒过期。
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
public class BasicCaffeineExample {
public static void main(String[] args) {
// 创建缓存:最大100条,访问后10秒过期
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterAccess(10, TimeUnit.SECONDS)
.build();
// 放入数据
cache.put("key1", "value1");
// 获取数据
String value = cache.getIfPresent("key1");
System.out.println("key1 的值: " + value);
// 使用 get 方法,miss 时提供默认值
String value2 = cache.get("key2", k -> "defaultValue");
System.out.println("key2 的值: " + value2);
}
}
输出:
key1 的值: value1
key2 的值: defaultValue
2. LoadingCache 自动加载
以下示例展示如何使用 LoadingCache
自动加载数据。
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class LoadingCacheExample {
public static void main(String[] args) {
// 创建 LoadingCache,自动加载数据
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(key -> loadValue(key));
// 获取数据,自动触发加载
String value = cache.get("key1");
System.out.println("key1 的值: " + value);
// 批量获取
System.out.println("批量获取: " + cache.getAll(java.util.Arrays.asList("key1", "key2")));
}
private static String loadValue(String key) {
// 模拟从数据库加载数据
return "value-for-" + key;
}
}
输出:
key1 的值: value-for-key1
批量获取: {key1=value-for-key1, key2=value-for-key2}
3. 异步加载缓存
以下示例展示如何使用 AsyncLoadingCache
进行异步加载。
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class AsyncCacheExample {
public static void main(String[] args) throws Exception {
// 创建异步 LoadingCache
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.buildAsync(key -> loadValueAsync(key));
// 异步获取数据
CompletableFuture<String> future = cache.get("key1");
System.out.println("key1 的值: " + future.get());
// 批量异步获取
CompletableFuture<?> all = cache.getAll(java.util.Arrays.asList("key1", "key2"));
System.out.println("批量获取: " + all.get());
}
private static CompletableFuture<String> loadValueAsync(String key) {
// 模拟异步加载
return CompletableFuture.supplyAsync(() -> "value-for-" + key);
}
}
输出:
key1 的值: value-for-key1
批量获取: {key1=value-for-key1, key2=value-for-key2}
4. 统计缓存性能
以下示例展示如何启用统计并查看缓存的命中率等信息。
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class CacheStatsExample {
public static void main(String[] args) {
// 创建缓存并启用统计
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.recordStats()
.build(key -> "value-for-" + key);
// 触发多次访问
cache.get("key1");
cache.get("key1");
cache.get("key2");
// 打印统计信息
System.out.println("缓存统计: " + cache.stats());
}
}
输出(示例,可能因运行环境不同而变化):
缓存统计: CacheStats{hitCount=1, missCount=2, loadSuccessCount=2, loadFailureCount=0, totalLoadTime=..., evictionCount=0, evictionWeight=0}
五、Caffeine 的使用场景
- Web 应用:缓存用户会话、页面数据等。
- 数据库查询缓存:减少数据库查询压力。
- 配置管理:缓存动态加载的配置信息。
- 高并发场景:利用异步加载提升性能。
六、注意事项
- 内存管理:合理设置
maximumSize
,避免内存溢出。 - 过期策略:根据业务场景选择合适的淘汰策略(访问后过期 vs 写入后过期)。
- 线程安全:Caffeine 是线程安全的,但加载函数需要确保线程安全。
- 异步加载:异步缓存需要妥善处理异常和超时。
七、依赖引入
在 Maven 项目中引入 Caffeine:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
八、Caffeine 高级用法
1. 自定义淘汰策略
Caffeine 允许通过 weigher
和 evictionListener
自定义淘汰逻辑:
- Weigher:为缓存条目指定权重,代替简单的条目计数。
- EvictionListener:监听被淘汰的条目,用于记录或触发后续操作。
示例:自定义权重和淘汰监听
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.TimeUnit;
public class CustomEvictionExample {
public static void main(String[] args) {
// 创建缓存,设置最大权重为100,并监听淘汰事件
Cache<String, String> cache = Caffeine.newBuilder()
.maximumWeight(100)
.weigher((String key, String value) -> key.length() + value.length()) // 权重为键值长度之和
.evictionListener((key, value, cause) ->
System.out.println("淘汰: key=" + key + ", value=" + value + ", 原因=" + cause))
.expireAfterWrite(10, TimeUnit.SECONDS)
.build();
// 存入数据
cache.put("key1", "value1"); // 权重: 4 + 6 = 10
cache.put("key2", "long-value2"); // 权重: 4 + 11 = 15
cache.put("largeKey", "very-large-value"); // 权重: 8 + 15 = 23
// 触发淘汰
cache.put("newKey", "another-large-value"); // 可能导致淘汰
}
}
输出(示例,可能因实际淘汰顺序不同):
淘汰: key=key1, value=value1, 原因=SIZE
说明:
weigher
计算键值对的内存占用(这里简化为字符串长度)。evictionListener
记录淘汰原因(如SIZE
表示因大小限制淘汰)。
2. 刷新策略
Caffeine 支持 refreshAfterWrite
,在指定时间后异步刷新缓存条目,而不会直接过期。这对于需要保持数据新鲜但不希望阻塞读取的场景(如热点数据)非常有用。
示例:异步刷新
import com.github.benmanes.caffeine.cache.*;
import java.util.concurrent.TimeUnit;
public class RefreshExample {
public static void main(String[] args) throws InterruptedException {
// 创建 LoadingCache,写入后5秒刷新
LoadingCache<String, String> cache = Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.SECONDS)
.build(key -> loadValue(key));
// 第一次获取
System.out.println("第一次获取: " + cache.get("key1"));
// 等待刷新触发
Thread.sleep(6000);
System.out.println("刷新后获取: " + cache.get("key1"));
}
private static String loadValue(String key) {
// 模拟加载新数据
return "value-for-" + key + "-" + System.currentTimeMillis();
}
}
输出(示例):
第一次获取: value-for-key1-1698765432100
刷新后获取: value-for-key1-1698765438100
说明:
refreshAfterWrite
不会让条目立即失效,而是异步调用加载函数。- 读取时返回旧值,直到新值加载完成。
3. 两级缓存(结合 Redis)
具体看我上篇的Caffeine+Redis实现两级缓存
九、Caffeine 优化技巧
-
批量加载优化:
-
实现
CacheLoader.loadAll
方法,减少多次数据库查询。 -
示例(参考你的问题关于
loadAll
):LoadingCache<String, String> cache = Caffeine.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String key) { return loadFromDatabase(key); } @Override public Map<String, String> loadAll(Iterable<? extends String> keys) { // 批量查询数据库 Map<String, String> result = new HashMap<>(); for (String key : keys) { result.put(key, loadFromDatabase(key)); } return result; } });
-
-
异步加载优化:
-
使用
AsyncLoadingCache
避免阻塞主线程。 -
配置线程池(如
ForkJoinPool
)以控制加载线程:Caffeine.newBuilder() .executor(ForkJoinPool.commonPool()) .buildAsync(key -> loadValueAsync(key));
-
-
统计分析:
- 启用
recordStats()
,定期分析命中率,调整maximumSize
或过期时间。 - 示例:通过
cache.stats().hitRate()
检查命中率,若低于预期,增加缓存容量。
- 启用
-
热点数据处理:
- 对于热点数据,使用
refreshAfterWrite
保持新鲜度。 - 结合 Redis Pub/Sub(如你的
initPubSub
场景)广播失效消息,确保分布式环境一致性。
- 对于热点数据,使用
十、常见问题及解决方案
-
缓存穿透:
-
问题:查询不存在的键导致频繁访问数据库。
-
解决:缓存空值或使用布隆过滤器。
-
示例:
String value = cache.get(key, k -> { String dbValue = loadFromDatabase(k); return dbValue != null ? dbValue : "NULL"; // 缓存空值 });
-
-
缓存雪崩:
-
问题:大量缓存同时过期,导致数据库压力激增。
-
解决:设置随机过期时间或使用
refreshAfterWrite
。 -
示例:
Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(5, 15)));
-
-
内存溢出:
- 问题:缓存条目过多导致 OOM。
- 解决:严格设置
maximumSize
或使用weigher
控制内存。
-
布隆过滤器结合Caffeine实现过滤案例
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>33.0.0-jre</version> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.8</version> </dependency>
public class BloomFilterCacheExample { // 布隆过滤器:预期插入10万个元素,假阳性率0.01 private final BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 100_000, // 预期元素数量 0.01); // 假阳性率 即误判率 // Caffeine 缓存:最大1000条,访问后10秒过期 private final Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(1_000) .expireAfterAccess(10, TimeUnit.SECONDS) .build(); // 初始化布隆过滤器(模拟从数据库加载已有键) public void initBloomFilter() { // 假设数据库中已有的键 String[] existingKeys = {"user1", "user2", "user3"}; for (String key : existingKeys) { bloomFilter.put(key); } } // 查询方法 public String get(String key) { // 1. 检查布隆过滤器 if (!bloomFilter.mightContain(key)) { System.out.println("布隆过滤器判定: " + key + " 不存在"); return null; // 直接返回,跳过缓存和数据库 } // 2. 查询 Caffeine 缓存 String value = cache.get(key, k -> loadFromDatabase(k)); return value; } // 模拟数据库查询 private String loadFromDatabase(String key) { // 模拟数据库:只有特定键存在 if (key.equals("user1") || key.equals("user2") || key.equals("user3")) { String value = "data-for-" + key; bloomFilter.put(key); // 确保布隆过滤器包含有效键 return value; } // 不存在的键返回空值并缓存 return "NULL"; // 缓存空值防止重复查询 } // 失效缓存 public void invalidate(String key) { cache.invalidate(key); // 注意:标准布隆过滤器不支持删除,可定期重建 } public static void main(String[] args) { BloomFilterCacheExample example = new BloomFilterCacheExample(); example.initBloomFilter(); // 测试存在的键 System.out.println("查询 user1: " + example.get("user1")); // 测试不存在的键 System.out.println("查询 user999: " + example.get("user999")); // 测试缓存空值 System.out.println("再次查询 user999: " + example.get("user999")); } }
输出:
查询 user1: data-for-user1 布隆过滤器判定: user999 不存在 查询 user999: null 布隆过滤器判定: user999 不存在 再次查询 user999: null
-
布隆过滤器优化建议
-
根据实际数据量调整 expectedInsertions 和 fpp(假阳性率)
-
将布隆过滤器存储在 Redis(如 redisson 提供的 RBloomFilter),实现分布式共享。
-
由于布隆过滤器不能删除值,为避免布隆过滤器过载,定期从数据库重建(如每天定时任务)。
-