本地缓存Caffeine

释放双眼,带上耳机,听听看~!

Caffeine

说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经,它的性能是非常不错的。但所谓长江后浪推前浪,总会有更加优秀的技术出现。今天,我就来介绍一个比Guava Cache性能更高的缓存框架:Caffeine。

Tips: Spring5(SpringBoot2)开始用Caffeine取代guava.详见官方信息SPR-13797
https://jira.spring.io/browse/SPR-13797

什么时候用

  1. 愿意消耗一些内存空间来提升速度
  2. 预料到某些键会被多次查询
  3. 缓存中存放的数据总量不会超出内存容量

性能

由图可以看出,Caffeine不论读还是写的效率都远高于其他缓存。

这里只列出部分性能比较,详细请看官方官方 https://github.com/ben-manes/caffeine/wiki/Benchmarks

依赖

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

版本问题参考https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine


1
2
3
4
5
6
7
1<dependency>
2    <groupId>com.github.ben-manes.caffeine</groupId>
3    <artifactId>caffeine</artifactId>
4    <version>2.7.0</version>
5</dependency>
6
7

新建对象


1
2
3
4
5
6
7
8
9
10
11
1// 1、最简单
2Cache<String, Object> cache = Caffeine.newBuilder()
3    .build();
4// 2、真实使用过程中我们需要自己配置参数。这里只列举部分,具体请看下面列表
5Cache<String, Object> cache = Caffeine.newBuilder()
6    .initialCapacity(2)//初始大小
7    .maximumSize(2)//最大数量
8    .expireAfterWrite(3, TimeUnit.SECONDS)//过期时间
9    .build();
10
11

参数含义

  • initialCapacity: 初始的缓存空间大小
  • maximumSize: 缓存的最大数量
  • maximumWeight: 缓存的最大权重
  • expireAfterAccess: 最后一次读或写操作后经过指定时间过期
  • expireAfterWrite: 最后一次写操作后经过指定时间过期
  • refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存
  • weakKeys: 打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开发统计功能

添加数据

Caffeine 为我们提供了三种填充策略:

手动、同步和异步

手动添加

很简单的


1
2
3
4
5
6
7
8
1public static void main(String[] args) {
2    Cache<String, String> cache = Caffeine.newBuilder()
3            .build();
4    cache.put("hello", "world");
5    System.out.println(cache.getIfPresent("hello"));
6}
7
8

自动添加1(自定义添加函数)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1Cache<String, String> cache = Caffeine.newBuilder()
2    .build();
3
4// 1.如果缓存中能查到,则直接返回
5// 2.如果查不到,则从我们自定义的getValue方法获取数据,并加入到缓存中
6cache.get("hello", new Function<String, String>() {
7    @Override
8    public String apply(String k) {
9        return getValue(k);
10    }
11});
12System.out.println(cache.getIfPresent("hello"));
13}
14
15// 缓存中找不到,则会进入这个方法。一般是从数据库获取内容
16private static String getValue(String k) {
17    return k + ":value";
18
19

// 这种写法可以简化成下面Lambda表达式
cache.get(“hello”, new Function<String, String>() {
@Override
public String apply(String k) {
return getValue(k);
}
});
// 可以简写为
cache.get(“hello”, k -> getValue(k));

自动添加2(初始添加)

和上面方法一样,只不过这个是在新建对象的时候添加


1
2
3
4
5
6
7
8
9
10
11
12
1LoadingCache&lt;String, String&gt; loadingCache = Caffeine.newBuilder()
2    .build(new CacheLoader&lt;String, String&gt;() {
3        @Override
4        public String load(String k) {
5            return getValue(k);
6        }
7    });
8// 同样可简化为下面这样
9LoadingCache&lt;String, String&gt; loadingCache2 = Caffeine.newBuilder()
10    .build(k -&gt; getValue(k));
11
12

过期策略

Caffeine提供三类驱逐策略:

  1. 基于大小(size-based)
  2. 基于时间(time-based)
  3. 基于引用(reference-based)

1、大小


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2        .maximumSize(3)
3        .build();
4cache.put(&quot;key1&quot;, &quot;value1&quot;);
5cache.put(&quot;key2&quot;, &quot;value2&quot;);
6cache.put(&quot;key3&quot;, &quot;value3&quot;);
7cache.put(&quot;key4&quot;, &quot;value4&quot;);
8cache.put(&quot;key5&quot;, &quot;value5&quot;);
9cache.cleanUp();
10System.out.println(cache.getIfPresent(&quot;key1&quot;));
11System.out.println(cache.getIfPresent(&quot;key2&quot;));
12System.out.println(cache.getIfPresent(&quot;key3&quot;));
13System.out.println(cache.getIfPresent(&quot;key4&quot;));
14System.out.println(cache.getIfPresent(&quot;key5&quot;));
15
16

输出结果


1
2
3
4
5
6
7
1null
2value2
3null
4value4
5value5
6
7

1、淘汰2个

2、淘汰并不是按照先后顺序,内部有自己的算法

2、时间

Caffeine提供了三种定时驱逐策略:

  • expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
  • expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
  • expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算。

缓存的删除策略使用的是惰性删除和定时删除。这两个删除策略的时间复杂度都是O(1)。

expireAfterWrite


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2    .expireAfterWrite(3, TimeUnit.SECONDS)
3    .build();
4cache.put(&quot;key1&quot;, &quot;value1&quot;);
5cache.put(&quot;key2&quot;, &quot;value2&quot;);
6cache.put(&quot;key3&quot;, &quot;value3&quot;);
7cache.put(&quot;key4&quot;, &quot;value4&quot;);
8cache.put(&quot;key5&quot;, &quot;value5&quot;);
9System.out.println(cache.getIfPresent(&quot;key1&quot;));
10System.out.println(cache.getIfPresent(&quot;key2&quot;));
11Thread.sleep(3*1000);
12System.out.println(cache.getIfPresent(&quot;key3&quot;));
13System.out.println(cache.getIfPresent(&quot;key4&quot;));
14System.out.println(cache.getIfPresent(&quot;key5&quot;));
15
16

结果


1
2
3
4
5
6
7
1value1
2value2
3null
4null
5null
6
7

例子2


1
2
3
4
5
6
7
8
9
10
11
12
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2        .expireAfterWrite(3, TimeUnit.SECONDS)
3        .build();
4cache.put(&quot;key1&quot;, &quot;value1&quot;);
5Thread.sleep(1*1000);
6System.out.println(cache.getIfPresent(&quot;key1&quot;));
7Thread.sleep(1*1000);
8System.out.println(cache.getIfPresent(&quot;key1&quot;));
9Thread.sleep(1*1000);
10System.out.println(cache.getIfPresent(&quot;key1&quot;));
11
12

结果


1
2
3
4
5
1value1
2value1
3null
4
5

expireAfterAccess

Access就是读和写


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2        .expireAfterAccess(3, TimeUnit.SECONDS)
3        .build();
4cache.put(&quot;key1&quot;, &quot;value1&quot;);
5Thread.sleep(1*1000);
6System.out.println(cache.getIfPresent(&quot;key1&quot;));
7Thread.sleep(1*1000);
8System.out.println(cache.getIfPresent(&quot;key1&quot;));
9Thread.sleep(1*1000);
10System.out.println(cache.getIfPresent(&quot;key1&quot;));
11Thread.sleep(3*1000);
12System.out.println(cache.getIfPresent(&quot;key1&quot;));
13
14

结果


1
2
3
4
5
6
1value1
2value1
3value1
4null
5
6

读和写都没有的情况下,3秒后才过期

expireAfter 和 refreshAfter 之间的区别

  • expireAfter 条件触发后,新的值更新完成前,所有请求都会被阻塞,更新完成后其他请求才能访问这个值。这样能确保获取到的都是最新的值,但是有性能损失。
  • refreshAfter 条件触发后,新的值更新完成前也可以访问,不会被阻塞,只是获取的是旧的数据。更新结束后,获取的才是新的数据。有可能获取到脏数据。

3、引用

  • Caffeine.weakKeys() 使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。

  • Caffeine.weakValues() 使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。

  • Caffeine.softValues() 使用软引用存储value。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1Cache&lt;String, Object&gt; cache = Caffeine.newBuilder()
2    .weakValues()
3    .build();
4Object value1 = new Object();
5Object value2 = new Object();
6cache.put(&quot;key1&quot;, value1);
7cache.put(&quot;key2&quot;, value2);
8
9value2 = new Object(); // 原对象不再有强引用
10System.gc();
11System.out.println(cache.getIfPresent(&quot;key1&quot;));
12System.out.println(cache.getIfPresent(&quot;key2&quot;));
13
14

结果


1
2
3
4
1java.lang.Object@7a4f0f29
2null
3
4

解释:当给value2引用赋值一个新的对象之后,就不再有任何一个强引用指向原对象。System.gc()触发垃圾回收后,原对象就被清除了。

简单回顾下Java中的四种引用

Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用

强引用
从来不会
对象的一般状态
JVM停止运行时终止
软引用
在内存不足时
对象缓存
内存不足时终止
弱引用
在垃圾回收时
对象缓存
GC运行后终止
虚引用
Unknown
Unknown
Unknown

显式删除缓存

除了通过上面的缓存淘汰策略删除缓存,我们还可以手动的删除


1
2
3
4
5
6
7
8
9
10
11
1// 1、指定key删除
2cache.invalidate(&quot;key1&quot;);
3// 2、批量指定key删除
4List&lt;String&gt; list = new ArrayList&lt;&gt;();
5list.add(&quot;key1&quot;);
6list.add(&quot;key2&quot;);
7cache.invalidateAll(list);//批量清除list中全部key对应的记录
8// 3、删除全部
9cache.invalidateAll();
10
11

淘汰、移除监听器

可以为缓存对象添加一个移除监听器,这样当有记录被删除时可以感知到这个事件。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2        .expireAfterAccess(3, TimeUnit.SECONDS)
3        .removalListener(new RemovalListener&lt;Object, Object&gt;() {
4            @Override
5            public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
6                System.out.println(&quot;key:&quot; + key + &quot;,value:&quot; + value + &quot;,删除原因:&quot; + cause);
7            }
8        })
9        .expireAfterWrite(1, TimeUnit.SECONDS)
10        .build();
11cache.put(&quot;key1&quot;, &quot;value1&quot;);
12cache.put(&quot;key2&quot;, &quot;value2&quot;);
13cache.invalidate(&quot;key1&quot;);
14Thread.sleep(2 * 1000);
15cache.cleanUp();
16
17

结果


1
2
3
4
1key:key1,value:value1,删除原因:EXPLICIT
2key:key2,value:value2,删除原因:EXPIRED
3
4

统计


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1Cache&lt;String, String&gt; cache = Caffeine.newBuilder()
2    .maximumSize(3)
3    .recordStats()
4    .build();
5cache.put(&quot;key1&quot;, &quot;value1&quot;);
6cache.put(&quot;key2&quot;, &quot;value2&quot;);
7cache.put(&quot;key3&quot;, &quot;value3&quot;);
8cache.put(&quot;key4&quot;, &quot;value4&quot;);
9
10cache.getIfPresent(&quot;key1&quot;);
11cache.getIfPresent(&quot;key2&quot;);
12cache.getIfPresent(&quot;key3&quot;);
13cache.getIfPresent(&quot;key4&quot;);
14cache.getIfPresent(&quot;key5&quot;);
15cache.getIfPresent(&quot;key6&quot;);
16System.out.println(cache.stats());
17
18

结果


1
2
3
1CacheStats{hitCount=4, missCount=2, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
2
3

除了结果输出的内容,CacheStats还可以获取如下数据。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

Bootstrap 间隔 (Spacing)

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索