1.简介
对于用户来说,响应的快慢是判断一个系统的重要指标,缓存就是必不可少的优化工具,在一个高并发的场景中往往占有着非常重要的角色,所以开发人员需要根据不同的应用场景来选择不同的缓存框架,比如分布式缓存redis,或者进程缓存GuavaCache。
进程缓存与Map之间的本质区别就是能自动的回收存储的元素,而GuavaCache是一款非常优秀的进程缓存框架,很好的提供了读写和自动失效的功能。而今天要介绍的进程缓存Caffeine,在设计上参考了GuavaCache的经验,也进行了大量的改进优化,以下数据图片均来源于Caffeine GitHub地址:caffeine,首先是读写性能的比较:
可以看到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方法都伴随这一个三目运算符,并且最后会实例化两个子类返回,buildAsync方法内部也是这样的实现。那么这些实现类是干什么用的呢,我们先要明白Caffeine内部接口的一个大致关系。
Cache
首先是Caffeine的Cache接口,这个接口是Caffeine最底层的一个接口,主要提供了一些方法定义:
V getIfPresent(@Nonnull Object key); //获取缓存条目,不存在则返回NULL
V get(@Nonnull K key, @Nonnull Function super K, ? extends V> mappingFunction); //获取缓存条目,不存在则执行mappingFunction进行计算,并存入缓存
MapgetAllPresent(@Nonnull Iterable> keys); //批量获取条目,返回一个Map
void put(@Nonnull K key, @Nonnull V value); //插入一个条目到缓存中
void putAll(@Nonnull Map extends K,? extends V> map); //批量缓存数据
void invalidate(@Nonnull Object key); //回收一个条目
void invalidateAll(@Nonnull Iterable> keys); 批量回收条目
void invalidateAll(); //回收全部条目
long estimatedSize(); //获取缓存大小
CacheStats stats(); //获取缓存状态
ConcurrentMapasMap(); //转换为ConcurrentMap
void cleanUp(); //触发清除缓存
Policypolicy(); //设定策略
LoadingCache
LoadingCache类继承自Cache,同时也定义了一些接口
V get(@Nonnull K key); //获取条目,没有function参数,但是为空会调用CacheLoader的loadMap
getAll(@Nonnull Iterable extends K> 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。
该实现类最低限度的提供了缓存的功能,初始化时提供了一个默认大小为16的ConcurrentHashMap用来存储数据,也提供了基本的状态计数器,删除监听器,编写器等。由于没有任何主动的回收策略,UnboundedLocalManualCache的本质就是对Map的操作。
BoundedLocalManualCache
BoundedLocalManualCache是有回收策略的,所有Caffeine对于设置的每种回收策略都有一个对应的实现类,所以就有了LocalCacheFactory类来构建响应的实现类。
newBoundedLocalCache针对我们配置的每种情况都拼接了一个字符,最终得到一个对应的实现类名,这样穷举性的写法也是因为Caffeine对每种情况都作出了优化。
newBoundedLocalCache方法最后返回一个BoundedLocalCache,也是我们最终用到的实现类。
6.缓存过期策略解析
我们知道了Caffeine有三种过期策略,接下来我们来大致分析下Caffeine是怎么主动的进行缓存回收的。从源码中我们找到了这样两个方法:
这是在读写时分别调用的两个方法,进行一些读写的后续操作,其中都调用了一个scheduleDrainBuffers方法,这个方法就是用来进行过期任务调度的。
首先尝试加锁,如果锁失败表明其他线程正在进行操作。锁成功后会执行drainBuffersTask,也就是Caffeine的PerformCleanupTask异步回收。
PerformCleanupTask的performCleanUp方法会再次加锁
进到maintenance方法中,在这里我们看到很多的方法,都是用来进行回收的。
drainReadBuffer:读缓存用尽
drainWriteBuffer:写缓存用尽
drainKeyReferences:key引用队列耗尽
drainValueReferences:value引用耗尽
expireEntries:达到过期时间
evictEntries:达到大小限制
获取到当前时间后对expireAfterAccess进行淘汰。
之后淘汰expireAfterWrite。
对于自定义时间通过时间轮来进行淘汰。
最后
本篇文章大致介绍了Caffeine的使用方法,填充策略,回收策略以及粗略的进行了源码的解析,Caffeine是一款非常优秀的缓存框架,使用的设计理念和代码实现都让我受益良多,之后有机会我会继续进行深入的理解和学习,谢谢大家的浏览。