Google Guava Cache 全解析

Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。
本文先介绍了Guava Cache囊括的基本使用方法,然后结合体系类图和LocalCache的数据结构对典型的几个方法源码进行流程分析。

为什么要用本地缓存

相对于IO操作
速度快,效率高
相对于Redis
Redis是一种优秀的分布式缓存实现,受限于网卡等原因,远水救不了近火。

DB + Redis + LocalCache = 高效存储,高效访问

什么时候用

  • 愿意消耗一些内存空间来提升速度
  • 预料到某些键会被多次查询
  • 缓存中存放的数据总量不会超出内存容量

怎么用

  1. 设置缓存容量
  2. 设置超时时间
  3. 提供移除监听器
  4. 提供缓存加载器
  5. 构建缓存

Demo1:

public class GuavaCacheDemo1 {
    public static void main(String[] args){
        CacheLoader<String, String> loader = new CacheLoader<String, String> () {
            public String load(String key) throws Exception {
                Thread.sleep(1000);
                if("key".equals(key)) return null;
                System.out.println(key + " is loaded from a cacheLoader!");
                return key + "'s value";
            }
        };

        RemovalListener<String, String> removalListener = new RemovalListener<String, String>() {
            public void onRemoval(RemovalNotification<String, String> removal) {
                System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");
            }
        };

        LoadingCache<String, String> testCache = CacheBuilder.newBuilder()
                .maximumSize(7)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(loader);

        for (int i = 0; i < 10; i ++){
            String key = "key" + i;
            String value = "value" + i;
            testCache.put(key,value);
            System.out.println("[" + key + ":" + value + "] is put into cache!");
        }

        System.out.println(testCache.getIfPresent("key6"));

        try{
            System.out.println(testCache.get("key"));
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }
}

运行效果:

加载

CacheLoader

如果有合理的默认方法来加载或计算与键关联的值。

LoadingCache是附带CacheLoader构建而成的缓存实现。创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法。

从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常。

Callable

如果没有合理的默认方法来加载或计算与键关联的值,或者想要覆盖默认的加载运算,同时保留“获取缓存-如果没有-则计算”[get-if-absent-compute]的原子语义。
所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable<V>)方法。这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。这个方法简便地实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。

Demo2:

public class GuavaCacheDemo2 {

    static Cache<String, String> testCache = CacheBuilder.newBuilder()
            .maximumSize(3)
            .build();

    public static void main(String[] args){
        testCache.put("1234","45");

        System.out.println(testCache.getIfPresent("key6"));

        try {

            System.out.println(testCache.get("123", new Callable<String>() {
                public String call() throws Exception {
                    return "134";
                }
            }));

            System.out.println(testCache.get("1234", new Callable<String>() {
                public String call() throws Exception {
                    return "134";
                }
            }));
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

运行效果:

Cache.put

但自动加载是首选的,因为它可以更容易地推断所有缓存内容的一致性。
使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 应该总是优先使用。

缓存回收

Guava Cache提供了三种基本的缓存回收方式:

1. 基于容量回收

maximumSize(long):当缓存中的元素数量超过指定值时。

2. 定时回收

expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。

3. 基于引用回收(Reference-based Eviction)

CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。

显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()

移除监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

统计

CacheBuilder.recordStats():用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheS tats 对象以提供如下统计信息:

hitRate():缓存命中率;
averageLoadPenalty():加载新值的平均时间,单位为纳秒;
evictionCount():缓存项被回收的总数,不包括显式清除。
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

Demo3:

public class GuavaCacheDemo3 {

    static Cache<String, Object> testCache = CacheBuilder.newBuilder()
            .weakValues()
            .recordStats()
            .build();

    public static void main(String[] args){
        Object obj1 = new Object();

        testCache.put("1234",obj1);

        obj1 = new String("123");

        System.gc();

        System.out.println(testCache.getIfPresent("1234"));

        System.out.println(testCache.stats());

    }
}

运行结果


LRU缓存回收算法

LRU(Least?recently?used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

1.?新数据插入到链表头部;
2.?每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.?当链表满的时候,将链表尾部的数据丢弃。

Guava Cache中借助读写队列来实现LRU算法。

Guava Cache体系类图

CacheBuilder

缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。
主要采用builder的模式,CacheBuilder的每一个方法都返回这个CacheBuilder知道build方法的调用。
注意build方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。

LocalManualCache

作为LocalCache的一个内部类,在构造方法里面会把LocalCache类型的变量传入,并且调用方法时都直接或者间接调用LocalCache里面的方法。

LocalLoadingCache

可以看到该类继承了LocalManualCache并实现接口LoadingCache。
覆盖了get,getUnchecked等方法。

LocalCache

Guava Cache中的核心类,重点了解。

LocalCache数据结构

根据上面的分析可知,LocalCache为Guava Cache的核心类,先看一个该类的数据结构: � LocalCache的数据结构与ConcurrentHashMap很相似,都由多个segment组成,且各segment相对独立,互不影响,所以能支持并行操作。每个segment由一个table和若干队列组成。缓存数据存储在table中,其类型为AtomicReferenceArray。

Segment<K, V>[] segments;

Segment继承于ReetrantLock,减小锁粒度,提高并发效率。

AtomicReferenceArray<ReferenceEntry<K, V>> table;

类似于HasmMap中的table一样,相当于entry的容器。

ReferenceEntry<K, V> referenceEntry;

基于引用的Entry,其实现类有弱引用Entry,强引用Entry等

ReferenceQueue<K> keyReferenceQueue;

已经被GC,需要内部清理的键引用队列。

ReferenceQueue<V> valueReferenceQueue;

已经被GC,需要内部清理的值引用队列。

Queue<ReferenceEntry<K, V>> recencyQueue;

记录升级可访问列表清单时的entries,当segment上达到临界值或发生写操作时该队列会被清空。

Queue<ReferenceEntry<K, V>> writeQueue;

按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。

Queue<ReferenceEntry<K, V>> accessQueue;

按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。

put

public V put(K key, V value); //onlyIfAbsent为false
public V putIfAbsent(K key, V value); //onlyIfAbsent为true
该方法显式往本地缓存里面插入值。从下面的流程图中可以看出,在执行每次put前都会进行preWriteCleanUP,在put返回前如果更新了entry则要进行evictEntries操作。

preWriteCleanup

void preWriteCleanup(long now);
传人参数只有当前时间。
键值引用队列中都是存储已经被GC,等待清除的entry信息,所以首先去处理这个里面的entry.
读写队列里面是按照读写时间排序的,取出队列中的首元素,如果当前时间与该元素的时间相差值大于设定值,则进行回收。

evictEntries

void evictEntries(ReferenceEntry<K, V> newest);
传入的参数为最新的Entry,可能是刚插入的,也可能是刚更新过的。
该方法只有在设置了在构建缓存的时候指定了maximumSize才会往下执行。首先清除recencyQueue,判断该元素自身的权重是否超过上限,如果超过则移除当前元素。然后判断总的权重是否大于上限,如果超过则去accessQueue里找到队首(即最不常访问的元素)进行移除,直到小于上限。

getIfPresent

public V getIfPresent(Object key);
该方法从本地缓存中找值,如果找不到返回null,找到就返回相应的值。

get

首先会在缓存中找,缓存中找不到再通过load加载。

remove

public V remove(@Nullable Object key);
调用LocalManualCache的invalidate(Object key)方法即可调用remove.

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

推荐阅读更多精彩内容