Caffeine缓存

1. 前言

缓存(Cache)在代码世界中无处不在。从底层的CPU多级缓存,到客户端的页面缓存,处处都存在着缓存的身影。缓存从本质上来说,是一种空间换时间的手段,通过对数据进行一定的空间安排,使得下次进行数据访问时起到加速的效果。

就Java而言,其常用的缓存解决方案有很多,例如数据库缓存框架EhCache,分布式缓存Memcached等,这些缓存方案实际上都是为了提升吞吐效率,避免持久层压力过大。

对于常见缓存类型而言,可以分为本地缓存以及分布式缓存两种

  • 分布式缓存: redis、memcached等

  • 本地缓存: ehcache、GuavaCache、Caffeine等

1.1 官方性能比较

说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经它的性能是非常不错的。正所谓长江后浪推前浪,前浪被拍在沙滩上,目前Spring5(SpringBoot2)开始用Caffeine取代Guava,详见官方信息:SPR-13797

下面就来对比一下各个缓存框架的性能,以下测试都是基于jmh测试的:

  1. 8个线程读,100%的读操作

  2. 6个线程读,2个线程写

  1. 8个线程写,100%的写操作

可以从数据看出来Caffeine的性能都比Guava要好。然后Caffeine的API的操作功能和Guava是基本保持一致的,并且 Caffeine为了兼容之前是Guava的用户,做了一个Guava的Adapter给大家使用也是十分的贴心

2. 什么是Caffeine

Caffeine官网

Caffeine是基于Java1.8的高性能本地缓存库,由Guava改进而来,官方说明指出,其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的区别主要在于:

  • ConcurrentMap: 将存储所有存入的数据,直到显式将其移除
  • Caffeine: 将通过给定的配置,自动移除”不常用”的数据,以保持内存的合理占用。

2. Caffeine基础

使用Caffeine需要在工程中引入如下依赖

1
2
3
4
5
6
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<!--https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeinez 找最新版-->
<version>3.0.5</version>
</dependency>

2.1 缓存加载策略

Cache手动创建

最普通的一种缓存,无需指定加载方式,需要手动调用put()进行加载。需要注意的是put()方法对已经存在的key将进行覆盖,这点和Map的表现是一致的。

在获取缓存值的时候,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k->value)方法,该方法将避免写入竞争,调用invalidate()方法,将手动移除缓存

在多线程的情况下,当时用get(key, k->value)时,如果有另一个线程同时调用本方法进行竞争,则后一个线程会被阻塞,直到前一个线程更新缓存完成;若另一个线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Cache<Object, Object> cache = Caffeine.newBuilder()
//初始数量
.initialCapacity(10)
//最大条数
.maximumSize(10)
//最后一次写操作经过指定时间过期,expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准
.expireAfterWrite(1, TimeUnit.SECONDS)
//最后一次读或写操作后经过指定时间过期
.expireAfterAccess(1, TimeUnit.SECONDS)
//监听缓存被移除
.removalListener((key, value, removalCause) -> {
})
//记录命中
.recordStats()
.build();
cache.put("1","张三");
System.out.println(cache.getIfPresent("1"));
//存储的是默认值
System.out.println(cache.get("2", o-> "默认值"));
}

Loading Cache自动创建

LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于,党缓存不存在/缓存已过期时,若调用get()方法,则自动调用CacheLoader.load()方法加载最新值。调用getAll()方法将遍历所有的key调用get(),除非实现了CacheLoader.loadAll()方法。
使用LoadingCache时,需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。

在使用多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新完成

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache
.refreshAfterWrite(10, TimeUnit.SECONDS)
.expireAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(10, TimeUnit.SECONDS)
.maximumSize(10)
.build(key -> new Date().toString());
}

Async Cache异步获取

AsyncCache是Cache的一个变体,其响应结果均为CompletableFuture,通过这种方式,AsyncCache对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.excutor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力,它将以Cache进行返回。

在多线程情况下,当两个线程同时调用get(key, k->value),则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞带灯或者非阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
final AsyncLoadingCache<Object, String> asyncLoadingCache = Caffeine.newBuilder()
//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存;仅支持LoadingCache
.refreshAfterWrite(1, TimeUnit.SECONDS)
.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(10)
//根据key查询数据库里面的值
.buildAsync(key -> {
Thread.sleep(1000);
return new Date().toString();
});

//异步缓存返回的是CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);
}

2.2 驱逐策略

驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐基于时间的驱逐

  • 基于容量的驱逐需要指定缓存容量的最大值,当缓存容量达到最大时,Caffeine将使用LRU策略对缓存进行淘汰;
  • 基于时间的驱逐如字面意思,可以设置在最后访问/写入一个缓存经过指定时间后,自动进行淘汰

驱逐策略可以组合使用,任意驱逐策略生效后,该缓存条目级被驱逐:

  • LRU 最近最少使用: 淘汰最长时间没有被使用的页面

  • LFU 最不经常使用:淘汰一段时间内使用次数最小的页面

  • FIFO先进显出

Caffeine有4种缓存淘汰设置:

  • 大小(LFU算法进行淘汰)

  • 权重(大小与权重,只能二选一)

  • 时间

  • 引用(不常用,本文不介绍)