一、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);
发表评论