Caffeine 是一个高性能、出色的缓存类库,基于Java 8。它的性能非常的出色,API也比较友好,本篇,我们就来介绍一下Caffeine 使用。
Caffeine使用的是一个内存缓存,是基于Google 的 Guava与ConcurrentLinkedHashMap进行实现的。
Maven地址:
com.github.ben-manes.caffeine
caffeine
2.7.0
我们首先来看一个其使用的demo:
LoadingCache graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
就这样,我们即可创建一个缓存结构,其中createExpensiveGraph()方法是我们自行定义的,用于生成缓存的逻辑,其他的方法我们将会在后面进行依次介绍。
Caffeine 提供了多种构建方式创建一个缓存,我们来看一下它的主要特性:
以上就是Caffeine 的主要特性,接下来,我们就对上面的特性中比较常用的几个,进行展开详细介绍一下。
缓存加载是Caffeine 的最基础特性,其支持四中模式的加载策略:手工加载、同步加载、异步加载、异步手动加载。
首先,我们来看一下手动加载:
//Build a manual cache
Cache cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
// Lookup an entry, or null if not found
Graph graph = cache.getIfPresent(key);
// Lookup and compute an entry if absent, or null if not computable
graph = cache.get(key, k -> createExpensiveGraph(key));
// Insert or update an entry
cache.put(key, graph);
// Remove an entry
cache.invalidate(key);
//Cache entry view
cache.asMap();
上面的demo就是手动加载一个缓存元素的流程,Cache 接口允许精确的控制检索、更新与废弃一个元素。
在构建过程中,我们可以指定key值的失效时间,以及缓存的最大容量。
使用cache.get(key, k -> createExpensiveGraph(key))时,会首先检查缓存中是否已经key值对应的元素值,如果不存在,通过createExpensiveGraph()这个自定义的方法来初始化一个元素,放入缓存中来。
使用手工加载的方式可以给我们带来更大的灵活性,但是总是手动去加载缓存,有时未免有些不便,这种情况下,我们可以使用自动同步加载的方式。
//Build a loading synchronously cache
LoadingCache cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// Lookup and compute an entry if absent, or null if not computable
Graph graph = cache.get(key);
// Lookup and compute entries that are absent
Map graphs = cache.getAll(keys);
同步加载模式的构建与手动加载模式的构建方式与使用非常相似,只不过使用是LoadingCache接口进行的构建。
LoadingCache支持批量获取缓存,可以通过getAll()方法,默认的情况下,getAll()方法的实现将会循环调用get()方法,获取缓存值,如果批量获取的key值过多时,需要考虑性能;同时,也可以通过实现CacheLoader.loadAll()方法,自行实现批量获取缓存的逻辑。
//Build a manual asynchronous cache
AsyncCache cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.buildAsync();
// Lookup and asynchronously compute an entry if absent
CompletableFuture graph = cache.get(key, k -> createExpensiveGraph(key));
AsyncCache 是 Cache 的一个变体类,其内部使用一个Executor进行实现,通过get()方法返回一个CompletableFuture类,通过这个方法可以构建响应式编程模型。
内部默认使用的executor 是 ForkJoinPool.commonPool(),可以通过重写Caffeine.executor(Executor)来自行指定线程池。
看完了上面几种缓存加载方式,我们接下来看一下最后一种加载方式,也是我个人最推荐使用的加载方式,异步加载:
//Build a loading asynchronous cache
AsyncLoadingCache cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// Either: Build with a synchronous computation that is wrapped as asynchronous
.buildAsync(key -> createExpensiveGraph(key));
// Or: Build with a asynchronous computation that returns a future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// Lookup and asynchronously compute an entry if absent
CompletableFuture graph = cache.get(key);
// Lookup and asynchronously compute entries that are absent
CompletableFuture
异步加载是通过AsyncLoadingCache接口实现的,构建方式基本与同步加载相同,只不过需要注意的是,其额外提供了buildAsync((key, executor))的构造方式,可以支持传入Executor执行器。
我们在最前面提到过,Caffeine是支持淘汰策略的,其支持三种策略:基于大小、基于过期时间、基于引用,我们分别来介绍一下。
// Evict based on the number of entries in the cache
LoadingCache graphs = Caffeine.newBuilder()
.maximumSize(1000)
.build(key -> createExpensiveGraph(key));
当构建一个缓存的时候,我们可以通过maximumSize()方法来指定缓存的最大容量,当到达容量阈值时,Caffeine 将基于Window TinyLfu策略进行缓存淘汰。根据Caffeine 的文档描述,它并没有采用比较常见的LRU淘汰策略,是因为LRU的淘汰策略命中率比较低,有可能会触发全量扫描。采用Window TinyLfu的原因是其拥有较高的命中率,同时较少的内存使用。
这种模式的淘汰策略是比较推荐的。
同时,我们还可以使用另外一种方式来指定淘汰策略,使用权重的方式。
// Evict based on the number of vertices in the cache
LoadingCache graphs = Caffeine.newBuilder()
.maximumWeight(1000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
可以使用maximumWeight()方法来指定最大权重值,但是关于权重的淘汰策略,其官方文档的描述我没有太理解到位,该方式也不是特别常用,这里就不过多的说明,以防误导读者,感兴趣的小伙伴可以自行查询 官方文档的说明。
// Evict based on a fixed expiration policy
LoadingCache graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// Evict based on a varying expiration policy
LoadingCache graphs = Caffeine.newBuilder()
.expireAfter(new Expiry() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));
Caffeine 提供了三种基于时间的淘汰策略:
// Evict when neither the key nor value are strongly reachable
LoadingCache graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
// Evict when the garbage collector needs to free memory
LoadingCache graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
Caffeine 支持设定你的缓存元素为软引用或弱引用,这样可以更加方便与GC回收元素。需要注意的是,此种方式不支持AsyncLoadingCache 接口构建缓存。
关于这种方式的缓存构建,并不是特别的常用,也并不是特别的推荐,这里不占用过多篇幅叙述。
刚刚我们了解了缓存的淘汰策略,淘汰策略是我们在构建缓存结构时,进行设定的,同时,我们也可以手动的移除缓存。
Caffeine 为我们提供了方法,来灵活操控移除缓存元素。
// 移除指定的key
cache.invalidate(key)
// 移除指定的key列表
cache.invalidateAll(keys)
// 移除全部key
cache.invalidateAll()
如果,你希望在元素被移除的时候,你可以知道它被移除了,那么,你可以设定一个监听器,来监听移除的行为:
Cache graphs = Caffeine.newBuilder()
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();
在非常多的场景下,我们希望缓存数据是需要在一定周期范围内能自动更新的,当底层的数据源变更后,缓存也可以进行相应的更新,这时,我们可以通过Caffeine 提供的refreshAfterWrite()方法,来进行实现:
LoadingCache graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
refreshAfterWrite()可以指定时间周期,在数据写入后,在指定时间后进行更新。这里的更新,与我们上面提到的淘汰机制有所不同,刷新数据,是一个异步行为,当在刷新数据的过程中,旧的缓存值依旧可以被读取到,而对于淘汰策略,当缓存元素失效后,必须等到新的数据写入完成后,新的缓存数据才可以被读取的到。
对比于expireAfterWrite()方法,refreshAfterWrite()方法是一个轻量级的数据更新,刷新的行为只有当一个元素被查询的时候,才进行触发。我们可以在构建缓存时,同时指定expireAfterWrite()方法与refreshAfterWrite()方法,这样的话,只有当数据具备刷新条件的时候才会去刷新数据,不会盲目去执行刷新操作。如果数据在刷新后就一直没有被再次查询,那么该数据也会过期。
上面说完了缓存的更新操作,接下来,我们再看一下缓存的写入。
LoadingCache graphs = Caffeine.newBuilder()
.writer(new CacheWriter() {
@Override
public void write(Key key, Graph graph) {
// write to storage or secondary cache
}
@Override
public void delete(Key key, Graph graph, RemovalCause cause) {
// delete from storage or secondary cache
}
})
.build(key -> createExpensiveGraph(key));
CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。
CacheWriter可以同步的监听到缓存的创建、变更和删除操作。
需要注意的是:CacheWriter 不能与weakKeys和AsyncLoadingCache一起使用。
上面的篇幅,我们了解了Caffeine的主要特性与使用方式,接下来我们看一个简单的例子,也是我在生产环境中使用的例子,希望为读者做一个简单的参考:
public class CaffeineCacheDemo {
AsyncLoadingCache asyncLoadingCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(key -> doQueryFromDB(key));
public Person doQueryFromStorage() {
//do query from redis
//redis do something......
//if not exists, then query from db, and write for redis
//db query
//write redis
}
}
这里我用一个非常简单的demo演示了Caffeine的使用,在生产环境中,建议使用AsyncLoadingCache这种异步的方式,来加载缓存,我们可以配合redis,来构建一个三级缓存的模型,具体的代码这里没有给出,请读者自行发挥,根据自己的实际业务逻辑,来构建多级缓存结构,这里只是抛砖引玉。
本篇,我们介绍了高性能缓存类库Caffeine的使用及其原理,由于篇幅有限,很多细节的点没有进行一一介绍,请读者在实际应用中使用的时候,再对其使用细节进行深入研究,本文只作为入门基础介绍。
该缓存经过我们生产环境的验证,证明是非常可靠、高效的,特别对于并发量要求比较大的应用,如果您的应用也有高并发的需求,请不妨尝试使用一下,好啦,就介绍到这里,我们下篇再见~