赞
踩
Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。
这一章节我们会从 Caffeine 的使用引入,并提出一些问题,之后分析其源代码解决这些问题来让我们更好的去了解 Caffeine 的原理,更好的使用与优化,并且会对于我们之后的编码有所裨益。
我们来看一下 Caffeine 的基本使用,首先是创建:
Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置
1. 创建一个限制容量 Cache
Cache<String, Object> cache = Caffeine
.newBuilder()
//设置缓存的 Entries 个数最多不超过1000个
.maximumSize(1000)
.build();
需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:
配置了 maximumSize 就不能配置下面的 maximumWeight 和 weigher
2. 创建一个自定义权重限制容量的 Cache
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1000)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
当你的缓存的 Key 或者 Value 比较大的时候,想灵活地控制缓存大小,可以使用这种方式。上面我们的 key 是一个 list,以 list 的大小作为 Entry 的大小。
当把 Weigher 实现为只返回1,maximumWeight 其实和 maximumSize 是等效的。
同样的,为了性能考虑,这个限制也不会很死板。
在这里,我们提出第一个问题:Entry是怎么保存,怎么过期的呢?
3. 指定初始大小
Cache<String, Object> cache = Caffeine.newBuilder()
//指定初始大小
.initialCapacity(1000)
.build();
和HashMap
类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。
在这里,我们提出第二个问题:这个初始大小,影响那些存储参数呢?
4. 指定Key, Value为非强引用类型
Cache<String, Object> cache = Caffeine.newBuilder() // 设置 key 为 WeakReference .weakKeys() .build(); cache = Caffeine.newBuilder() // 设置 key 为 WeakReference .weakKeys() // 设置 value 为 WeakReference .weakValues() .build(); cache = Caffeine.newBuilder() // 设置 key 为 WeakReference .weakKeys() // 设置 value 为 SofReference .softValues() .build();
对于 Java 中的 StrongReference,WeakReference,SoftReference,可以参考我的另外一篇文章:JDK核心JAVA源码解析(3) - 引用相关
在这里简单归纳下:
Caffeine 中的 Key,可以是 WeakReference,但是目前不能指定为 SoftReference,所以我们在这里提出第三个问题,为什么 Key 不能指定为 SoftReference,SoftReference 为何被区别对待。
设置 Key 和 Value 的 Reference 类型,也是一种限制大小的方式,但是限制比较多:
一般通过 maximumSize 还有 maximumWeight 就能满足我们的需求。
1. 自定义过期
Cache<String, Order> cache = Caffeine.newBuilder() .expireAfter(new Expiry<String, Order>() { @Override //设置 Entry 创建后的过期时间 //这里设置为 60s 后过期 public long expireAfterCreate(@NonNull String key, @NonNull Order value, long currentTime) { return 1000 * 1000 * 1000 * 60; } @Override //设置 Entry 更新后的过期时间 //这里返回 currentDuration 表示永远不过期 public long expireAfterUpdate(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) { return currentDuration; } @Override //设置 Entry 读取后的过期时间 //这里设置为 Order 的 createTime 的 60s 后过期 public long expireAfterRead(@NonNull String key, @NonNull Order value, long currentTime, @NonNegative long currentDuration) { return 1000 * 1000 * 1000 * 60 - (System.currentTimeMillis() - value.createTime()) * 1000; } }) .build();
通过实现 Expiry 接口,设置过期策略。这个接口主要包括三个值:
这个配置与接下来的 expireAfterWrite 和 expireAfterAccess 互斥。不能同时配置
** 2. 设置写入以及更新后过期**
Cache<String, Object> cache = Caffeine.newBuilder()
//写入或者更新1分钟后,缓存过期并失效
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
这个配置与上面的 expireAfter 互斥,不能同时配置
** 3. 设置操作后过期**
Cache<String, Object> cache = Caffeine.newBuilder()
//写入或者更新或者读取1分钟后,缓存过期并失效
.expireAfterAccess(1, TimeUnit.MINUTES)
.build();
这个配置与上面的 expireAfter 互斥,不能同时配置
** 1. 生成LoadingCache **
Cache<String, Object> cache = Caffeine.newBuilder()
//使用 CacheLoader 初始化
.build(key -> {
return loadFromDB(key);
});
当 Key 不存在或者已过期时,会调用 CacheLoader 重新加载这个 Key。那么,这里要提出下面这些问题:
2. 设置定时重新加载时间
Cache<String, Object> cache = Caffeine.newBuilder()
//设置在写入或者更新之后1分钟后,调用 CacheLoader 重新加载
.refreshAfterWrite(1, TimeUnit.MINUTES)
//使用 CacheLoader 初始化
.build(key -> {
return loadFromDB(key);
});
注意设置了这个配置,就只能通过build(CacheLoader)
来生成 LoadingCache,不能生成普通的 Cache 了
1. 统计记录相关
Cache<String, Object> cache = Caffeine.newBuilder() //打开数据采集 .recordStats().build(); Cache<String, Object> cache = Caffeine.newBuilder() //自定义数据采集器 .recordStats(() -> new StatsCounter() { @Override public void recordHits(@NonNegative int count) { } @Override public void recordMisses(@NonNegative int count) { } @Override public void recordLoadSuccess(@NonNegative long loadTime) { } @Override public void recordLoadFailure(@NonNegative long loadTime) { } @Override public void recordEviction() { } @Override public void recordEviction(@NonNegative int weight) { } @Override public void recordEviction(@NonNegative int weight, RemovalCause cause) { } @Override public @NonNull CacheStats snapshot() { return null; } }).build();
这里我们提出两个问题:
2. 某个 Entry 过期被移除后的回调
Cache<String, Object> cache = Caffeine
.newBuilder()
.removalListener((key, value, cause) -> {
log.info("{}, {}, {}", key, value, cause);
})
.build();
回调里面有三个参数,包括 Entry 的 Key, Entry 的 Value 以及移除原因 cause。这个原因是一个枚举类型:
public enum RemovalCause { EXPLICIT { @Override public boolean wasEvicted() { return false; } }, REPLACED { @Override public boolean wasEvicted() { return false; } }, COLLECTED { @Override public boolean wasEvicted() { return true; } }, EXPIRED { @Override public boolean wasEvicted() { return true; } }, SIZE { @Override public boolean wasEvicted() { return true; } }; }
这里再提出一个问题:失效原因究竟对应哪些 API 的操作导致的失效?
3. 缓存主动更新其他存储或者资源
我们还可以通过设置 Writer,将对于缓存的更新,作用于其他存储,例如数据库:
Cache<String, Object> cache = Caffeine.newBuilder() .writer(new CacheWriter<String, Object>() { @Override public void write(@NonNull String key, @NonNull Object value) { //缓存更新时(包括创建和修改,不包括load),回调这里 //数据库更新 db.upsert(key, value); } @Override public void delete(@NonNull String key, @Nullable Object value, @NonNull RemovalCause cause) { //缓存失效时(包括任何原因的失效),回调这里 //数据库更新 db.markAsDeleted(key, value); } }) .build();
那么就引出了如下几个问题:
1. 生成异步缓存
AsyncCache<String, Object> cache = Caffeine.newBuilder()
//生成异步缓存
.buildAsync();
这种缓存,获取的 Value 都是一个 CompletableFuture
。
**2. 生成异步 LoadingCache **
AsyncCache<String, Object> cache = Caffeine.newBuilder()
//生成异步缓存
.buildAsync(key -> {
return loadFromDB(key);
});
3. 设置异步任务线程池
AsyncCache<String, Object> cache = Caffeine.newBuilder()
.executor(new ForkJoinPool(10))
//生成异步缓存
.buildAsync();
这里我们提出如下问题:
到这里我们基本把创建说完了,接下来看一下使用这些缓存:
Cache<String, String> syncCache = Caffeine.newBuilder().build(); //加入缓存 syncCache.put(key, value); //批量加入 syncCache.putAll(keyValueMap); //读取缓存,如果不存在,则执行后面的mappingFunction读取并放入缓存 syncCache.get(key, k -> { return readFromOther(k); }); //批量读取 syncCache.getAll(keys, ks -> { return readFromOther(k); }); //获取缓存配置信息,以及其他维度的信息 Policy<String, String> policy = syncCache.policy(); //获取统计信息,前提是必须打开统计 CacheStats stats = syncCache.stats(); //获取某个key,如果不存在则返回null syncCache.getIfPresent(key); //将map转换为map,对map的修改会影响缓存 ConcurrentMap<@NonNull String, @NonNull String> map = syncCache.asMap(); //让某个key生效 syncCache.invalidate(key); //让所有key失效 syncCache.invalidateAll(); //批量失效 syncCache.invalidateAll(keys); //估计大小 @NonNegative long estimatedSize = syncCache.estimatedSize(); //等待过期清理任务完成,让缓存处于一个稳定状态 syncCache.cleanUp();
这里只提了同步缓存,异步缓存的 API 类似,只是取值变成了 CompletableFuture
包装的
接下来的章节,我们会深入研究 Caffeine 的源代码和实现原理及思想
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。