解析高性能进程缓存-caffeine

1.简介

    对于用户来说,响应的快慢是判断一个系统的重要指标,缓存就是必不可少的优化工具,在一个高并发的场景中往往占有着非常重要的角色,所以开发人员需要根据不同的应用场景来选择不同的缓存框架,比如分布式缓存redis,或者进程缓存GuavaCache。

    进程缓存与Map之间的本质区别就是能自动的回收存储的元素,而GuavaCache是一款非常优秀的进程缓存框架,很好的提供了读写和自动失效的功能。而今天要介绍的进程缓存Caffeine,在设计上参考了GuavaCache的经验,也进行了大量的改进优化,以下数据图片均来源于Caffeine GitHub地址:caffeine,首先是读写性能的比较:

8个线程同时从缓存中读取


8个线程同时从缓存中写入


6个线程读取,2个线程写入

可以看到caffeine在读写方面明显优与其他框架,在缓存命中率上Caffeine也不同于Guava,采用了更为优秀的Window TinyLfu算法,该算法是在LRU的基础上改进的版本。


2.填充策略

(1)手动填充

手动填充

    newBuilder方法只是Caffeine类的一个空的构造函数,类属性的实例化是在build方法中进行的,put方法就是手动填充缓存。newBuilder方法后面还能跟很多配置方法,比如

我们也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key,则该函数将用于提供默认值,该值在计算后插入缓存中。
Caffeine类是Caffeine的基础类,里面提供了很多配置方法和参数:

maximumSize:设置缓存最大条目数,超过条目则触发回收。 
maximumWeight:设置缓存最大权重,设置权重是通过weigher方法, 需要注意的是权重也是限制缓存大小的参数,并不会影响缓存淘汰策略,也不能和maximumSize方法一起使用。 
weakKeys:将key设置为弱引用,在GC时可以直接淘汰
weakValues:将value设置为弱引用,在GC时可以直接淘汰
softValues:将value设置为软引用,在内存溢出前可以直接淘汰
expireAfterWrite:写入后隔段时间过期
expireAfterAccess:访问后隔断时间过期
refreshAfterWrite:写入后隔断时间刷新
removalListener:缓存淘汰监听器,配置监听器后,每个条目淘汰时都会调用该监听器
writer:writer监听器其实提供了两个监听,一个是缓存写入或更新是的write,一个是缓存淘汰时的delete,每个条目淘汰时都会调用该监听器

手动填充表示任何数据都需要手动put到cache中,没有任何自动加载策略。put方法会覆盖相同key的条目

(2)同步填充   

同步填充

通过在build方法中传入一个CacheLoader的实现来进行同步填充,CacheLoader中的load方法制定了对key的计算,也可以重写loadAll来进行批量计算。

还有种方法是通过在build方法中传入一个参数为 key 的 Function来进行同步填充,这种方法类似于手动填充中的get方法。

(3)异步填充

异步填充

异步填充于同步填充大致相似,区别是传入一个执行器进行异步执行,并且返回一个CompletableFuture对象,可以通过CompletableFuture.get来获取数据并设置超时时间。


3.回收策略

    条目的自动淘汰回收是map于cache最大的区别,Caffeine同样包含了3中缓存回收机制,分别是基于大小,基于时间,基于引用类型。

(1)基于大小

基于大小

    设置了maximumSize属性大小为1,cache实例化是缓存size为0,执行了第一个put方法后缓存到达上限,第二个put执行后会回收第一个缓存。调用cleanUp方法是因为缓存回收是异步执行,cleanUp可以等待异步执行完成。

基于权重
执行结果

除了设置maximumSize外,设置maximumWeight也可以进行基于大小的缓存回收,weigher简单的设定了每个条目的权重为5,进行2次put后权重达到上限,所以第三次put执行时会进行回收。

(2)基于时间

基于时间

基于时间的方式主要是三种配置:
    expireAfterWrite:上次写入后开始计时
    expireAfterAccess:上次访问后开始计时,包括读和写
    expireAfter:自定义的时间计时器

(3)基于引用


基于引用

我们可以显式的定义key或value为弱引用,或者value单独定义为软引用,这样就会启用基于引用的回收策略了,主要用到Java的GC进行回收。
    软引用:在内存溢出前回收
    弱引用:在下次GC时回收
使用到的回收策略时LRU算法

RemovalCause

RemovalCause是一个enum,记录了缓存失效的原因,并且通过wasEvicted方法定义是否是自动淘汰。
EXPLICIT    //手动调用invalidate或remove等方法
REPLACED        //调用put等方法进行修改
COLLECTED    //设置了key或value的引用方式
EXPIRED    //设置了过期时间
SIZE    //设置了大小


4.刷新

cache除了会自动淘汰,也能进行自动刷新操作

自动刷新

refreshAfterWrite就是设置写入后多就会刷新,expireAfterWrite和refreshAfterWrite的区别是,当缓存过期后,配置了expireAfterWrite,则调用时会阻塞,等待缓存计算完成,返回新的值并进行缓存,refreshAfterWrite则是返回一个旧值,并异步计算新值并缓存。


5.源码解析

    说完了基本的功能,接下来我们简单的解析一下Caffeine内部的实现,因为Caffeine设计复杂,功能强大,所以本篇先进行粗力度的解析。如有错误欢迎指正。
    首先我们看看在构建cache的时候用来区分填充方式的build方法:

build

可以看到build方法都伴随这一个三目运算符,并且最后会实例化两个子类返回,buildAsync方法内部也是这样的实现。那么这些实现类是干什么用的呢,我们先要明白Caffeine内部接口的一个大致关系。

Cache

首先是Caffeine的Cache接口,这个接口是Caffeine最底层的一个接口,主要提供了一些方法定义:

V getIfPresent(@Nonnull Object key);                    //获取缓存条目,不存在则返回NULL
V get(@Nonnull K key, @Nonnull Function mappingFunction);    //获取缓存条目,不存在则执行mappingFunction进行计算,并存入缓存
Map getAllPresent(@Nonnull Iterable keys);    //批量获取条目,返回一个Map
void put(@Nonnull K key, @Nonnull V value);    //插入一个条目到缓存中
void putAll(@Nonnull Map map);    //批量缓存数据
void invalidate(@Nonnull Object key);    //回收一个条目
void invalidateAll(@Nonnull Iterable keys);    批量回收条目
void invalidateAll();    //回收全部条目
long estimatedSize();    //获取缓存大小
CacheStats stats();    //获取缓存状态
ConcurrentMap asMap();    //转换为ConcurrentMap
void cleanUp();    //触发清除缓存
Policy policy();    //设定策略

LoadingCache

LoadingCache类继承自Cache,同时也定义了一些接口

V get(@Nonnull K key);    //获取条目,没有function参数,但是为空会调用CacheLoader的loadMap getAll(@Nonnull Iterable keys);    //获取条目,为空会调用CacheLoader的loadAllvoid refresh(@Nonnull K key);    //会异步的通过CacheLoader的load更新缓存

可以看到Cache接口更像是Map,用来存放key-value,而LoadingCache定义了加载和更新的机制,通过build方法中传入的CacheLoader来操作条目。

LocalManualCache

LocalManualCache也继承自Cache,这个接口有两个主要的实现类,就是上文提到的BoundedLocalManualCache和UnboundedLocalManualCache。这些是实现类提供了Cache的具体实现,并且UnboundedLocalManualCache也最低限度的提供了LocalCache的功能。而却分使用这两个实现的方式就是看我们是否配置了回收策略。

UnboundedLocalManualCache

如果我们没有配置任何的回收策略,则会默认使用UnboundedLocalManualCache。

UnboundedLocalManualCache

    该实现类最低限度的提供了缓存的功能,初始化时提供了一个默认大小为16的ConcurrentHashMap用来存储数据,也提供了基本的状态计数器,删除监听器,编写器等。由于没有任何主动的回收策略,UnboundedLocalManualCache的本质就是对Map的操作。

BoundedLocalManualCache

BoundedLocalManualCache是有回收策略的,所有Caffeine对于设置的每种回收策略都有一个对应的实现类,所以就有了LocalCacheFactory类来构建响应的实现类。

LocalCacheFactory

newBoundedLocalCache针对我们配置的每种情况都拼接了一个字符,最终得到一个对应的实现类名,这样穷举性的写法也是因为Caffeine对每种情况都作出了优化。

LocalCacheFactory的实现类

newBoundedLocalCache方法最后返回一个BoundedLocalCache,也是我们最终用到的实现类。


6.缓存过期策略解析

我们知道了Caffeine有三种过期策略,接下来我们来大致分析下Caffeine是怎么主动的进行缓存回收的。从源码中我们找到了这样两个方法:

读后操作
写后操作

这是在读写时分别调用的两个方法,进行一些读写的后续操作,其中都调用了一个scheduleDrainBuffers方法,这个方法就是用来进行过期任务调度的。

scheduleDrainBuffers

首先尝试加锁,如果锁失败表明其他线程正在进行操作。锁成功后会执行drainBuffersTask,也就是Caffeine的PerformCleanupTask异步回收。

PerformCleanupTask

PerformCleanupTask的performCleanUp方法会再次加锁

maintenance

进到maintenance方法中,在这里我们看到很多的方法,都是用来进行回收的。
drainReadBuffer:读缓存用尽
drainWriteBuffer:写缓存用尽
drainKeyReferences:key引用队列耗尽
drainValueReferences:value引用耗尽
expireEntries:达到过期时间
evictEntries:达到大小限制

获取到当前时间后对expireAfterAccess进行淘汰。

之后淘汰expireAfterWrite。

对于自定义时间通过时间轮来进行淘汰。


最后

    本篇文章大致介绍了Caffeine的使用方法,填充策略,回收策略以及粗略的进行了源码的解析,Caffeine是一款非常优秀的缓存框架,使用的设计理念和代码实现都让我受益良多,之后有机会我会继续进行深入的理解和学习,谢谢大家的浏览。

你可能感兴趣的:(解析高性能进程缓存-caffeine)