【干货点】 该篇文章是前三篇文章Caffeine系列的总结,算是全网最全的实战教程了,每个知识点都有相关demo和应用场景,以及相关的坑,这些都一一声明了,看完该篇文章,你就能够入门Caffeine了,可以真正用进项目了。
前阵子看见这几个字,确实是颇有感触,有很多想法、很多要做的事情,不过我也明白,急不了的,那就慢慢来。
我这边专门维护了一个github仓库,后续存放Caffeine实战文章、教程分析、以及组件和应用,有兴趣关注下

github:https://github.com/wiatingpub/Caffeine
正文
如果是想直接看官网教程的请移步:https://github.com/ben-manes/caffeine/wiki
而如果还想结合实际应用场景,以及各种坑的,请看本文。
最近来了一个实习生小张,看了我在公司项目中使用的缓存框架Caffeine,三天两头跑来找我取经,说是要把Caffeine吃透,为此无奈的也只能一个个细心解答了。
后来这件事情被总监直到了,说是后面还有新人,让我将相关问题和细节汇总成一份教程,权当共享好了,该份教程也算是全网第一份,结合了目前我司游戏中业务场景的应用和思考,以及踩过的坑。
实习生小张:稀饭稀饭,以前我们游戏中应用的缓存其实是谷歌提供的ConcurrentLinkedHashMap,为什么后面你强烈要求换成用Caffeine呢?
关于上面的问题,具体有以下几个原因:
使用谷歌提供的ConcurrentLinkedHashMap有个漏洞,那就是缓存的过期只会发生在缓存达到上限的情况,否则便只会一直放在缓存中。咋一看,这个机制没问题,是没问题,可是却不合理,举个例子,有玩家上线后加载了一堆的数据放在缓存中,之后便不再上线了,那么这份缓存便会一直存在,知道缓存达到上限。
ConcurrentLinkedHashMap没有提供基于时间淘汰时间的机制,而Caffeine有,并且有多种淘汰机制,并且支持淘汰通知。
目前Spring也在推荐使用,Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
实习生小张:哦哦哦,我了解了,是否可以跟我介绍下Caffeine呢?
可以的,Caffeine是基于Java8的高性能缓存库,可提供接近最佳的命中率。Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。
再说一个劲爆的消息,很多人都听说过Google的GuavaCache,而没有听说过Caffeine,其实和Caffeine相比,GuavaCache简直就是个弟中弟,这不SpringFramework5.0(SpringBoot2.0)已经放弃了Google的GuavaCache,转而选择了Caffeine。

为什么敢这么夸Caffeine呢?我们可以用官方给出的数据说话。

Caffeine提供了多种灵活的构造方法,从而可以创建多种特性的本地缓存。
自动把数据加载到本地缓存中,并且可以配置异步;
基于数量剔除策略;
基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
异步刷新;
Key会被包装成Weak引用;
Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
数据剔除提醒;
写入广播机制;
缓存访问可以统计;
实习生小张:我擦,这么强大,为什么可以这么强大呢,稀饭你不是自称最熟悉Caffeine的人吗?能否给我大概所说内部结构呢?
我日,我没有,我只是说在我们项目组我最熟悉,别污蔑我

那接下来我大概介绍下Caffeine的内部结构

Cache的内部包含着一个ConcurrentHashMap,这也是存放我们所有缓存数据的地方,众所周知,ConcurrentHashMap是一个并发安全的容器,这点很重要,可以说Caffeine其实就是一个被强化过的ConcurrentHashMap。
Scheduler,定期清空数据的一个机制,可以不设置,如果不设置则不会主动的清空过期数据。
Executor,指定运行异步任务时要使用的线程池。可以不设置,如果不设置则会使用默认的线程池,也就是ForkJoinPool.commonPool()
实习生小张:听起来就是一个强化版的ConcurrentHashMap,那么需要导入什么包吗?
Caffeine的依赖,其实还是很简单的,直接引入maven依赖即可。
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
实习生小张:可以,导入成功了,你一直和我说Caffeine的数据填充机制设计的很优美,不就是put数据吗?有什么优美的?说说看吗?
是put数据,只是针对put数据,Caffeine提供了三种机制,分别是
手动加载
同步加载
异步加载
我分别举个例子,比如手动加载
/**
* @author xifanxiaxue
* @date 2020/11/17 0:16
* @desc 手动填充数据
*/
public class CaffeineManualTest {
@Test
public void test() {
// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
Cache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
int key1 = 1;
// 使用getIfPresent方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null:
System.out.println(cache.getIfPresent(key1));
// 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key
// 则该函数将用于提供默认值,该值在计算后插入缓存中:
System.out.println(cache.get(key1, new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return 2;
}
}));
// 校验key1对应的value是否插入缓存中
System.out.println(cache.getIfPresent(key1));
// 手动put数据填充缓存中
int value1 = 2;
cache.put(key1, value1);
// 使用getIfPresent方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null:
System.out.println(cache.getIfPresent(1));
// 移除数据,让数据失效
cache.invalidate(1);
System.out.println(cache.getIfPresent(1));
}
}
上面提到了两个get数据的方式,一个是getIfPercent,没数据会返回Null,而get数据的话则需要提供一个Function对象,当缓存中不存在查询的key则将该函数用于提供默认值,并且会插入缓存中。
实习生小张:如果同时有多个线程进行get,那么这个Function对象是否会被执行多次呢?
实际上不会的,可以从结构图看出,Caffeine内部最主要的数据结构就是一个ConcurrentHashMap,而get的过程最终执行的便是ConcurrentHashMap.compute,这里仅会被执行一次。
接下来说说同步加载数据
/**
* @author xifanxiaxue
* @date 2020/11/19 9:47
* @desc 同步加载数据
*/
public class CaffeineLoadingTest {
/**
* 模拟从数据库中读取key
*
* @param key
* @return
*/
private int getInDB(int key) {
return key + 1;
}
@Test
public void test() {
// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(10