zoukankan      html  css  js  c++  java
  • 解析高性能进程缓存-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<? super K, ? extends V> mappingFunction);    //获取缓存条目,不存在则执行mappingFunction进行计算,并存入缓存
    Map<K, V> getAllPresent(@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();    //获取缓存状态
    ConcurrentMap<K, V> asMap();    //转换为ConcurrentMap
    void cleanUp();    //触发清除缓存
    Policy<K, V> policy();    //设定策略

    LoadingCache

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

    V get(@Nonnull K key);    //获取条目,没有function参数,但是为空会调用CacheLoader的loadMap<K, V> 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。

     
    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是一款非常优秀的缓存框架,使用的设计理念和代码实现都让我受益良多,之后有机会我会继续进行深入的理解和学习,谢谢大家的浏览。

     
     
    链接:https://www.jianshu.com/p/6eb8b0a16c12

  • 相关阅读:
    错误处理和调试 C++快速入门30
    错误处理和调试 C++快速入门30
    虚继承 C++快速入门29
    多继承 C++快速入门28
    界面设计01 零基础入门学习Delphi42
    鱼C记事本 Delphi经典案例讲解
    界面设计01 零基础入门学习Delphi42
    虚继承 C++快速入门29
    linux系统中iptables防火墙管理工具
    linux系统中逻辑卷快照
  • 原文地址:https://www.cnblogs.com/xd502djj/p/13841222.html
Copyright © 2011-2022 走看看