Files
rikako-note/spring/caffeine/caffeine.md

11 KiB
Raw Blame History

caffeine

caffeine是一个高性能的java缓存库其几乎能够提供最佳的命中率。
cache类似于ConcurrentMap但并不完全相同。在ConcurrentMap中会维护所有添加到其中的元素直到元素被显式移除而Cache则是可以通过配置来自动的淘汰元素从而限制cache的内存占用。  

Cache

注入

Cache提供了如下的注入策略

手动

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .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接口可以显示的操作缓存条目的获取、失效、更新。
条目可以直接通过cache.put(key,value)来插入到缓存中该操作会覆盖已存在的key对应的条目。
也可以使用cache.get(key,k->value)的形式来对缓存进行插入该方法会在缓存中查找key对应的条目如果不存在会调用k->value来进行计算并将计算后的将计算后的结果插入到缓存中。该操作是原子的。如果该条目不可计算会返回null如果计算过程中发生异常则是会抛出异常。
除上述方法外也可以通过cache.asMap()返回map对象并且调用ConcurrentMap中的接口来对缓存条目进行修改。

Loading

// build方法可以指定一个CacheLoader参数
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .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<Key, Graph> graphs = cache.getAll(keys);

LoadingCache和CacheLoader相关联。
可以通过getAll方法来执行批量查找默认情况下getAll方法会为每个cache中不存在的key向CacheLoader.load发送一个请求。当批量查找比许多单独的查找效率更加高时可以重写CacheLoader.loadAll方法。

异步(手动)

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .buildAsync();

// Lookup an entry, or null if not found
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// Lookup and asynchronously compute an entry if absent
graph = cache.get(key, k -> createExpensiveGraph(key));
// Insert or update an entry
cache.put(key, graph);
// Remove an entry
cache.synchronous().invalidate(key);

AsyncCache允许异步的计算条目并且返回CompletableFuture。
AsyncCache可以调用synchronous方法来提供同步的视图。
默认情况下executor是ForkJoinPool.commonPool()可以通过Caffeine.executor(threadPool)来进行覆盖。

Async Loading

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .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> graph = cache.get(key);
// Lookup and asynchronously compute entries that are absent
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

AsyncLoadingCache是一个AsyncCache加上一个AsyncCacheLoader。
同样地AsyncCacheLoader支持重写load和loadAll方法。

淘汰

Caffeine提供了三种类型的淘汰基于size的基于时间的基于引用的。

基于时间的

// Evict based on the number of entries in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .build(key -> createExpensiveGraph(key));

// Evict based on the number of vertices in the cache
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumWeight(10_000)
    .weigher((Key key, Graph graph) -> graph.vertices().size())
    .build(key -> createExpensiveGraph(key));

如果你的缓存不应该超过特定的容量限制,应该使用Caffeine.maximumSize(long)方法。该缓存会对不常用的条目进行淘汰。
如果每条记录的权重不同,那么可以通过Caffeine.weigher(Weigher)指定一个权重计算方法,并且通过Caffeine.maximumWeight(long)指定缓存最大的权重值。

基于时间的淘汰策略

// Evict based on a fixed expiration policy
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Evict based on a varying expiration policy
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      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));

caffine提供三种方法来进行基于时间的淘汰

  • expireAfterAccess(long, TimeUnit):基于上次读写操作过后的时间来进行淘汰
  • expireAfterWrite(long, TimeUnit):基于创建时间、或上次写操作执行的时间来进行淘汰
  • expireAfter(Expire):基于自定义的策略来进行淘汰 过期在写操作之间周期性的进行触发偶尔也会在读操作之间进行出发。调度和发送过期事件都是在o(1)时间之内完成的。
    为了及时过期,而不是通过缓存活动来触发过期,可以通过Caffeine.scheuler(scheduler)来指定调度线程  

基于引用的淘汰策略

// Evict when neither the key nor value are strongly reachable
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> createExpensiveGraph(key));

// Evict when the garbage collector needs to free memory
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .softValues()
    .build(key -> createExpensiveGraph(key));

caffeine允许设置cache支持垃圾回收通过使用为key指定weak reference为value制定soft reference

移除

可以通过下述方法显式移除条目:

// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()

removal listener

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();

在entry被移除时可以指定listener来执行一系列操作通过Caffeine.removalListener(RemovalListener)。操作是通过Executor异步执行的。
当想要在缓存失效之后同步执行操作时,可以使用Caffeine.evictionListener(RemovalListener).该监听器将会在RemovalCause.wasEvicted()时被触发

compute

通过computecaffeine可以在entry创建、淘汰、更新时原子的执行一系列操作

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .evictionListener((Key key, Graph graph, RemovalCause cause) -> {
      // atomically intercept the entry's eviction
    }).build();

graphs.asMap().compute(key, (k, v) -> {
  Graph graph = createExpensiveGraph(key);
  ... // update a secondary store
  return graph;
});

统计

通过Caffeine.recordStats()方法,可以启用统计信息的收集,cache.stats()方法将返回一个CacheStats对象提供如下接口

  • hitRate():返回请求命中率
  • evictionCount():cache淘汰次数
  • averageLoadPenalty():load新值花费的平均时间
Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

cleanup

默认情况下Caffine并不会在自动淘汰entry后或entry失效之后立即进行清理而是在写操作之后执行少量的清理工作如果写操作很少则是偶尔在读操作后执行少量读操作。
如果你的缓存是高吞吐量的,那么不必担心过期缓存的清理,如果你的缓存读写操作都比较少,那么需要新建一个外部线程来调用Cache.cleanUp()来进行缓存清理。

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .scheduler(Scheduler.systemScheduler())
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

scheduler可以用于及时的清理过期缓存

软引用和弱引用

caffeine支持基于引用来设置淘汰策略。caffeine支持针对key和value使用弱引用针对value使用软引用。

weakKeys

caffeine.weakKeys()存储使用弱引用的key如果没有强引用指向key那么key将会被垃圾回收。垃圾回收时只会比较对象地址故而整个缓存在比较key时会通过==而不是equals来进行比较

weakValues

caffeine.weakValues()存储使用弱引用的value如果没有强引用指向valuevalue会被垃圾回收。同样地在整个cache中会使用==而不是equals来对value进行比较

softValues

软引用的value通常会在垃圾回收时按照lru的方式进行回收根据内存情况决定是否进行回收。由于使用软引用会带来性能问题通常更推荐使用基于max-size的回收策略。
同样地基于软引用的value在整个缓存中会通过==而不是equals()来进行垃圾回收。