guava 堆缓存

缓存在应用中是必不可少的,经常用的如redis、memcache以及内存缓存等。Guava是Google出的一个工具包,它里面的cache即是对本地内存缓存的一种实现,支持多种缓存过期策略。 

Guava cache的缓存加载方式有两种:

CacheLoader

Callable callback

具体两种方式的介绍看官方文档:http://ifeve.com/google-guava-cachesexplained/

接下来看看常见的一些使用方法。 

后面的示例实践都是以CacheLoader方式加载缓存值。

1.简单使用:定时过期

LoadingCache caches = CacheBuilder.newBuilder()

                .maximumSize(100)

                .expireAfterWrite(10, TimeUnit.MINUTES)

                .build(new CacheLoader() {

                    @Override

                    public Object load(String key) throws Exception {

                        return generateValueByKey(key);

                    }

                });try {

    System.out.println(caches.get("key-zorro"));

} catch (ExecutionException e) {

    e.printStackTrace();

}


如代码所示新建了名为caches的一个缓存对象,maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上线时,则会进行缓存回收,回收最近没有使用或总体上很少使用的缓存项。需要注意的是在接近这个容量上限时就会发生,所以在定义这个值的时候需要视情况适量地增大一点。 

另外通过expireAfterWrite这个方法定义了缓存的过期时间,写入十分钟之后过期。 

在build方法里,传入了一个CacheLoader对象,重写了其中的load方法。当获取的缓存值不存在或已过期时,则会调用此load方法,进行缓存值的计算。 

这就是最简单也是我们平常最常用的一种使用方法。定义了缓存大小、过期时间及缓存值生成方法。

如果用其他的缓存方式,如redis,我们知道上面这种“如果有缓存则返回;否则运算、缓存、然后返回”的缓存模式是有很大弊端的。当高并发条件下同时进行get操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成数据库雪崩。这也就是我们常说的“缓存穿透”。 

而Guava cache则对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存穿透的危险。

2.进阶使用:定时刷新

如上的使用方法,虽然不会有缓存穿透的情况,但是每当某个缓存值过期时,老是会导致大量的请求线程被阻塞。而Guava则提供了另一种缓存策略,缓存值定时刷新:更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。 

这里就需要用到Guava cache的refreshAfterWrite方法。如下所示:

LoadingCache caches = CacheBuilder.newBuilder()

                .maximumSize(100)

                .refreshAfterWrite(10, TimeUnit.MINUTES)

                .build(new CacheLoader() {

                    @Override

                    public Object load(String key) throws Exception {

                        return generateValueByKey(key);

                    }

                });try {

    System.out.println(caches.get("key-zorro"));

} catch (ExecutionException e) {

    e.printStackTrace();

}

如代码所示,每隔十分钟缓存值则会被刷新。

此外需要注意一个点,这里的定时并不是真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,让该线程去进行load方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

3.进阶使用:异步刷新

如2中的使用方法,解决了同一个key的缓存过期时会让多个线程阻塞的问题,只会让用来执行刷新缓存操作的一个用户线程会被阻塞。由此可以想到另一个问题,当缓存的key很多时,高并发条件下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞,并且给数据库带来很大压力。这个问题的解决办法就是将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。 

详细做法如下:

ListeningExecutorService backgroundRefreshPools =

                MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));

        LoadingCache caches = CacheBuilder.newBuilder()

                .maximumSize(100)

                .refreshAfterWrite(10, TimeUnit.MINUTES)

                .build(new CacheLoader() {

                    @Override                    public Object load(String key) throws Exception {

                        return generateValueByKey(key);

                    }

                    @Override                    public ListenableFuture reload(String key,

                            Object oldValue) throws Exception {

                        return backgroundRefreshPools.submit(new Callable() {

                            @Override                            public Object call() throws Exception {

                                return generateValueByKey(key);

                            }

                        });

                    }

                });try {

    System.out.println(caches.get("key-zorro"));

} catch (ExecutionException e) {

    e.printStackTrace();

}

在上面的代码中,我们新建了一个线程池,用来执行缓存刷新任务。并且重写了CacheLoader的reload方法,在该方法中建立缓存刷新的任务并提交到线程池。 

注意此时缓存的刷新依然需要靠用户线程来驱动,只不过和2不同之处在于该用户线程触发刷新操作之后,会立马返回旧的缓存值。

TIPS

可以看到防缓存穿透和防用户线程阻塞都是依靠返回旧值来完成的。所以如果没有旧值,同样会全部阻塞,因此应视情况尽量在系统启动时将缓存内容加载到内存中。

在刷新缓存时,如果generateValueByKey方法出现异常或者返回了null,此时旧值不会更新。

题外话:在使用内存缓存时,切记拿到缓存值之后不要在业务代码中对缓存直接做修改,因为此时拿到的对象引用是指向缓存真正的内容的。如果需要直接在该对象上进行修改,则在获取到缓存值后拷贝一份副本,然后传递该副本,进行修改操作。

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

推荐阅读更多精彩内容

  • com.google.common.cache 1、背景 缓存,在我们日常开发中是必不可少的一种解决性能问题的方法...
    拾壹北阅读 22,211评论 0 25
  • 未经历牛熊十载余年,不足以言股海人生,经得起浮层大浪,才能受的住顶礼荣耀。十年磨一剑,一朝试锋芒,股长如战场,知己...
    老樊解股阅读 248评论 0 1
  • 你还记得那个老鹰风筝吗?风筝不在了,但那件事我记得,终生不忘,也忘不掉。 那次,由于我闹脾气不吃饭,你一句...
    透过滤光片看蓝天阅读 244评论 0 1
  • 旗袍协会邀请函 协会简介 宁德市蕉城区旗袍协会主管单位为蕉城区文体局,是经蕉城区民政局登记...
    学院词人阅读 1,035评论 1 2
  • 真正的高贵不是优于别人,而是优于过去的自己。 还有接受现在的自己。
    随心所欲_e050阅读 165评论 0 0