Guava Cache实现原理浅析

一.概述

在上篇文章《Guava Cache做本地缓存那些事》中我介绍了Guava Cache作为本地缓存的一般用法,叙述了构造LoadingCache的方式和其基本使用,以及两种get方法的区别,LoadingCache和Cache的区别等内容。而为了知其然并知其所以然,本文对Guava Cache的实现原理做一些简单介绍,如有不对的地方请大家批评指正。

首先,先把Guava Cache在github上的源码链接放出来。里面有作者写的注释,可以方便大家学习。
Guava Cache带注释源码

二.关注的问题

从上述源码和前一篇介绍Guava Cache用法的文章中大家可以看出Guava Cache涉及的类和代码还是比较多的,如果挨个文件去分析涉及的细节太多,不容易把握关键的问题。所以笔者决定从get方法的执行过程出发对Guava Cache的原理进行分析。分析主要关注以下三个问题:

  1. Guava Cache是如何通过key映射到value的;
  2. 未命中缓存时如何调用的CacheLoader.load方法来载入缓存的;
  3. 两种缓存失效策略是如何实现的。

三.源码分析

3.1 key-value的映射过程

通过以下例子我们来看一下key-value的映射过程:

    public static void demo1() throws Exception {
        //定义Cache,并定义未命中时的载入方法
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().build(new CacheLoader<Integer, Integer>() {
            @Override
            public Integer load(Integer key) throws Exception {
                return key + 1;
            }
        });
        cache.get(1); //第一个get:第一次访问key,使用载入方法载入
        cache.get(1); //第二个get:第二次访问,在缓存中得到value
    }

以上demo定义了一个LoadingCache,并传入了CacheLoader提供在未命中缓存时的处理方法。后面可以看到对get方法调用了两次,第一次是为了通过load方法将相应的value载入缓存,这个过程在3.2节中介绍。本节主要看一下第二次get的过程:在缓存中存在对应value的情况下,如何通过key拿到value的。

通过debug断点,我们进入get方法,观察其执行过程:

step1:

首先进入的是如下这个方法,可以推断它所在的LoaclLoadingCache是LoadingCache接口的默认实现。这个类是LoaclCache的一个内部类。get方法直接调用了getOrLoad方法。

static class LocalLoadingCache<K, V> extends LocalCache.LocalManualCache<K, V> implements LoadingCache<K, V> {
        public V get(K key) throws ExecutionException {
            return this.localCache.getOrLoad(key);
        }
}

step2:
我们继续往下看getOrLoad方法,发现它也只是一层封装,go on!

    V getOrLoad(K key) throws ExecutionException {
        return this.get(key, this.defaultLoader);
    }

step3:
继续往下追踪到LocalCache.get方法。到这里终于可以看出一些操作了。一共有两条语句,分别分析下。

第一句是求哈希值,其中Preconditions.checkNotNull方法用于检查空指针,如果key是null的话就抛出空指针异常,然后进行hash操作。hash操作时,先取得key.hashCode()的返回值,再进行rehash操作。rehash的目的是为了防御由key.hashCode()质量差引起的hash冲突严重问题。个人理解的其具体做法是进行一系列位运算,将高位和低位的hash值进行混淆。

第二句是我们分析的重点,请看step4。

    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = this.hash(Preconditions.checkNotNull(key));
        return this.segmentFor(hash).get(key, hash, loader);
    }

step4:
LoadingCache采用了类似ConcurrentHashMap的方式,将映射表分为多个segment。segment之间可以并发访问,这样可以大大提高并发的效率,使得并发冲突的可能性降低了(同时访问一个segment上的key时还是有可能冲突的)。

可以将step3中最后一行代码分为两部分,一是this.segmentFor(hash),这是通过hash值确定该key位于哪一个segment上,并获取该segment。而是再通过segment执行get方法来获取具体的value值。

首先看一下this.segmentFor(hash)的代码:

LocalCache.Segment<K, V> segmentFor(int hash) {
    return this.segments[hash >>> this.segmentShift & this.segmentMask];
}

可以看出segments是存储在一个数组中的,该数组的长度是2的次幂,同时等于LoadingCache的并发等级。那么segmentShift和segmentMask是如何取值的呢?可以将segmentMash理解为数组长度减一,这样和它进行与操作后的范围总是在数组的长度范围内。segmentShift的取值有兴趣的可以参考源码LocalCache的构造方法了解,这里不再赘述。

取得segment后,便可以执行我们最终的get方法了。其核心主要代码如下:

       //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
           //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

代码ReferenceEntry<K, V> e = this.getEntry(key, hash);就获取到了key相关的键值对,后续代码进行了过期策略(3.3节介绍)和载入操作(3.2介绍)等操作。这里主要看一下getEntry方法。

和HashMap等类似,segment也采用了数组加链表的数据结构。首先由hash值和数组长度减一进行与操作获取链表的头结点,再遍历链表得到真正的结果。

小结
以上就是对LocalCache的get方法的分析,可以看出其实现方法除了一些细节上的不同基本和ConcurrentHashMap等Java标准类库中的实现类似。从分析的step4我们也知道了进行get操作时会进行CacheLoader.load的调用和缓存失效的判断,下面就具体再来看一下这两块内容的原理。

3.2 CacheLoader.load的调用机制

先回顾一下上篇文章知识,CacheLoader.load是在构建LoadingCache时传入给CacheBuilder的,并在缓存未命中的情况下进行调用。在3.1节step4中我们看到了Load被调用的入口点,为了阅读方便,这里再把该块代码贴一下:

       //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
           //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

可以看到当this.count==0时就会执行倒数第二行的this.lockedGetOrLoad方法,在这个方法里就对 CacheLoader.load进行了调用。由于这里是写操作,需要对segment进行加锁。segment是ReentrantLock的子类,直接调用其lock()方法即可加锁。之后对CacheLoader.load()进行调用获取value值,并放入table中的链表里。具体的代码较为繁琐就不再分析了。

3.3 两种缓存过期策略的实现

Guava Cache可以设置两种过期策略,一是单位时间内没有读缓存过期,另一种是单位时间内没有写缓存过期。

判断是否过期的入口在3.1节step4中提到的segment.get中:

   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
           //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               ……
           }
           ……
       }

一路追踪getLiveValue方法,找到了以下判断的位置:

    boolean isExpired(ReferenceEntry<K, V> entry, long now) {
        Preconditions.checkNotNull(entry);
        if (this.expiresAfterAccess() && now - entry.getAccessTime() >= this.expireAfterAccessNanos) {
            return true;
        } else {
            return this.expiresAfterWrite() && now - entry.getWriteTime() >= this.expireAfterWriteNanos;
        }
    }

可以看到根据过期策略、当前的时间、设置的过期时间进行了是否过期判断。同时我们也知道了Guava Cache判断过期并不是单独再开一个线程,而是在读写操作时顺带着进行了过期判断。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • Guava Cache以下的特性: automatic loading of entries into the c...
    小锄禾阅读 8,612评论 2 11
  • 王二北原创,转载请标明出处:来自[王二北] 1、简单介绍guava cache guava cache是Googl...
    王二北阅读 2,836评论 0 2
  • 在上篇文章01 初识缓存-了解缓存中简单了介绍了下缓存的历程以及几种常见的技术进行简单介绍,本着学习的目的本节针对...
    花神子阅读 736评论 0 4
  • 范例 适用性 缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值...
    爱情小傻蛋阅读 634评论 0 2
  • “千岁殿下,慢点慢点,那里您可不能进呀!。” 崇明殿上一堆上朝的大臣就这么看着,一个粉雕玉砌的小孩子跌跌撞撞...
    顿大鱼鱼阅读 411评论 0 0