高性能缓存类库Caffeine介绍

介绍

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 提供了多种构建方式创建一个缓存,我们来看一下它的主要特性:

  • 自动加载数据放入缓存,支持异步方式
  • 基于缓存容量的淘汰策略,当存储的元素超过最大值的时候,根据使用元素最近的使用频率策略进行淘汰
  • 基于过期时间的淘汰策略
  • 异步刷新策略
  • key值自动包装成弱引用
  • 元素值自动包装成弱引用或软引用
  • 通知淘汰元素策略
  • 向外部存储资源写入元素
  • 统计缓存访问信息

以上就是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> graphs = cache.getAll(keys);

异步加载是通过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 提供了三种基于时间的淘汰策略:

  • expireAfterAccess(long, TimeUnit):缓存元素被创建后,将会在最后一次被读写访问后,在指定时间后过期。
    也就是说,如果一个元素被访问频度极高,那么其将一直不会过期。
  • expireAfterWrite(long, TimeUnit):缓存元素被创建后,在指定时间后过期。如果你需要缓存不停的更新变化,建议采用这种模式。
  • expireAfter(Expiry):缓存元素被创建后,按照自定义的过期时间策略,进行过期,这种方式相对比较灵活。

基于引用

// 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的使用及其原理,由于篇幅有限,很多细节的点没有进行一一介绍,请读者在实际应用中使用的时候,再对其使用细节进行深入研究,本文只作为入门基础介绍。
该缓存经过我们生产环境的验证,证明是非常可靠、高效的,特别对于并发量要求比较大的应用,如果您的应用也有高并发的需求,请不妨尝试使用一下,好啦,就介绍到这里,我们下篇再见~

你可能感兴趣的:(缓存,Caffeine缓存,Caffeine缓存使用,Caffeine缓存介绍,Caffeine,高性能缓存)