http://www.baeldung.com/java-caching-caffeine

作者:baeldung

译者:oopsguy.com

1、介绍

在本文中,我将介绍 Caffeine — 一个高性能的 Java 缓存库

缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。

回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 —— 缓存库的一个重要特性。

Caffeine 因使用了 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率

2、依赖

我们需要在 pom.xml 中添加 caffeine 依赖:

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

你可以在 Maven Central 上找到最新版本的 caffeine。

3、填充缓存

让我们来了解一下 Caffeine 的三种缓存填充策略:手动、同步加载和异步加载。

首先,我们为要缓存中存储的值类型写一个类:

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

3.1、手动填充

在此策略中,我们手动将值放入缓存后再检索。

初始化缓存:

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 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中:

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

get 方法可以以原子方式执行计算。这意味着你只进行一次计算 —— 即使有多个线程同时请求该值。这就是为什么使用 get 要优于 getIfPresent

有时我们需要手动触发一些缓存的值失效

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

3.2、同步加载

这种加载缓存的方式使用了与用于初始化值的 Function 的手动策略类似的 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 方法的初始化函数检索值,这使得可以使用缓存作为访问值的主要门面(Facade)。

3.3、异步加载

此策略的作用与之前相同,但是以异步方式执行操作,并返回一个包含值的 CompletableFuture

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

我们可以以相同的方式使用 getgetAll 方法,同时考虑到他们返回的是 CompletableFuture

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,你可以在此文中获取更多内容。

4、值回收

Caffeine 有三个值回收策略:基于大小,基于时间和基于引用。

4.1、基于大小回收

这种回收方式假定当缓存大小超过配置的大小限制时会发生回收。 获取大小有两种方法:缓存中计数对象,或获取权重。

让我们看看如何计算缓存中的对象。当缓存初始化时,其大小等于零:

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 Function 来获取缓存的大小:

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

当 weight 超过 10 时,值将从缓存中删除:

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

4.2、基于时间回收

这种回收策略是基于条目的到期时间,有三种类型:

  • 访问后到期 — 从上次读或写发生后,条目即过期。
  • 写入后到期 — 从上次写入发生之后,条目即过期
  • 自定义策略 — 到期时间由 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));

4.3、基于引用回收

我们可以将缓存配置启用基于缓存键值的垃圾回收。为此,我们将 key 和 value 配置为 弱引用,并且可以仅配置软引用以进行垃圾回收。

当对象的没有任何强引用时,使用 WeakRefence 可以启用对象的垃圾收回收。SoftReference 允许对象根据 JVM 的全局最近最少使用(Least-Recently-Used)的策略进行垃圾回收。有关 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));

5、刷新

可以将缓存配置为在指定时间段后自动刷新条目。让我们看看如何使用 refreshAfterWrite 方法:

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

这里我们要明白 expireAfterrefreshAfter 之间的区别。当请求过期条目时,执行将发生阻塞,直到 build Function 计算出新值为止。

但是,如果条目可以刷新,则缓存将返回一个旧值,并异步重新加载该值

6、统计

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 supplier,创建一个 StatsCounter 的实现。每次与统计相关的更改将推送此对象。

7、结论

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

文中示例的源代码可以在 Github 上找到。

高性能 Java 缓存库 — Caffeine的更多相关文章

  1. Java高性能本地缓存框架Caffeine

    一.序言 Caffeine是一个进程内部缓存框架,使用了Java 8最新的[StampedLock]乐观锁技术,极大提高缓存并发吞吐量,一个高性能的 Java 缓存库,被称为最快缓存. 二.缓存简介 ...

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

    概览 本文我们将介绍Caffeine-一个Java高性能缓存库.缓存和Map之间的一个根本区别是缓存会将储存的元素逐出.逐出策略决定了在什么时间应该删除哪些对象,逐出策略直接影响缓存的命中率,这是缓存 ...

  3. Caffeine 缓存库

    介绍 Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库. 缓存和ConcurrentMap有点相似,但还是有所区别.最根本的区别是ConcurrentMap将会持有所有加 ...

  4. 最佳内存缓存框架Caffeine

    Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架. Cache(缓存),基于Google Guava,Caffeine提供一个内存缓存,大大改善了设计Guava's ca ...

  5. 【Jetlang】一个高性能的Java线程库

    actor  Jetlang 提供了一个高性能的Java线程库,该库是 JDK 1.5 中的 java.util.concurrent 包的补充,可用于基于并发消息机制的应用. .net的MS CCR ...

  6. 高性能Java科学与技术运算库Colt

    在学习<Machine Learning in Action>和<NLTK Natural Language Processing with Python>的过程中,我真切地感 ...

  7. 一文深入了解史上最强的Java堆内缓存框架Caffeine

    它提供了一个近乎最佳的命中率.从性能上秒杀其他一堆进程内缓存框架,Spring5更是为了它放弃了使用多年的GuavaCache 缓存,在我们的日常开发中用的非常多,是我们应对各种性能问题支持高并发的一 ...

  8. 如何打造高性能的 Go 缓存库

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/531 文中代码位置: https://github.com/devY ...

  9. 本地缓存解决方案-Caffeine Cache

    1.1 关于Caffeine Cache ​ Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式.基于容量的方式内部实现采用LRU算法,基于引 ...

随机推荐

  1. 【干货篇】步步为营,带你轻松掌握jQuery!

    写在前面:经过系统的学习了原生JS之后,会发现其具有以下三个特点: 1.是一种解释性脚本语言(代码不进行预编译). 2.主要用来向 HTML 页面添加交互行为. 3.可以直接嵌入 HTML 页面,但写 ...

  2. Python可视化----------matplotlib.pylot

    1 >>> import matplotlib.pyplot as plt 2 >>> plt.axis([0,5,0,20]) 3 [0, 5, 0, 20] 4 ...

  3. 利用ASCII码生成指定规则的字符串

    /** * 上送终端编号的后两位生成规则 总共可以生成 (36*36-1)1295个编号 * 01...09 0A...0Z * 10...19 1A...1Z * ............... * ...

  4. IIS部署新网站

    Windows Server使用IIS 6.0配置ASP动态Web网站 http://jingyan.baidu.com/article/c1a3101ee43ae9de656debb4.html h ...

  5. Excel开发之旅

    开发环境:Microsoft Visual Studio 2015,Office 2013 开发语言:C# 1.创建工程:文件à新建à项目,选择office 外接程序àExcel 2013和2016 ...

  6. TCP/IP(二)物理层详解

    前言 在前面说了一下,计算机网络的大概内容,没有去深刻的去了解它,这篇文章给大家分享一下物理层! 我们知道ISO模型是七层,TCP/IP模型是五层,而tcp/ip协议只将七层概括为4层,我们将学习其中 ...

  7. 《深入理解Java虚拟机》读书笔记-垃圾收集器与内存分配策略

    在堆里存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前需要知道哪些对象还存活,哪些对象已经死去.那怎么样去判断对象是否存活呢? 一.判断对象是否存活算法 1.引用计数法 实现思路:给 ...

  8. hdu1512 Monkey King(左偏树 + 并查集)

    Once in a forest, there lived N aggressive monkeys. At the beginning, they each does things in its o ...

  9. 架构师之路-在Dubbo中开发REST风格的远程调用

    架构师之路:从无到有搭建中小型互联网公司后台服务架构与运维架构 http://www.roncoo.com/course/view/ae1dbb70496349d3a8899b6c68f7d10b 概 ...

  10. web前端面试官挖的那些坑(js)

    题目1: function Foo() { getName = function () { alert (1); }; return this; } Foo.getName = function () ...