一,介绍
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。
缓存和ConcurrentMap有点相似,但还是有所区别。最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache
通常会被配置成自动驱逐缓存中元素,以限制其内存占用。在某些场景下,LoadingCache
和AsyncLoadingCache
因为其自动加载缓存的能力将会变得非常实用。
Maven坐标:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
二,快速入门
Cache<String, String> cache = Caffeine.newBuilder()
//5秒没有读写自动删除
.expireAfterAccess(5, TimeUnit.SECONDS)
//最大容量1024个,超过会自动清理空间
.maximumSize(1024)
.build();
//添加值
cache.put("key", "value");
//获取值,如果没有就返回null
cache.getIfPresent("张三key
//remove
cache.invalidate("key");
三,填充策略
填充策略是指如何在key不存在的情况下,如何创建一个对象进行返回,主要分为下面四种:
- 手动加载
- 自动加载
- 手动异步加载
- 自动异步加载
3.1 手动填充
Cache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
User user = cache.get("key1", key -> {
//本地查不到,一般去redis或者数据库查询
//查询的同时会将查询到的内容put到caffeine中
return new User("张三",1);
});
- 通过
cache.put(key, value)
方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据。 - 更加建议使用
cache.get(key,k - > value)
的方式,get 方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该键,则调用这个 Function 函数,并将返回值作为该缓存的值插入缓存中。get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。
注意:如果调用该方法返回NULL,则cache.get返回null,如果调用该方法抛出异常,则get方法也会抛出异常。
3.2 自动填充
LoadingCache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> {
//本地查不到,一般去redis或者数据库查询
//查询的同时会将查询到的内容put到caffeine中
return new User("张三",1);
});
//效果是跟手动填充一样的,只不过将查询代码放到了build里面
User user = cache.get("key1");
3.3 异步手动填充
AsyncCache<String, User> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<User> user1 = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
CompletableFuture<User> user2 = cache.get("key", key -> {
return new User("张三",12);
});
// 添加或者更新一个缓存元素
cache.put("key", new User("张三",20));
// 移除一个缓存元素
cache.synchronous().invalidate(key);
- 异步手动填充和手动填充的区别就是,没有查询到就会异步执行get里面的方法
- 默认的线程池实现是
ForkJoinPool.commonPool()
(,当然你也可以通过覆盖并实现Caffeine.executor(Executor)
方法来自定义你的线程池选择。
3.4 异步自动填充
AsyncLoadingCache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(key -> {
//本地查不到,一般去redis或者数据库查询
//查询的同时会将查询到的内容put到caffeine中
return new User("张三",13);
});
// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<User> user = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<String, User>> graphs = cache.getAll(keys);
- 异步自动填充和异步手动填充区别就是将查询放到了build里面
四,驱逐策略
Caffeine提供三类驱逐策略:
- 基于大小(size-based)
- 基于时间(time-based)
- 基于引用(reference-based)
4.1 基于大小
基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。
// 基于缓存内的元素个数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));
// 基于缓存内元素权重进行驱逐
LoadingCache<String, User> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((String key, User user) -> user.getAge())
.build(key -> createExpensiveGraph(key));
4.2 基于时间
// 基于固定的过期时间驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// 基于不同的到期策略进行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
// 设置自定义的到期策略
.expireAfter(new Expiry<Key, Graph>() {
// 创建时的到期策略
@Override
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// 使用挂钟时间,而不是纳秒时间,如果数据来自外部资源
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
// 返回到期时间(以纳秒为单位)
return TimeUnit.SECONDS.toNanos(seconds);
}
// 更新时的到期策略
@Override
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
// 保持当前的到期时间不变
return currentDuration;
}
// 读取时的到期策略
@Override
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
// 保持当前的到期时间不变
return currentDuration;
}
})
// 创建缓存对象时,调用该方法生成 Graph 实例
.build(key -> createExpensiveGraph(key));
Caffeine提供了三种定时驱逐策略:
expireAfterAccess(long, TimeUnit)
:在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。expireAfterWrite(long, TimeUnit)
: 在最后一次写入缓存后开始计时,在指定的时间后过期。expireAfter(Expiry)
: 自定义策略,过期时间由Expiry实现独自计算。
4.3 基于引用
// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
-
Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住
AsyncCache
不支持软引用和弱引用。 -
Caffeine.weakKeys()
在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等equals()
去进行key之间的比较。 -
Caffeine.weakValues()
在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等equals()
去进行value之间的比较。 -
Caffeine.softValues()
在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用softValues()
将会通过引用相等(==)而不是对象相等equals()
去进行value之间的比较。
五,移除
概念:
- 驱逐 缓存元素因为策略被移除
- 失效 缓存元素被手动移除
- 移除 由于驱逐或者失效而最终导致的结果
5.1 显式移除
// 失效key
cache.invalidate(key)
// 批量失效key
cache.invalidateAll(keys)
// 失效所有的key
cache.invalidateAll()
5.2 移除监听器
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.evictionListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was evicted (%s)%n", key, cause))
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
- 通过
Caffeine.removalListener(RemovalListener)
方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor 异步执行的 - 默认的 Executor 实现是
ForkJoinPool.commonPool()
并且可以通过覆盖Caffeine.executor(Executor)
方法自定义线程池的实现。
六,刷新
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
// 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
.refreshAfterWrite(Duration.ofMinutes(1))
.build(key -> createExpensiveGraph(key));
-
刷新和驱逐并不相同。可以通过
LoadingCache.refresh(K)
方法,异步为key对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。 -
与expireAfterWrite不同的是,refreshAfterWrite将在查询数据的时候判断该数据是不是符合查询条件,如果符合条件该缓存就会去执行刷新操作。
可以在同一个缓存中同时指定refreshAfterWrite和expireAfterWrite,只有当数据具备刷新条件的时候才会去刷新数据,不会盲目去执行刷新操作。如果数据在刷新后就一直没有被再次查询,那么该数据也会过期。
-
刷新操作是使用Executor异步执行的。默认执行程序是
ForkJoinPool.commonPool()
,可以通过Caffeine.executor(Executor)
覆盖。 -
如果刷新时引发异常,则使用log记录日志,并不会抛出。
七,统计
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
使用Caffeine.recordStats(),您可以打开统计信息收集。Cache.stats() 方法返回提供统计信息的CacheStats,如:
hitRate()
:返回命中与请求的比率hitCount()
: 返回命中缓存的总数evictionCount()
:缓存逐出的数量averageLoadPenalty()
:加载新值所花费的平均时间
八,策略
在创建缓存的时候,缓存的策略就指定好了。但是我们可以在运行时可以获得和修改该策略。这些策略可以通过一些选项来获得,以此来确定缓存是否支持该功能。
8.1 Size-based
cache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(2 * eviction.getMaximum());
});
-
如果缓存配置的时基于权重来驱逐,那么我们可以使用
weightedSize()
来获取当前权重。这与获取缓存中的记录数的Cache.estimatedSize()
方法有所不同。 -
缓存的最大值(maximum)或最大权重(weight)可以通过
getMaximum()
方法来读取,并使用setMaximum(long)进行调整。当缓存量达到新的阀值的时候缓存才会去驱逐缓存。 -
如果有需用我们可以通过
hottest(int)
和coldest(int)
方法来获取最有可能命中的数据和最有可能驱逐的数据快照。
8.2 Time-based
cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);
ageOf(key,TimeUnit)
提供了从expireAfterAccess,expireAfterWrite或refreshAfterWrite策略的角度来看条目已经空闲的时间。最大持续时间可以从getExpiresAfter(TimeUnit)
读取,并使用setExpiresAfter(long,TimeUnit)
进行调整。
如果有需用我们可以通过hottest(int)
和 coldest(int)
方法来获取最有可能命中的数据和最有可能驱逐的数据快照。
九,CleanUp
缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()
方法手动触发一次回收操作。cache.cleanUp()
是一个同步方法。