Java 缓存框架 Caffeine 应用场景解析
作者:祈祷苍天赐我java之术
一、Caffeine 简介
1. 框架概述
Caffeine是由Google工程师Ben Manes开发的一款Java本地缓存框架,其初始版本发布于2014年。该框架的设计灵感来源于Guava Cache,但在性能和功能方面进行了革命性的优化。Caffeine基于"W-TinyLFU"(Window-Tiny Least Frequently Used)算法实现,这是一种改进的LFU缓存淘汰算法,结合了LFU的高命中率优势和LRU的时效性特点。
1.1 Caffeine的核心优势
1.1.1 超高性能
Caffeine在性能方面实现了质的飞跃:
- 基准测试显示,相比Guava Cache,Caffeine的读性能提升约8-12倍,写性能提升约5-10倍
- 支持每秒数百万次(典型值300-500万QPS)的缓存操作
- 采用无锁并发设计,大幅减少线程竞争(如使用并发哈希表和非阻塞队列)
- 对JVM的内存模型进行了深度优化,减少缓存行伪共享问题
1.1.2 灵活的过期策略
Caffeine提供三种核心过期策略:
- 写入后过期:通过
expireAfterWrite设置,例如:Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
- 访问后过期:通过
expireAfterAccess设置,适合热点数据场景 - 自定义过期:通过
expireAfter方法实现基于业务逻辑的复杂过期判断
1.1.3 异步支持
Caffeine提供完整的异步缓存(AsyncCache)支持:
- 异步加载机制:通过
AsyncLoadingCache实现非阻塞式数据加载 - 支持CompletableFuture:可与Java8的异步编程模型完美结合
- 典型应用场景:高并发环境下的微服务接口缓存
1.1.4 丰富的监听器
Caffeine提供完善的监控支持:
- 移除监听器(
RemovalListener):可监听缓存项的驱逐、失效或手动移除 - 统计功能:通过
recordStats()开启命中率统计 - 典型配置:
cache.recordStats(); CacheStats stats = cache.stats(); double hitRate = stats.hitRate();
1.1.5 内存安全
Caffeine提供多种内存保护机制:
- 基于容量:通过
maximumSize限制缓存项数量 - 基于时间:通过上述过期策略控制
- 基于引用:支持弱引用键/值(
weakKeys/weakValues)和软引用值(softValues) - 权重控制:通过
weigher和maximumWeight实现基于对象大小的精确控制
典型内存安全配置示例:
Caffeine.newBuilder()
.maximumSize(10_000)
.weigher((String key, String value) -> value.length())
.maximumWeight(50_000_000) // ~50MB
.build();二、Caffeine 基础
在使用 Caffeine 前,需先引入依赖,并了解其核心组件的作用。
2.1 依赖引入(Maven/Gradle)
Caffeine 的最新版本可在 Maven 中央仓库查询(https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine)
Maven 配置示例(含注释说明)
<!-- Caffeine核心依赖(必选) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.7</version> <!-- 截至2024年1月最新稳定版 -->
<!-- 建议通过dependencyManagement统一管理版本 -->
</dependency>
<!-- 异步支持依赖(可选) -->
<!-- 当需要配合Java11+的HttpClient实现异步缓存加载时添加 -->
<dependency>
<groupId>java.net.http</groupId>
<artifactId>http-client</artifactId>
<version>11.0.1</version> <!-- 最低要求JDK11 -->
<scope>runtime</scope> <!-- 通常只需运行时依赖 -->
</dependency>Gradle 配置示例(Kotlin DSL)
dependencies {
// 核心实现(必选)
implementation("com.github.ben-manes.caffeine:caffeine:3.2.7")
// 异步支持(可选)
runtimeOnly("java.net.http:http-client:11.0.1") {
because("For async cache loading with HTTP requests")
}
}2.2 核心组件解析
Caffeine 的核心组件采用分层设计,主要分为基础缓存接口和功能扩展接口两大类:
1.基础缓存接口层次结构
Cache (基本功能)
├── LoadingCache (同步加载)
└── AsyncCache (异步基础)
└── AsyncLoadingCache (异步加载)2.关键组件详细说明(含典型应用场景)
| 组件 | 作用说明 | 典型使用场景 | 示例代码片段 |
|---|---|---|---|
Cache<K,V> | 手动管理缓存,需显式处理缓存未命中 | 简单缓存场景,数据源访问成本较低 | cache.get(key, k -> fetchFromDB(k)) |
LoadingCache<K,V> | 自动加载缓存,内置CacheLoader | 高频访问且加载逻辑固定的场景 | LoadingCache.from(this::loadFromAPI) |
AsyncCache<K,V> | 返回CompletableFuture的异步接口 | 配合非阻塞IO或远程调用 | cache.get(key).thenAccept(value -> ...) |
AsyncLoadingCache<K,V> | 异步自动加载缓存 | 微服务间数据缓存 | AsyncLoadingCache.from(this::asyncLoad) |
CacheLoader<K,V> | 定义加载逻辑的函数式接口 | 统一数据加载策略 | new CacheLoader<>() { @Override public V load(K key)... } |
RemovalListener<K,V> | 移除事件监听器 | 缓存一致性维护、监控统计 | listener((key,value,reason) -> logRemoval()) |
Expiry<K,V> | 细粒度过期控制 | 动态TTL场景(如会话缓存) | expireAfter((key,value,currentTime) -> customTTL) |
3.高级特性支持
- 权重计算:通过
weigher接口实现基于缓存对象大小的淘汰策略 - 刷新机制:
refreshAfterWrite配合CacheLoader.reload实现后台刷新 - 统计监控:
recordStats()启用命中率等统计指标 - 线程模型:默认使用ForkJoinPool.commonPool(),可通过
executor自定义
4.最佳实践提示:
- 对于长时间加载操作,优先选择AsyncLoadingCache避免阻塞
- 移除监听器不要执行耗时操作,否则会影响缓存性能
- 在Spring环境中建议通过@Bean配置全局缓存管理器
- 生产环境务必启用统计功能(recordStats)进行监控
三、Caffeine 核心用法
Caffeine 的使用流程遵循 "构建器模式配置 → 创建缓存实例 → 读写缓存" 的逻辑,下面分场景讲解具体用法。
3.1 基础缓存(Cache):手动控制读写
Cache 是最基础的缓存类型,需手动处理缓存未命中(未命中时返回 null),适合缓存逻辑简单的场景。
3.1.1 创建 Cache 实例
通过 Caffeine.newBuilder() 配置缓存规则,常见配置包括:
- 容量控制:
maximumSize(long):设置缓存最大容量(条目数),超过后按 W-TinyLFU 算法淘汰。maximumWeight(long)+weigher(Weigher):基于权重控制缓存大小,适合不同条目占用不同内存的场景。
- 过期策略:
expireAfterWrite(Duration):写入后过期(如 10 分钟未更新则过期),适合数据变更频繁的场景。expireAfterAccess(Duration):访问后过期(如 5 分钟未访问则过期),适合热点数据缓存。expireAfter(Expiry):自定义过期时间计算逻辑,可实现基于业务规则的过期。
- 监听器:
removalListener(RemovalListener):设置缓存移除监听器,可记录日志或触发后续操作。
- 其他特性:
weakKeys()/weakValues():使用弱引用,允许被垃圾回收。softValues():使用软引用,在内存不足时被回收。recordStats():启用统计信息收集。
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Cache;
import java.util.concurrent.TimeUnit;
public class CaffeineBasicDemo {
public static void main(String[] args) {
// 1. 配置并创建Cache实例(带详细注释)
Cache<String, String> userCache = Caffeine.newBuilder()
.maximumSize(1000) // 最大容量1000条
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期(优先级低于expireAfterWrite)
.removalListener((key, value, cause) -> { // 缓存移除监听器
System.out.printf("缓存移除:key=%s, value=%s, 原因=%s%n",
key, value, cause.toString());
// 原因可能是:EXPLICIT(手动删除)、REPLACED(值被替换)、
// COLLECTED(垃圾回收)、EXPIRED(过期)、SIZE(超过容量限制)
})
.recordStats() // 启用统计
.build(); // 构建Cache实例
// 2. 写入缓存(多种方式)
userCache.put("user:1001", "张三"); // 常规put
userCache.asMap().putIfAbsent("user:1002", "李四"); // 线程安全写入
// 3. 读取缓存(未命中返回null)
String user1 = userCache.getIfPresent("user:1001");
System.out.println("读取user:1001:" + user1); // 输出:张三
// 4. 读取并计算(未命中时执行函数逻辑,但不自动存入缓存)
String user3 = userCache.get("user:1003", key -> {
// 模拟从数据库查询数据(仅当缓存未命中时执行)
System.out.println("缓存未命中,查询DB:" + key);
return "王五"; // 此结果不会自动存入缓存
});
System.out.println("读取user:1003:" + user3); // 输出:王五
// 5. 缓存维护操作
userCache.invalidate("user:1002"); // 单个删除
userCache.invalidateAll(List.of("user:1001", "user:1003")); // 批量删除
userCache.cleanUp(); // 手动触发清理过期条目
userCache.invalidateAll(); // 清空所有缓存
// 6. 查看统计信息(需先启用recordStats)
System.out.println("命中率:" + userCache.stats().hitRate());
}
}3.1.2 应用场景示例
- 简单KV缓存:
- 缓存用户Session信息
- 缓存系统配置项
- 临时数据存储(如验证码)
- 配合Spring Cache:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return manager;
}
}多级缓存:
// 作为本地缓存与Redis组成二级缓存
public class MultiLevelCache {
private final Cache<String, Object> localCache;
private final RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
Object value = localCache.getIfPresent(key);
if (value == null) {
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
}
}
return value;
}
}3.2 加载缓存(LoadingCache):自动加载未命中数据
LoadingCache 是 Cache 的子类,通过实现 CacheLoader 接口,实现 "缓存未命中时自动加载数据并存入缓存",适合缓存数据需从数据源(如 DB、Redis)加载的场景。
3.2.1 创建 LoadingCache 实例
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.List;
public class CaffeineLoadingDemo {
public static void main(String[] args) throws ExecutionException {
// 1. 实现CacheLoader:定义缓存未命中时的加载逻辑
LoadingCache<String, String> productCache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(30, TimeUnit.MINUTES)
.refreshAfterWrite(10, TimeUnit.MINUTES) // 10分钟后刷新(不阻塞读取)
.recordStats()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 模拟从数据库加载数据(缓存未命中时自动执行)
System.out.println("缓存未命中,从DB加载:" + key);
if (key.startsWith("prod:")) {
return "商品-" + key.substring(5); // 如key=prod:101 → 商品-101
}
throw new IllegalArgumentException("Invalid key format");
}
// 可选:实现批量加载(提升getAll性能)
@Override
public Map<String, String> loadAll(Iterable<? extends String> keys) {
System.out.println("批量加载keys:" + keys);
// 实际应从DB批量查询
Map<String, String> result = new HashMap<>();
for (String key : keys) {
result.put(key, "商品-" + key.substring(5));
}
return result;
}
});
// 2. 读取缓存(未命中时自动调用load()加载并存入缓存)
String product1 = productCache.get("prod:101"); // 首次:加载并返回
System.out.println("读取prod:101:" + product1); // 输出:商品-101
// 3. 批量读取(getAll())
Map<String, String> products = productCache.getAll(List.of("prod:102", "prod:103"));
System.out.println("批量读取结果:" + products);
// 4. 主动刷新(异步)
productCache.refresh("prod:101"); // 后台刷新,旧值仍可用
// 5. 统计信息
System.out.println("加载次数:" + productCache.stats().loadCount());
}
}3.2.2 关键特性:刷新(Refresh)与过期(Expire)的区别
| 特性 | 刷新(Refresh) | 过期(Expire) |
|---|---|---|
| 触发时机 | 刷新时间到后 | 过期时间到后 |
| 读取行为 | 异步刷新,立即返回旧值 | 同步重新加载,可能阻塞请求 |
| 适用场景 | 数据允许短暂不一致(如商品详情) | 数据强一致要求(如订单状态) |
| 实现方式 | 需配置refreshAfterWrite | 配置expireAfterWrite/AfterAccess |
典型使用模式:
// 商品详情缓存:10分钟强制过期,5分钟自动刷新
LoadingCache<String, Product> productCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES) // 强制过期时间
.refreshAfterWrite(5, TimeUnit.MINUTES) // 自动刷新时间
.build(this::loadProductFromDB);
3.3 异步缓存(AsyncCache/AsyncLoadingCache):非阻塞读写
在高并发场景下,同步缓存的 load() 可能会阻塞线程,而 AsyncCache 通过返回 CompletableFuture 实现非阻塞操作,所有 IO 操作均在异步线程池中执行。
3.3.1 创建 AsyncLoadingCache 实例
import com.github.ben-manes.caffeine.cache.AsyncLoadingCache;
import com.github.ben-manes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CaffeineAsyncDemo {
public static void main(String[] args) throws Exception {
// 1. 自定义线程池(生产环境建议使用有界队列和拒绝策略)
Executor executor = Executors.newFixedThreadPool(5, r -> {
Thread thread = new Thread(r);
thread.setName("caffeine-async-" + thread.getId());
return thread;
});
// 2. 创建AsyncLoadingCache实例
AsyncLoadingCache<String, String> orderCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(15, TimeUnit.MINUTES)
.executor(executor) // 指定异步线程池
.buildAsync(key -> {
// 模拟耗时操作(如RPC调用,耗时200ms)
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + " 加载订单:" + key);
return "订单-" + key.substring(6); // 如key=order:2024 → 订单-2024
});
// 3. 异步读取(推荐方式)
CompletableFuture<String> future = orderCache.get("order:2024");
future.thenApplyAsync(order -> {
System.out.println("处理订单数据:" + order);
return order.toUpperCase();
}, executor); // 使用相同线程池处理结果
// 4. 批量读取(返回Map<Key, CompletableFuture>)
Map<String, CompletableFuture<String>> futures =
orderCache.getAll(List.of("order:2025", "order:2026"));
// 5. 同步获取(仅测试用,实际应避免)
String order = orderCache.get("order:2027").get();
System.out.println("同步获取结果:" + order);
}
}3.3.2 最佳实践
线程池配置:
// 更完善的线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder().setNameFormat("cache-loader-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
异常处理:
orderCache.get("badKey").exceptionally(ex -> {
System.err.println("加载失败: " + ex.getMessage());
return "defaultValue";
});
结合Spring使用:
@Cacheable(value = "orders", cacheManager = "asyncCacheManager")
public CompletableFuture<Order> getOrderAsync(String orderId) {
return CompletableFuture.supplyAsync(() -> orderService.loadOrder(orderId));
}
性能监控:
CacheStats stats = orderCache.synchronous().stats();
System.out.println("平均加载时间:" + stats.averageLoadPenalty() + "ns");
四、Caffeine 高级特性
4.1 缓存统计(Cache Statistics)
缓存统计功能是优化缓存性能的重要工具。通过开启缓存统计,可以实时监控以下关键指标:
- 命中率(Hit Rate):反映缓存有效性,计算公式为:
命中次数/(命中次数+未命中次数) - 加载耗时(Load Penalty):统计从数据源加载数据的平均耗时
- 移除次数(Eviction Count):因容量或过期策略导致的缓存移除次数
- 加载失败率(Load Failure Rate):数据源加载失败的比例
典型应用场景:
- 评估缓存配置是否合理
- 识别热点数据
- 监控缓存性能瓶颈
import com.github.ben-manes.caffeine.cache.CacheStats;
public class CaffeineStatsDemo {
public static void main(String[] args) {
LoadingCache<String, String> statsCache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats() // 必须显式开启统计功能
.build(key -> {
// 模拟耗时加载
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "统计测试:" + key;
});
// 模拟读写操作
statsCache.get("key1"); // 第一次加载(未命中)
statsCache.get("key1"); // 命中已有缓存
statsCache.get("key2"); // 新键加载
statsCache.invalidate("key1"); // 手动失效
// 获取统计结果
CacheStats stats = statsCache.stats();
System.out.println("缓存命中率:" + stats.hitRate()); // 50%(1次命中/2次查询)
System.out.println("加载成功次数:" + stats.loadSuccessCount()); // 2次加载
System.out.println("移除次数:" + stats.evictionCount()); // 0(未达到容量上限)
System.out.println("平均加载耗时(ns):" + stats.averageLoadPenalty()); // 约100ms
System.out.println("加载失败率:" + stats.loadFailureRate()); // 0.0
}
}4.2 自定义过期策略(Expiry)
标准的TTL(Time-To-Live)过期策略对所有缓存条目采用统一设置,而自定义过期策略允许基于业务特性实现精细化控制。
常见应用场景:
- 不同优先级数据设置不同有效期(如热点数据短时效,冷数据长时效)
- 读写操作影响过期时间(如读操作续期)
- 动态调整过期时间(如根据数据价值计算)
import com.github.ben-manes.caffeine.cache.Caffeine;
import com.github.ben-manes.caffeine.cache.Expiry;
import com.github.ben-manes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
class CustomExpiry implements Expiry<String, String> {
@Override
public long expireAfterCreate(String key, String value, long currentTime) {
// 创建时过期策略
if (key.startsWith("flash:")) { // 闪存数据:30秒过期
return TimeUnit.SECONDS.toNanos(30);
} else if (key.startsWith("hot:")) { // 热门数据:5分钟
return TimeUnit.MINUTES.toNanos(5);
} else { // 普通数据:30分钟
return TimeUnit.MINUTES.toNanos(30);
}
}
@Override
public long expireAfterUpdate(String key, String value,
long currentTime, long currentDuration) {
// 更新策略:保持原有过期时间(默认)
return currentDuration;
// 或者重置为创建时间:return expireAfterCreate(key, value, currentTime);
}
@Override
public long expireAfterRead(String key, String value,
long currentTime, long currentDuration) {
// 读取时策略:热门数据读取后续期5分钟
if (key.startsWith("hot:")) {
return TimeUnit.MINUTES.toNanos(5);
}
return currentDuration;
}
}
public class CaffeineCustomExpiryDemo {
public static void main(String[] args) {
LoadingCache<String, String> customExpiryCache = Caffeine.newBuilder()
.expireAfter(new CustomExpiry())
.build(key -> "自定义过期:" + key);
customExpiryCache.get("flash:news:2023"); // 30秒过期
customExpiryCache.get("hot:product:101"); // 5分钟且读取续期
customExpiryCache.get("normal:user:201"); // 30分钟过期
}
}4.3 弱引用与软引用:避免内存溢出
Java引用类型与缓存回收策略:
| 引用类型 | GC行为 | 适用场景 | Caffeine配置 |
|---|---|---|---|
| 强引用 | 永不回收 | 默认方式 | - |
| 软引用 | 内存不足时回收 | 缓存大对象 | .softValues() |
| 弱引用 | 下次GC时回收 | 临时性缓存 | .weakKeys()/.weakValues() |
注意事项:
- 使用
weakKeys()时,key比较基于==而非equals() softValues()可能导致GC压力增大- 引用回收与显式失效策略共同作用
// 弱引用Key+Value的缓存(适合临时性数据)
Cache<String, byte[]> weakCache = Caffeine.newBuilder()
.weakKeys() // Key无强引用时回收
.weakValues() // Value无强引用时回收
.maximumSize(10_000) // 仍保持容量限制
.build();
// 软引用Value的缓存(适合大对象)
Cache<String, byte[]> softCache = Caffeine.newBuilder()
.softValues() // 内存不足时回收Value
.expireAfterWrite(1, TimeUnit.HOURS) // 配合显式过期
.build();
// 典型使用场景
void processLargeData(String dataId) {
byte[] data = softCache.get(dataId, id -> {
// 从数据库加载大对象(如图片、文件等)
return loadLargeDataFromDB(id);
});
// 使用数据...
}五、Caffeine 注意事项
在实际开发中,若使用不当,Caffeine 可能出现缓存穿透、内存溢出、线程阻塞等问题,以下是核心注意事项:
5.1 区分 "刷新(Refresh)" 与 "过期(Expire)"
刷新(refreshAfterWrite):
- 工作机制:当缓存条目超过指定时间未被写入时,下次读取会触发异步刷新,但在此期间仍会返回旧值
- 适用场景:对数据一致性要求不高,可接受短暂延迟的场景
- 商品详情页的评论数统计
- 新闻资讯的阅读量统计
- 排行榜数据的更新
过期(expireAfterWrite/expireAfterAccess):
- expireAfterWrite:从写入开始计时
- expireAfterAccess:从最后一次访问开始计时
- 工作机制:过期后缓存条目立即失效,读取时会同步阻塞直到重新加载完成
- 适用场景:对数据一致性要求高的核心业务
- 用户账户余额
- 订单支付状态
- 库存数量
⚠️ 典型误用场景:
将用户余额这类强一致性数据配置为refreshAfterWrite(5s).可能导致:
- 用户A看到余额100元
- 用户B完成扣款50元
- 5秒内用户A仍看到100元(旧值)
- 直到下次读取才刷新为50元
5.2 避免缓存穿透:空值缓存与布隆过滤器
缓存穿透的典型特征:
- 查询一个必然不存在的数据(如不存在的用户ID)
- 每次请求都穿透到数据库
- 可能被恶意攻击者利用,造成数据库压力
解决方案1:空值缓存
LoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES) // 空值缓存1分钟
.build(key -> {
String value = queryFromDB(key);
// 特殊空值标记,避免与真实空值混淆
return value != null ? value : "NULL_VALUE";
});
解决方案2:布隆过滤器(适合千万级key场景)
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期元素数量
0.01 // 误判率
);
// 查询流程
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在
} else {
return cache.get(key); // 可能存在
}5.3 缓存键(Key)必须重写 hashCode() 和 equals()
常见错误案例:
class CompositeKey {
private Long id;
private String category;
// 缺少hashCode/equals实现
}
// 实际使用中
CompositeKey key1 = new CompositeKey(1L, "A");
CompositeKey key2 = new CompositeKey(1L, "A");
cache.put(key1, "value");
// 将返回null,因为key2被视为不同key
cache.getIfPresent(key2); 正确实现要点:
- 使用Objects工具类自动生成
- 保证不可变(final字段)
- 实现Serializable接口(分布式缓存需要)
class CompositeKey implements Serializable {
private final Long id;
private final String category;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CompositeKey)) return false;
CompositeKey that = (CompositeKey) o;
return Objects.equals(id, that.id) &&
Objects.equals(category, that.category);
}
@Override
public int hashCode() {
return Objects.hash(id, category);
}
}5.4 异步缓存(AsyncCache)的线程池选择
默认线程池的问题:
- ForkJoinPool.commonPool()是JVM全局共享的
- 可能被CompletableFuture等其他组件占用
- 在容器环境中可能线程数不足
推荐配置:
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new ThreadFactoryBuilder()
.setNameFormat("caffeine-loader-%d")
.setDaemon(true)
.build()
);
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.executor(executor) // 指定专属线程池
.buildAsync(key -> loadExpensiveValue(key));5.5 避免内存溢出:合理配置容量与过期时间
典型配置示例:
Caffeine.newBuilder()
.maximumSize(10_000) // 基于条目数限制
.expireAfterWrite(30, TimeUnit.MINUTES) // 写入后30分钟过期
.expireAfterAccess(10, TimeUnit.MINUTES) // 10分钟无访问过期
.weigher((String key, String value) -> value.length()) // 按value大小计算权重
.maximumWeight(50_000_000) // 约50MB内存限制
监控建议:
- 通过cache.stats()获取命中率
- 使用JMX监控缓存大小
- 设置告警阈值(如内存使用>80%)
5.6 CacheLoader 的异常处理
完整异常处理方案:
LoadingCache<String, String> cache = Caffeine.newBuilder()
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
try {
return queryDB(key);
} catch (SQLException e) {
// 记录详细日志
log.error("DB查询失败, key: {}", key, e);
// 返回降级值
return "DEFAULT_VALUE";
// 或者抛出特定异常
// throw new CacheLoadException(e);
}
}
});
// 使用时的异常处理
try {
return cache.get(key);
} catch (CacheLoaderException e) {
// 处理加载失败
return processFallback(key);
} catch (Exception e) {
// 兜底处理
return "SYSTEM_ERROR";
}六、常见问题
Q1:Caffeine 与 Guava Cache 的详细区别
性能比较
Caffeine 采用了创新的 W-TinyLFU 缓存淘汰算法,该算法结合了 TinyLFU 和 LRU 的优势:
- 使用 Count-Min Sketch 数据结构高效统计访问频率
- 适应不同工作负载模式(突发性和长期性访问)
- 在基准测试中,Caffeine 的读写性能比 Guava Cache 高出 10-20 倍
功能特性对比
| 特性 | Caffeine | Guava Cache |
|---|---|---|
| 异步加载 | 支持 AsyncLoadingCache | 仅同步加载 |
| 过期策略 | 支持基于大小、时间、引用等多种策略 | 仅基本过期策略 |
| 自动刷新 | 支持 refreshAfterWrite | 不支持 |
| 权重计算 | 支持自定义权重 | 支持但性能较差 |
| 监听器 | 支持移除监听器 | 支持移除监听器 |
| 统计 | 提供命中率等详细统计 | 提供基本统计 |
兼容性与迁移
Caffeine 在设计时特别考虑了与 Guava Cache 的兼容性:
- 90%以上的 API 可以直接替换
- 主要差异在于构建方式(Caffeine.newBuilder() vs CacheBuilder.newBuilder())
- 迁移示例:
// Guava Cache
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<Key, Value>() {
public Value load(Key key) {
return createValue(key);
}
});
// 迁移到 Caffeine
LoadingCache<Key, Value> cache = Caffeine.newBuilder()
.maximumSize(1000)
.build(key -> createValue(key));Q2:Caffeine 的分布式缓存支持与多级缓存架构
本地缓存特性
Caffeine 作为本地缓存的核心特点:
- 仅作用于单个 JVM 进程内
- 不同服务器节点间的缓存数据不共享
- 适用于高频访问、低变化率的数据
二级缓存架构实现
典型的生产级缓存架构组合:
- 第一层:Caffeine 本地缓存(纳秒级响应)
- 设置合理的过期时间(如30秒)
- 适合极端热点数据
- 第二层:Redis 分布式缓存(毫秒级响应)
- 设置较长的过期时间(如5分钟)
- 使用Redis集群保证高可用
- 数据源:数据库/服务(秒级响应)
- 最终数据一致性保障
实现示例
public class TwoLevelCacheService {
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
private final RedisTemplate<String, Object> redisTemplate;
public Object getData(String key) {
// 1. 尝试从本地缓存获取
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 尝试从Redis获取
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. 回源查询
value = queryDatabase(key);
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
localCache.put(key, value);
return value;
}
}Q3:缓存击穿解决方案的深入分析
互斥锁方案详解
实现要点:
- 使用
key.intern()获取字符串规范表示,确保相同key锁定同一对象 - 采用双重检查锁定模式减少锁竞争
- 设置合理的锁等待超时时间
增强版实现:
public Object getDataWithLock(String key) {
Object value = cache.getIfPresent(key);
if (value != null) {
return value;
}
synchronized (key.intern()) {
// 双重检查
value = cache.getIfPresent(key);
if (value != null) {
return value;
}
try {
value = queryDataSource(key);
cache.put(key, value);
} finally {
// 释放资源
}
}
return value;
}热点Key永不过期方案
实现模式:
主动更新:后台线程定期(如每分钟)刷新热点数据
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
List<String> hotKeys = getHotKeyList();
hotKeys.forEach(key -> {
Object value = queryDataSource(key);
cache.put(key, value);
});
}, 0, 1, TimeUnit.MINUTES);
被动更新:获取数据时异步刷新
LoadingCache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> queryDataSource(key));
其他防护策略
- 布隆过滤器:前置过滤不存在的key请求
- 缓存预热:系统启动时加载热点数据
- 随机过期时间:对相同类型key设置不同的过期时间偏移量
int baseExpire = 3600; // 基础1小时 int randomOffset = ThreadLocalRandom.current().nextInt(600); // 0-10分钟随机 cache.put(key, value, baseExpire + randomOffset, TimeUnit.SECONDS);
