高性能缓存 Caffeine 原理及实战
引言
上一篇我们写了一个非常简单的监控告警系统,对我们线上的数据库、服务及中间件进行定时探测及异常告警,但我们发现,偶尔会因为一些其它情况(如网络抖动)导致误报警出现,所以我们需要设置一个异常阈值,在一定时间内达到设定阈值则说明应用确实出现了异常,此时我们才发送告警信息。
技术选型
对于上述需求,我们首先想到的就是缓存策略,日常开发过程中我们常用的缓存就是Redis
。但这是一个独立的监控系统,其目的就是为了监控包括Redis在内的其他应用的健康状态,显然此时不能选择Redis作为我们的缓存,而应该选择Java内存缓存来实现上述需求;
对于Java内存缓存,常见的有Guava Cache
、Caffeine、Encache
等。当然,我们也可以使用HashMap来自己实现本地缓存,此时我们的场景非常简单,也是可以的;其中Caffeine
的表现最为出色,此点从Spring官方都已经弃用Guava Cache
选择Caffeine
就很有说服力。
参数说明
Caffeine
提供了多种灵活的构造方法,可根据自己的具体需求来灵活配置,其参数如下:
initialCapacity
设置内部数据结构的最小总大小maximumSize
指定缓存可以包含的最大条目数,此功能不能
与maximumWeight一起使用expireAfterWrite
设置最后一次写入后多久过期expireAfterAccess
设置在最后一次读或写
后多久过期expireAfter
自定义淘汰策略,该策略下 Caffeine 通过时间轮算法来实现不同key 的不同过期时间evictionListener
设置缓存淘汰监听器executor
设置自定义线程池,默认的线程池实现是 ForkJoinPool.commonPool()removalListener
设置缓存删除监听器scheduler
设置一个调度器,用于定时执行缓存操作。默认情况下,Caffeine 使用 ScheduledExecutorService 作为调度器softValues
设置为 true 时,缓存项的值会被包装成软引用,在内存溢出前可以直接淘汰ticker
设置一个 Ticker,用于定期检查缓存的状态。可以用于监控缓存的性能weakKeys
设置为 true 时,缓存项的键会被包装成弱引用,在 GC 时可以直接淘汰weakValues
设置为 true 时,缓存项的值会被包装成弱引用,在 GC 时可以直接淘汰weigher
用于计算缓存项的权重。权重越高,缓存项越容易被选中maximumWeight
设置缓存项的最大权重。当缓存项的权重超过这个值时,缓存项不会被选中recordStats
设置为 true 时,Caffeine 会记录缓存的统计信息,例如命中率、加载率等refreshAfterWrite
设置写入后间隔多久刷新,该刷新是基于访问被动触发的,支持异步刷新和同步刷新,如果和 expireAfterWrite 组合使用,能够保证即使该缓存访问不到、也能在固定时间间隔后被淘汰,否则如果单独使用容易造成OOM
使用
引入maven依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
使用缓存
Cache
Cache 是 Caffeine 中的一个核心接口,它代表了一个本地的、有限的、并发的数据结构,用于存储键值对。当调用 get(key) 方法尝试获取一个缓存项时,需要带入一个参数为 key 的 Function
, 如果这个缓存项不存在或已经被过期,Cache 会创建一个值并插入到缓存中然后返回该值。在计算进行期间,其他线程在此缓存上尝试的一些更新操作可能会被阻止,因此计算应简短而简单,并且不得尝试更新此缓存的任何其他映射
示例
// 初始化一个缓存,并设置过期时间及内存大小。
public static void main(String[] args) {
Cache<String, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.maximumSize(10)
.build();
String key = "test";
// 使用 `getIfPresent` 方法获取缓存值,如果不存在则返回 null
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.get(key, s -> 1));
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
cache.put(key, 2);
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
// 手动移除
cache.invalidate(key);
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
}
日志
获取键为 test 的缓存值: null
获取键为 test 的缓存值: 1
获取键为 test 的缓存值: 1
获取键为 test 的缓存值: 2
获取键为 test 的缓存值: null
LoadingCache
LoadingCache 是一个接口,它定义了如何将数据加载到缓存中的方式。当调用 get(key) 方法尝试获取一个缓存项时,如果这个缓存项还没有被加载到内存中,LoadingCache 会使用指定的加载器(Loader)来异步地加载这个缓存项,并在加载完成后将其添加到缓存中。这种方式的好处是,它允许你为不同的键提供不同的加载器,从而实现更复杂的缓存逻辑。例如,你可以根据键的前缀来决定从哪个后端存储系统中加载数据。
示例
private static Integer queryAgeByName(String name){
return Math.toIntExact(Math.round(Math.random() * 100));
}
public static void main(String[] args) {
LoadingCache<String, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.maximumSize(10)
.build(new CacheLoader<String, Integer>() {
@Override
public @Nullable Integer load(@NonNull String key) {
return queryAgeByName(key);
}
});
String key = "test";
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.get(key));
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
cache.put(key, 2);
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
List<String> keys = Arrays.asList("xiaoming", "zhangsan");
LOGGER.info("获取键为 {} 的缓存值: {}", keys, cache.getAll(keys));
}
日志
获取键为 test 的缓存值: null
获取键为 test 的缓存值: 12
获取键为 test 的缓存值: 12
获取键为 test 的缓存值: 2
获取键为 [xiaoming, zhangsan] 的缓存值: {xiaoming=37, zhangsan=82}
AsyncCache
这是 Cache 的一个变体,AsyncCache 提供了在 Executor 上异步生成缓存元素并返回 CompletableFuture 的能力。这意味着当你需要一个缓存项时,如果这个缓存项还没有被加载到内存中,AsyncCache 会在后台线程中异步地加载这个缓存项,并在加载完成后立即返回。这种机制特别适合于读取密集型的应用,因为它可以减少等待时间,提高系统的响应速度。此外,synchronous() 方法可以阻塞当前线程,直到异步缓存被完全生成。
示例
private static Integer queryAgeByName(String name){
return Math.toIntExact(Math.round(Math.random() * 100));
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
AsyncCache<String, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(1))
.maximumSize(10)
.buildAsync();
String key = "test";
LOGGER.info("获取键为 {} 的缓存值: {}", key, cache.getIfPresent(key));
CompletableFuture<Integer> value = cache.get(key, MonitorApplicationTests::queryAgeByName);
cache.put(key, value);
LOGGER.info("键为 {} 的插入值为 {} 获取的缓存值 : {}", key, value.get(), Objects.requireNonNull(cache.getIfPresent(key)).get());
}
日志
获取键为 test 的缓存值: null
键为 test 的插入值为 4 获取的缓存值 : 4
- 本文标签: java 开源 缓存
- 本文链接: https://www.58cto.cn/article/29
- 版权声明: 本文由程序言原创发布, 非商业性可自由转载、引用,但需署名作者且注明文章出处:程序言 》 高性能缓存 Caffeine 原理及实战 - https://www.58cto.cn/article/29