Caffeine
本文最后更新于 1446 天前,其中的信息可能已经有所发展或是发生改变。

简介

Caffeine是一个基于Java8高性能的缓存库。提供近似完美命中率。

这个缓存和ConcurrentMap很相似,但是并不相同。最根本的区别是ConcurrentMap会一直存储着元素直到元素被显式移除。相反,Cache会设置为自动清除元素,来控制缓存所占用的空间。在有些业务场景LoadingCacheAsyncLoadingCache缓存即使不设置自动清除元素,也可以很有效,因为它可以自动加载缓存。

Caffeine提供一个灵活的构造器来构建具有以下功能组合的缓存:

加载

Caffeine 提供三种类型的加载策略: 手动加载,同步加载,异步加载。

手动加载

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();
// 获取一个元素,如果找不到返回null
Graph graph = cache.getIfPresent(key);
// 获取一个元素,如果不存在就计算值, 如果无法完成计算就返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 插入或更新一个键
cache.put(key, graph);
// 清除一个元素
cache.invalidate(key);

这个Cache 接口允许显式读取、更新、清除元素。

可以调用cache.put(key, value)直接将元素插入缓存,这会直接覆盖掉之前映射的值,建议使用cache.get(key, k -> value)通过原子方式计算值并插入到缓存中,从而避免与其他写入者竞争。需要注意的是cache.get如果无法完成计算会返回null,如果计算出现错误会抛出异常。

也可以通过Cache.asMap()方法返回的ConcurrentMap视图的公共方法来修改元素。

同步加载

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));
// 获取一个元素,如果不存在就运行方法计算, 如果不能计算就返回null
Graph graph = cache.get(key);
// 查找并计算不存在的键
Map<Key, Graph> graphs = cache.getAll(keys);

LoadingCache是一个通过CacheLoader构建的Cache

可以使用getAll方法进行批量读取。默认情况下,getAll将会单独调用CacheLoader.load来加载缓存中每个缺少键。如果批量加载比多个单独加载更高效时, 你可以重写CacheLoader.loadAll来实现批量加载。

需要注意的是,你可以实现一个CacheLoader.loadAll来加载没有被请求过的元素,如果计算一个键就可获得整个组的元素,loadAll可以将整个组加入缓存。

异步加载

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    // 可以: 通过一个包装为异步的同步计算进行构建
    .buildAsync(key -> createExpensiveGraph(key));
    // 也可以: 通过异步计算返回的future进行构建
    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找并异步计算元素(如果这个元素不存在)
CompletableFuture<Graph> graph = cache.get(key);
// 查找并异步计算多个元素(如果有元素不存在)
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

AsyncLoadingCache是一个LoadingCache变体,通过Executor(线程池)进行元素计算并返回一个CompletableFuture。这允许在流行的响应式编程框架中使用该缓存。

当计算可以通过同步方法很好的实现时,需要提供一个CacheLoader,或者,当计算可以异步实现并返回CompletableFuture时,应该提供一个AsyncCacheLoader

这个synchronous()提供一个LoadingCache视图,他可以阻塞异步计算直到计算完成。

缓存默认的线程池是ForkJoinPool.commonPool()我们可以通过Caffeine.executor(Executor)方法设置特定的线程池。

清除

Caffeine 提供三种缓存清除策略: 基于缓存大小进行清除,基于时间进行清除,和基于引用进行清除。

基于容量的的清除(size-based eviction)

// 通过缓存里的元素数量来判断是否清除
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .build(key -> createExpensiveGraph(key));

// 通过缓存里的顶点数量来判断是否清除
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)指定缓存的最大权重。除了和maximumSize相同的注意事项外,还要注意元素的权重是在创建元素和更新元素时创建的,之后就不发生变化,在选择清除元素时不会参考相对权重。

定时清除(Timed Eviction)

// 通过统一过期策略来驱逐元素
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));

// 通过不同场景过期策略来驱逐元素
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      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);
      }
      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):如果元素达到指定时间后,将会被清除,当元素是否过期需要外部资源来确定的时候,需要用到该方法。

缓存会在每次写入操作和偶尔的读取操作执行定期的维护,调度和清除的时间会平均到O(1)的时间复杂度上。

测试时间驱逐不用等到系统到达相应时间,使用Ticker接口和Caffeine.ticker(Ticker)在缓存构建的时候指定一个时间源,这样就不必等待系统时钟了。

基于引用清除(Reference-based Eviction)

// 当键和值都不是强引用时清除
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> createExpensiveGraph(key));

// 当垃圾回收器需要释放内存时清除。
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .softValues()
    .build(key -> createExpensiveGraph(key));

Caffeine允许你通过对键或值使用弱引用或对值使用软引用,从而允许对条目进行垃圾回收。需要注意的是AsyncLoadingCache不支持值的弱引用和软引用。

Caffeine.weakKeys()使用弱引用来存储键。如果这个键没有其他的强引用,将会被垃圾回收。因为垃圾回收只依赖于对象的引用相同,这将导致整个缓存使用引用(==)相同来比较键,而不是equals()

Caffeine.weakValues()使用弱引用来存储值. 如果这个值没有强引入,将会被垃圾回收。因为垃圾回收只依赖于对象的引用相同,这将导致整个缓存使用引用(==)相同来比较值,而不是equals()

Caffeine.softValues()通过软引用来存储值,软引用只有在响应内存需要时,才按照全局最近最少使用的顺序清除。因为软引用对性能的影响,我们通常建议使用更可预测的最大缓存大小来代替软引用。使用softValues()将导致整个缓存使用引用(==)相同来比较值,而不是equals()

显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收。

// 清除一个
cache.invalidate(key)
// 批量清除
cache.invalidateAll(keys)
// 清空缓存
cache.invalidateAll()

移除监听器

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .removalListener((Key key, Graph graph, RemovalCause cause) ->
        System.out.printf("Key %s was removed (%s)%n", key, cause))
    .build();

你可以通过Caffeine.removalListener(RemovalListener)来实现一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获得移除元素的键,值和RemovalCause

移除监听器会使用Executor异步执行。默认的线程池是ForkJoinPool.commonPool(),可以通过 Caffeine.executor(Executor)方法指定线程池。如果我们想要移除操作和监听器同步执行, 可以用 CacheWriter来代替。

请注意RemovalListener抛出的任何异常都会在记录到日志后(使用Logger)被丢弃。

清理什么时候发生?

Caffeine构建的缓存不会”自动”执行清理和执行回收工作,也不会在某个缓存项过期后马上清理。恰恰相反,它在写操作之后执行少量维护,如果写操作很少,则偶尔在读操作后执行维护。这个维护操作被委托给Executor,默认的线程池是ForkJoinPool.commonPool(), 我们可以通过Caffeine.executor(Executor)指定线程池。

这样做的原因在于:如果要自动地持续清理缓存,我们就需要创建一个线程,这个线程会在每次操作中锁定数据。这会使得Caffeine在一些限制创建线程的环境中不能正常使用。(翻译不一定准确)

相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用cache.cleanUp()

如果你想计划定期维护一个不经常操作的缓存,你可以使用ScheduledExecutorService线程池。

刷新

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

刷新和清除并不相同。如上所示LoadingCache.refresh(K), 刷新方法会异步加载元素的新值。旧值(如果存在的话)仍然会在刷新没有结束的时候被返回,相对于清除,后者将会强制阻塞读取直到重新加载该值。

expireAfterWrite相比,refreshAfterWrite可以使元素定时刷新,但只有在读取元素的时候才进行刷新。如果你在缓存上同时声明refreshAfterWriteexpireAfterWrite这样当元素有资格刷新时。清除计时器不会盲目的重置。如果一个元素在符合刷新条件后没有被读取。他才会被清除。

可以通过重写CacheLoaderCacheLoader.reload(K, V)来指定要在刷新时的行为,这允许你在计算新值的时候使用旧值。

使用Executor来异步执行刷新操作。默认的线程池是ForkJoinPool.commonPool()可以通过Caffeine.executor(Executor)来指定线程池。

如果在刷新时抛出异常,会保留原来的值,抛出的任何会在记录到日志后(使用Logger)丢弃。

动态修改清除策略

虽然缓存的清除策略在构建的时候就固定了,我们也可以在缓存运行的时候进行查看和修改策略。通过返回的Optional可以判断缓存是否支持该策略并修改策略。

基于容量清除

cache.policy().eviction().ifPresent(eviction -> {
  eviction.setMaximum(2 * eviction.getMaximum());
});

如果缓存设置了最大权重,则可以使用weightedSize()获得缓存的当前权重。这与Cache.estimatedSize()返回的元素数量不同。

最大元素数量和元素权重可以通过getMaximum()setMaximum(long)进行获得和修改。修改后缓存将清除元素,直到满足设置的要求。

如果想要获取最可能被保留和清除元素的集合,可以通过hottest(int)coldest(int)方法来获得指定大小的有序快照集合。

定时清除

cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);

ageOf(key, TimeUnit)可以从expireAfterAccess,expireAfterWriterefreshAfterWrite策略的角度看一个键已经空闲了多长时间。最大空闲时间可以通过getExpiresAfter(TimeUnit)setExpiresAfter(long, TimeUnit)方法进行获取和修改。

如果需要获得最可能被保留或过期的元素集合,可以通过youngest(int)oldest(int)方法来获得最指定大小的有序快照集合。

写入监听器

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
  .writer(new CacheWriter<Key, Graph>() {
    @Override
    public void write(Key key, Graph graph) {
      // 写入存储器或二级缓存
    }
    @Override
    public void delete(Key key, Graph graph, RemovalCause cause) {
      // 从存储器或二级缓存中删除
    }
  })
  .build(key -> createExpensiveGraph(key));

CacheWriter允许缓存充当底层资源的门面,当和CacheLoader结合使用时,所有读和写操作都可以通过缓存进行传播。开发者可以通过扩展缓存的原子操作,从而实现对外部资源的同步。这意味着缓存将会阻塞元素的后续修改操作,而且读取操作将会返回之前的值,直到写入完成。如果写入器失败,映射将保持不变,异常将抛出给调用者。

CacheWriter会在元素创建,修改和移除时被调用。一个元素被加载(e.g.LoadingCache.get),刷新(e.g.LoadingCache.refresh),计算(e.g.Map.computeIfPresent)不会调用监听器。

需要注意CacheWriter不可以和weak keysAsyncLoadingCache一起使用。

业务场景

如果一个复杂的工作流需要外部资源来记录给定键的连续变化记录,可以通过CacheWriter实现 ,Caffeine支持这些用法,但并没有内置在缓存中。

写模式

CacheWriter可以用来实现直写式(write-through cache)或回写式(write-through cache)缓存。

直写式缓存操作是同步执行的,只有在写入成功完成之后才会更新缓存。这样就会避免资源和缓存两个独立的原子操作之间的竞态条件。

回写式缓存 对外部资源的操作实在缓存更新后异步执行的。这可以提高缓存写入的吞吐量,但会存在数据不一致的风险,例如写入失败的时候会在缓存中保留无效数据。此类缓存可用于将写入延迟到特定时间,限制写入速率或批量写入操作。

一个回写扩展可能会实现以下部分或全部功能:

  • 批量操作,合并操作
  • 将操作推迟到特定时间段
  • 如果操作数量超出了指定数量,就会在定时刷新前执行。
  • 如果操作没有被刷新到外部资源,则会在写后缓冲区(write-behind buffer)读取数据。
  • 根据外部资源的属性来限制重试频率,速度和并发数。

分层

CacheWriter可以用来集成多个缓存层

多级缓存缓存可以从记录系统支持的外部缓存加载和写入。这允许一个小的快速的缓存和一个大的慢速缓存,典型的多级缓存是堆外缓存,文件缓存和远程缓存。

牺牲缓存(victim cache)是一个多级缓存的一个变种,当元素被驱逐后,将会写入辅助缓存中。delete(K, V, RemovalCause) 方法允许查看元素删除的原因并进行相应的操作。

同步监听器

CacheWriter可以用来推送信息到同步监听器

同步监听器(synchronous listener)可以接收给定键在缓存上发生的事件通知,监听器可以阻塞缓存的操作或对异步执行的操作排序。这类监听器常用于主从复制和构建分布式缓存。

其他功能

统计

Cache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .recordStats()
    .build();

通过Caffeine.recordStats()你可以打开统计信息收集器。 Cache.stats() 方法返回一个 CacheStats ,他可以提供统计方法例如:

  • hitRate():返回缓存的命中率
  • evictionCount():缓存清除元素的数量
  • averageLoadPenalty():加载新值所消耗的平均时间

这些信息在缓冲调优的过程中至关重要,我们建议在性能优先的应用中,应该密切关注这些信息。

可以使用基于拉和推的方法将缓存信息与报告系统集成。如果基于拉,需要定期调用Cache.stats()并记录当前状态。如果基于推,需要提供自定义的StatsCounter以便在缓存运行期间直接更新数值。

测试

FakeTicker ticker = new FakeTicker(); // Guava's testlib
Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .executor(Runnable::run)
    .ticker(ticker::read)
    .maximumSize(10)
    .build();

cache.put(key, graph);
ticker.advance(30, TimeUnit.MINUTES)
assertThat(cache.getIfPresent(key), is(nullValue()));

测试指定时间驱逐策略不用无意义的等待系统时间。可以使用Ticker接口和Caffeine.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟Guava’s testlib提供了一个 FakeTicker方便的实现。因为缓存需要在维护时才清除过期元素,我们可以在确定测试元素已经过期时执行Cache.cleanUp()方法立即清除元素。

Caffeine 将定期维护,清除监听器和异步计算委托给Executor。通过不阻塞和默认使用ForkJoinPool.commonPool()缓冲池来提供更可预测的响应时间。可以使用Caffeine.executor(Executor)方法在缓存构建的时候指定一个direct(same thread)executor,这样你就不用等待异步任务完成了。

我们建议使用Awaitility来进行多线程测试。

原创声明
本文由 makese 于2019年01月15日发表在 我的博客
如未特殊声明,本站所有文章均为原创;你可以在保留作者及原文地址的情况下转载
转载请注明:Caffeine | 我的博客
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇