• Google guava cache源码解析1--构建缓存器(1)


    此文已由作者赵计刚授权网易云社区发布。

    欢迎访问网易云社区,了解更多网易技术产品运营经验。


    1、guava cache

    • 当下最常用最简单的本地缓存

    • 线程安全的本地缓存

    • 类似于ConcurrentHashMap(或者说成就是一个ConcurrentHashMap,只是在其上多添加了一些功能)

    2、使用实例

    具体在实际中使用的例子,去查看《第七章 企业项目开发--本地缓存guava cache》,下面只列出测试实例:

    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    
    public class Hello{
        
        LoadingCache<String, String> testCache = CacheBuilder.newBuilder()
                .expireAfterWrite(20, TimeUnit.MINUTES)// 缓存20分钟
                .maximumSize(1000)// 最多缓存1000个对象
                .build(new CacheLoader<String, String>() {
                    public String load(String key) throws Exception {
                        if(key.equals("hi")){
                            return null;
                        }
                        return key+"-world";
                    }
                });
        
        public static void main(String[] args){
            Hello hello = new Hello();
            System.out.println(hello.testCache.getIfPresent("hello"));//null
            hello.testCache.put("123", "nana");//存放缓存
            System.out.println(hello.testCache.getIfPresent("123"));//nana
            try {
                System.out.println(hello.testCache.get("hello"));//hello-world
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.println(hello.testCache.getIfPresent("hello"));//hello-world
            /***********测试null*************/
            System.out.println(hello.testCache.getIfPresent("hi"));//null
            try {
                System.out.println(hello.testCache.get("hi"));//抛异常
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            
        }
    }

    在这个方法中,基本已经覆盖了guava cache常用的部分。

    • 构造缓存器

      • 缓存器的构建没有使用构造器而不是使用了构建器模式,这是在存在多个可选参数的时候,最合适的一种配置参数的方式,具体参看《effective Java(第二版)》第二条建议。

    • 常用的三个方法

      • get(Object key)

      • getIfPresent(Object key)

      • put(Object key, Object value)

    3、源代码

    在阅读源代码之前,强烈建议,先看一下"Java并发包类源码解析"中的《第二章 ConcurrentHashMap源码解析》,链接如下:

    http://www.cnblogs.com/java-zhao/p/5113317.html

    对于源码部分,由于整个代码的核心类LocalCache有5000多行,所以只介绍上边给出的实例部分的相关源码解析。本节只说一下缓存器的构建,即如下代码部分:

        LoadingCache<String, String> testCache = CacheBuilder.newBuilder()
                .expireAfterWrite(20, TimeUnit.MINUTES)// 缓存20分钟(时间起点:entry的创建或替换(即修改))
                //.expireAfterAccess(10, TimeUnit.MINUTES)//缓存10分钟(时间起点:entry的创建或替换(即修改)或最后一次访问)
                .maximumSize(1000)// 最多缓存1000个对象
                .build(new CacheLoader<String, String>() {
                    public String load(String key) throws Exception {
                        if(key.equals("hi")){
                            return null;
                        }
                        return key+"-world";
                    }
                });

    说明:该代码的load()方法会在之后将get(Object key)的时候再说,这里先不说了。

    对于这一块儿,由于guava cache这一块儿的代码虽然不难,但是容易看的跑偏,一会儿就不知道跑到哪里去了,所以我下边先给出guava cache的数据结构以及上述代码的执行流程,然后大家带着这个数据结构和执行流程去分析下边的源代码,分析完源代码之后,我在最后还会再将cache的数据结构和构建缓存器的执行流程给出,并会结合我们给出的开头实例代码来套一下整个流程,最后画出初始化构建出来的缓存器(其实,这个缓存器就是上边以及文末给出的cache的数据结构图)。

     

    guava cache的数据结构图:



    需要说明的是:

    • 每一个Segment中的有效队列(废弃队列不算)的个数最多可能不止一个

    • 上图与ConcurrentHashMap及其类似,其中的ReferenceEntry[i]用于存放key-value

    • 每一个ReferenceEntry[i]都会存放一个链表,当然采用的也是Entry替换的方式。

    • 队列用于实现LRU缓存回收算法

    • 多个Segment之间互不打扰,可以并发执行

    • 各个Segment的扩容只需要扩自己的就好,与其他Segment无关

    • 根据需要设置好初始化容量与并发水平参数,可以有效避免扩容带来的昂贵代价,但是设置的太大了,又会耗费很多内存,要衡量好

    后边三条与ConcurrentHashMap一样

     

    guava cache的数据结构的构建流程:

    1)构建CacheBuilder实例cacheBuilder

    2)cacheBuilder实例指定缓存器LocalCache的初始化参数

    3)cacheBuilder实例使用build()方法创建LocalCache实例(简单说成这样,实际上复杂一些)

    3.1)首先为各个类变量赋值(通过第二步中cacheBuilder指定的初始化参数以及原本就定义好的一堆常量)

    3.2)之后创建Segment数组

    3.3)最后初始化每一个Segment[i]

    3.3.1)为Segment属性赋值

    3.3.2)初始化Segment中的table,即一个ReferenceEntry数组(每一个key-value就是一个ReferenceEntry)

    3.3.3)根据之前类变量的赋值情况,创建相应队列,用于LRU缓存回收算法

     

    类结构:(这个不看也罢)

     

    • CacheBuilder:设置LocalCache的相关参数,并创建LocalCache实例

    • CacheLoader:有用的部分就是一个load(),用于实现"取缓存-->若不存在,先计算,在缓存-->取缓存"的原子操作

    • LocalCache:整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法

    • LocalLoadingCache:LocalCache的一个静态内部类,这里的get(K key)是外部调用get(K key)入口

    • LoadingCache接口:继承于Cache接口,定义了get(K key)

    • Cache接口:定义了getIfPresent(Object key)和put(K key, V value)

    • LocalManualCache:LocalCache的一个静态内部类,是LocalLoadingCache的父类,这里的getIfPresent(Object key)和put(K key, V value)也是外部方法的入口

    关于上边的这些说明,结合之后的源码进行看就好了。

    注:如果在源码中有一些注释与最后的套例子的注释不同的话,以后者为准

    3.1、构建CacheBuilder+为LocalCache设置相关参数+创建LocalCache实例

    CacheBuilder的一些属性:

        private static final int DEFAULT_INITIAL_CAPACITY = 16;//用于计算每个Segment中的hashtable的大小
        private static final int DEFAULT_CONCURRENCY_LEVEL = 4;//用于计算有几个Segment
        private static final int DEFAULT_EXPIRATION_NANOS = 0;//默认的缓存过期时间
        
        static final int UNSET_INT = -1;
        
        int initialCapacity = UNSET_INT;//用于计算每个Segment中的hashtable的大小
        int concurrencyLevel = UNSET_INT;//用于计算有几个Segment
        long maximumSize = UNSET_INT;//cache中最多能存放的缓存entry个数
        long maximumWeight = UNSET_INT;
        
        Strength keyStrength;//键的引用类型(strong、weak、soft)
        Strength valueStrength;//值的引用类型(strong、weak、soft)
    
        long expireAfterWriteNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改)
        long expireAfterAccessNanos = UNSET_INT;//缓存超时时间(起点:缓存被创建或被修改或被访问)

    CacheBuilder-->newCacheBuilder():创建一个CacheBuilder实例

        /**
         * 采用默认的设置(如下)创造一个新的CacheBuilder实例
         * 1、strong keys
         * 2、strong values
         * 3、no automatic eviction of any kind.
         */
        public static CacheBuilder<Object, Object> newBuilder() {
            return new CacheBuilder<Object, Object>();//new 一个实例
        }

    接下来,使用构建器模式指定一些属性值(这里的话,就是超时时间:expireAfterWriteNanos+cache中最多能放置的entry个数:maximumSize),这里的entry指的就是一个缓存(key-value对)

    CacheBuilder-->expireAfterWrite(long duration, TimeUnit unit)

        /**
         * 指明每一个entry(key-value)在缓存中的过期时间
         * 1、时间的参考起点:entry的创建或值的修改
         * 2、过期的entry也许会被计入缓存个数size(也就是说缓存个数不仅仅只有存活的entry)
         * 3、但是过期的entry永远不会被读写
         */
        public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
            /*
             * 检查之前是否已经设置过缓存超时时间
             */
            checkState(expireAfterWriteNanos == UNSET_INT,//正确条件:之前没有设置过缓存超时时间
                       "expireAfterWrite was already set to %s ns",//不符合正确条件的错误信息
                       expireAfterWriteNanos);
            /*
             * 检查设置的超时时间是否大于等于0,当然,通常情况下,我们不会设置缓存为0
             */
            checkArgument(duration >= 0, //正确条件
                          "duration cannot be negative: %s %s",//不符合正确条件的错误信息,下边的是错误信息中的错误参数
                          duration, 
                          unit);
            this.expireAfterWriteNanos = unit.toNanos(duration);//根据输入的时间值与时间单位,将时间值转换为纳秒
            return this;
        }

    注意:

    • 设置超时时间,注意时间的起点是entry的创建或替换(修改)

    • expireAfterAccess(long duration, TimeUnit unit)方法的时间起点:entry的创建或替换(修改)或被访问


    免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐

    更多网易技术、产品、运营经验分享请点击


    相关文章:
    【推荐】 iOS SSL Pinning 保护你的 API
    【推荐】 Node.js之网游服务器实践
    【推荐】 2018中国云原生用户大会:网易云深度解析微服务框架

  • 相关阅读:
    报告分享|2022年汽车行业研究最新动态
    合阔智云核心生产系统切换到服务网格 ASM 的落地实践
    ATC'22顶会论文RunD:高密高并发的轻量级 Serverless 安全容器运行时 | 龙蜥技术
    万节点规模云服务的 SRE 能力建设
    只需 6 步,你就可以搭建一个云原生操作系统原型
    智能搜索引擎 | 驱动电商业务增长实践
    Dubbo Mesh:从服务框架到统一服务控制平台
    性能透明提升 50%!SMC + ERDMA 云上超大规模高性能网络协议栈
    通过Jenkins构建CI/CD实现全链路灰度
    ARMS 助力羽如贸易打造全链路可观测最佳实践
  • 原文地址:https://www.cnblogs.com/zyfd/p/10138519.html
Copyright © 2020-2023  润新知