本文主要是介绍Java 缓存框架 Caffeine 应用场景解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《Java缓存框架Caffeine应用场景解析》文章介绍Caffeine作为高性能Java本地缓存框架,基于W-TinyLFU算法,支持异步加载、灵活过期策略、内存安全机制及统计监控,重点解析其...
一、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"); // 单个删除 www.chinasem.cn 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>YjlSE 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)与过php期(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&pythonlt;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-phpMin 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 作为本地缓存的核心特点:
二级缓存架构实现
典型的生产级缓存架构组合:
- 第一层: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);
这篇关于Java 缓存框架 Caffeine 应用场景解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!