专栏首页TECH flower[译]高性能缓存库Caffeine介绍及实践

[译]高性能缓存库Caffeine介绍及实践

概览

本文我们将介绍Caffeine-一个Java高性能缓存库。缓存和Map之间的一个根本区别是缓存会将储存的元素逐出。逐出策略决定了在什么时间应该删除哪些对象,逐出策略直接影响缓存的命中率,这是缓存库的关键特征。Caffeine使用Window TinyLfu逐出策略,该策略提供了接近最佳的命中率。

添加依赖

首先在pom.xml文件中添加Caffeine相关依赖:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>

您可以在Maven Central上找到最新版本的Caffeine。

缓存填充

让我们集中讨论Caffeine的三种缓存填充策略:手动,同步加载和异步加载。

首先,让我们创建一个用于存储到缓存中的DataObject类:

class DataObject {
    private final String data;
 
    private static int objectCounter = 0;
    // standard constructors/getters
     
    public static DataObject get(String data) {
        objectCounter++;
        return new DataObject(data);
    }
}

手动填充

在这种策略中,我们手动将值插入缓存中,并在后面检索它们。

让我们初始化缓存:

Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();

现在,我们可以使用getIfPresent方法从缓存中获取值。如果缓存中不存在该值,则此方法将返回null:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);

我们可以使用put方法手动将值插入缓存:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
 
assertNotNull(dataObject);

我们还可以使用get方法获取值,该方法将Lambda函数和键作为参数。如果缓存中不存在此键,则此Lambda函数将用于提供返回值,并且该返回值将在计算后插入缓存中:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));
 
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

get方法以原子方式(atomically)执行计算。这意味着计算将只进行一次,即使多个线程同时请求该值。这就是为什么使用get比getIfPresent更好。

有时我们需要手动使某些缓存的值无效:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);

同步加载

这种加载缓存的方法具有一个函数,该函数用于初始化值,类似于手动策略的get方法。让我们看看如何使用它。

首先,我们需要初始化缓存:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

现在,我们可以使用get方法检索值:

DataObject dataObject = cache.get(key);
 
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());

我们还可以使用getAll方法获得一组值:

Map<String, DataObject> dataObjectMap
  = cache.getAll(Arrays.asList("A", "B", "C"));
 
assertEquals(3, dataObjectMap.size());

从传递给build方法的初始化函数中检索值。这样就可以通过缓存在来装饰访问值。

异步加载

该策略与先前的策略相同,但是异步执行操作,并返回保存实际值的CompletableFuture:

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));

考虑到它们返回CompletableFuture的事实,我们可以以相同的方式使用get和getAll方法:

String key = "A";
 
cache.get(key).thenAccept(dataObject -> {
    assertNotNull(dataObject);
    assertEquals("Data for " + key, dataObject.getData());
});
 
cache.getAll(Arrays.asList("A", "B", "C"))
  .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

CompletableFuture具有丰富而有用的API,您可以在本文中了解更多信息。

逐出元素

Caffeine具有三种元素逐出策略:基于容量,基于时间和基于引用。

基于容量的逐出

这种逐出发生在超过配置的缓存容量大小限制时。有两种获取容量当前占用量的方法,计算缓存中的对象数量或获取它们的权重。

让我们看看如何处理缓存中的对象。初始化高速缓存时,其大小等于零:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));
 
assertEquals(0, cache.estimatedSize());

当我们添加一个值时,大小显然会增加:

cache.get("A");
 
assertEquals(1, cache.estimatedSize());

我们可以将第二个值添加到缓存中,从而导致删除第一个值:

cache.get("B");
cache.cleanUp();
 
assertEquals(1, cache.estimatedSize());

值得一提的是,在获取缓存大小之前,我们先调用cleanUp方法。这是因为缓存逐出是异步执行的,并且此方法有助于等待逐出操作的完成。

我们还可以传递一个weigher函数来指定缓存值的权重大小:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumWeight(10)
  .weigher((k,v) -> 5)
  .build(k -> DataObject.get("Data for " + k));
 
assertEquals(0, cache.estimatedSize());
 
cache.get("A");
assertEquals(1, cache.estimatedSize());
 
cache.get("B");
assertEquals(2, cache.estimatedSize());

当权重超过10时,将按照时间顺序从缓存中删除多余的值:

cache.get("C");
cache.cleanUp();
 
assertEquals(2, cache.estimatedSize());

基于时间的逐出

此逐出策略基于元素的到期时间,并具有三种类型:

  • Expire after access — 自上次读取或写入发生以来,经过过期时间之后该元素到期。
  • Expire after write — 自上次写入以来,在经过过期时间之后该元素过期。
  • Custom policy — 通过Expiry实现分别计算每个元素的到期时间。

让我们使用expireAfterAccess方法配置访问后过期策略:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterAccess(5, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

要配置写后过期策略,我们使用expireAfterWrite方法:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));

要初始化自定义策略,我们需要实现Expiry接口:

cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
    @Override
    public long expireAfterCreate(
      String key, DataObject value, long currentTime) {
        return value.getData().length() * 1000;
    }
    @Override
    public long expireAfterUpdate(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
    @Override
    public long expireAfterRead(
      String key, DataObject value, long currentTime, long currentDuration) {
        return currentDuration;
    }
}).build(k -> DataObject.get("Data for " + k));

基于引用的逐出

我们可以将缓存配置为允许垃圾回收缓存的键或值。为此,我们将为键和值配置WeakRefence的用法,并且我们只能为值的垃圾收集配置为SoftReference。

当对象没有任何强引用时,WeakRefence用法允许对对象进行垃圾回收。SoftReference允许根据JVM的全局“最近最少使用”策略对对象进行垃圾收集。有关Java引用的更多详细信息,请参见此处。

我们应该使用Caffeine.weakKeys(),Caffeine.weakValues()和Caffeine.softValues()来启用每个选项:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));
 
cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .softValues()
  .build(k -> DataObject.get("Data for " + k));

刷新缓存

可以将缓存配置为在定义的时间段后自动刷新元素。让我们看看如何使用refreshAfterWrite方法执行此操作:

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

在这里,我们应该了解expireAfter和refreshAfter之间的区别。前者当请求过期元素时,执行将阻塞,直到build()计算出新值为止。

但是后者将返回旧值并异步计算出新值并插入缓存中,此时被刷新的元素的过期时间将重新开始计时计算。

统计

Caffeine可以记录有关缓存使用情况的统计信息:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .recordStats()
  .build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");
 
assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());

我们将recordStats传递给它,recordStats创建StatsCounter的实现。每次与统计相关的更改都将推送给此对象。

总结

在本文中,我们熟悉了Java的Caffeine缓存库。我们了解了如何配置和填充缓存,以及如何根据需要选择适当的过期或刷新策略。

本文分享自微信公众号 - TECH flower(tech-flower),作者:东溪陈姓少年

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 缓存之王Caffeine Cache,性能比Guava更强,命中率更高!

    在项目开发中,为提升系统性能,减少 IO 开销,本地缓存是必不可少的。最常见的本地缓存是 Guava 和 Caffeine,本篇文章将为大家介绍 Caffein...

    用户2781897
  • 来自未来的缓存-Caffeine,带你揭开它的神秘面纱

    我相信大家都了解缓存,了解redis,之前有发过几篇redis的文章忘记的小伙伴可以点击链接看看!

    狼王编程
  • Caffeine和Redis居然可以这么搭,想不到吧,爱了爱了

    在之前我们介绍了如何使用Redis或者Caffeine来做缓存,那么肯定会有人问,我用了redis已经很快了,为什么还要结合使用其他的缓存呢,缓存最大的作用确实...

    狼王编程
  • 用 Java 写个沙盒塔防游戏!已上架 Steam,Apple Store

    你好,我是 Guide!这里是 JavaGuide 的开源项目推荐第 2 期,每一期我都会精选 5 个高质量的 Java 开源项目推荐给大家。

    Guide哥
  • 本地缓存性能之王Caffeine

    随着互联网的高速发展,市面上也出现了越来越多的网站和app。我们判断一个软件是否好用,用户体验就是一个重要的衡量标准。比如说我们经常用的微信,打开一个页面要十几...

    java金融
  • 干掉 GuavaCache:Caffeine 才是本地缓存的王

    话说,中间件的选择上,Spring(SpringBoot)一直是业界的风向标。比如Spring一直使用「Jackson」,而没有使用Gson和fastjson。...

    程序猿DD
  • 本地缓存高性能之王Caffeine

    随着互联网的高速发展,市面上也出现了越来越多的网站和app。我们判断一个软件是否好用,用户体验就是一个重要的衡量标准。比如说我们经常用的微信,打开一个页面要十几...

    java金融
  • 如何把 Caffeine Cache 用得如丝般顺滑?

    在推荐服务中,虽然允许少量请求因计算超时等原因返回默认列表。但从运营指标来说,越高的“完算率”意味着越完整的算法效果呈现,也意味着越高的商业收益。(完算率类比视...

    2020labs小助手
  • 浅入浅出Caffeine cache

    本地缓存也就是我们适用内存缓存一些热点数据,使应用程序的程序处理的更加的快。以及保护我们的一些有磁盘/网络IO操作的函数/方法,以达到减小我们服务的响应时间的目...

    居士
  • 玩转Spring Cache --- 整合进程缓存之王Caffeine Cache和Ehcache3.x【享学Spring】

    前面文章大篇幅详细讲解了Spring Cache缓存抽象、三大缓存注解的工作原理等等。若是细心的小伙伴会发现:讲解时的Demo我使用的缓存实现方案均是Sprin...

    YourBatman
  • 如何优雅的使用缓存?

    在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介...

    用户5397975
  • 深入剖析来自未来的缓存-Caffeine

    读这篇文章之前希望你能好好的阅读: 你应该知道的缓存进化史 和 如何优雅的使用缓存? 。这两篇文章主要从一些实战上面去介绍如何去使用缓存。在这两篇文章中我都比较...

    用户5397975
  • 高性能本地缓存组件 Caffeine Cache

    Redis 作为分布式缓存组件提供多个服务间的缓存,但是 Redis 需要网络开销,增加耗时。本地缓存是直接从本地内存中读取数据,没有网络开销,性能更高,例如秒...

    微观技术
  • 2021-08-05SpringCloud升级之路2020.0.x版-5.所有项目的parent与spring-framework-common说明

    1. 使用 log4j2 异步日志所需要的依赖:需要排除默认的日志实现 logback,增加 log4j2 的依赖,并且添加 log4j2 异步日志需要的 di...

    干货满满张哈希
  • 缓存 | 从本地缓存到分布式缓存, Guava, Caffeine, Memcached, Redis

    在程序设计中,经常能听到的就是以时间换空间和以空间换时间。缓存作为一种能加快程序性能的银弹,它是典型的后者(以空间换时间).

    双鬼带单
  • 为什么SpringBoot2.x官方首推Caffeine作为缓存框架?

    Caffeine是一个高性能,高命中率,低内存占用,near optimal 的本地缓存,简单来说它是Guava Cache的优化加强版,有些文章把Caffei...

    业余草
  • 深入源码分析,缓存之王 Caffeine 为何这么猛?

    Caffeine[1]是一个高性能,高命中率,低内存占用,near optimal 的本地缓存,简单来说它是 Guava Cache 的优化加强版,有些文章把 ...

    程序员泥瓦匠
  • Redis 缓存性能实践及总结

    在互联网应用中,缓存成为高并发架构的关键组件。这篇博客主要介绍缓存使用的典型场景、实操案例分析、Redis使用规范及常规 Redis 监控。

    2020labs小助手
  • 有哪些Java源代码看了后让你收获很多?

    曾经读过Erudika/para的源码,此源码托管在github,大致是2014年前后开源的云计算通用后端框架,基于spring boot体系,...

    慕容千语

扫码关注云+社区

领取腾讯云代金券