GuavaCache整理

持续整理,持续学习

参考:GuavaCache简介(一)Google Guava Cache 全解析 - 简书

一、GuavaCache概述

Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。

在多线程高并发场景中往往离不开cache,需要根据不同的应用场景选择不同的cache,比如:分布式缓存redis、memcached。还有本地(进程内)缓存,一般使用map的方式保存在本地内存中。

一般我们在业务中操作缓存,都会操作缓存和数据源两部分。如:put数据时,先插入DB,再删除原来的缓存;ge数据时,先查缓存,命中则返回,没有命中时,需要查询DB,再把查询结果放入缓存中 。如果访问量大,我们还得兼顾本地缓存的线程安全问题。必要的时候也要考虑缓存的回收策略。
Guava Cache是google guava中的一个内存缓存模块,将数据缓存到JVM内存中,,有以下特性:

  • 很好的封装了get、put操作,能够集成数据源 ;
  • 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素;
  • Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收;
  • 监控缓存加载/命中情况

Guava Cache的架构设计灵感ConcurrentHashMap,在简单场景中可以通过HashMap实现简单数据缓存,但如果要实现缓存随时间改变、存储的数据空间可控则缓存工具还是很有必要的 

二、为什么要用本地缓存

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

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

三、什么时候用本地缓存?

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

二、POM依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

三、Cache接口

/**
 * 该接口的实现被认为是线程安全的,即可在多线程中调用
 * 通过被定义单例使用
 */
public interface Cache<K, V> {
 
  /**
   * 通过key获取缓存中的value,若不存在直接返回null
   */
  V getIfPresent(Object key);
 
  /**
   * 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value
   * 整个过程为 "if cached, return; otherwise create, cache and return"
   * 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null
   */
  V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
 
  /**
   * 添加缓存,若key存在,就覆盖旧值
   */
  void put(K key, V value);
 
  /**
   * 删除该key关联的缓存
   */
  void invalidate(Object key);
 
  /**
   * 删除所有缓存
   */
  void invalidateAll();
 
  /**
   * 执行一些维护操作,包括清理缓存
   */
  void cleanUp();
}

四、Cache对象构建

CacheBuilder 是Guava 提供的一个快速构建缓存对象的工具类。缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,它的每个方法都返回CacheBuilder本身,直到build方法被调用。 该类中提供了很多的参数设置选项,你可以设置cache的默认大小,并发数,存活时间,过期策略等等。

案例一:

	final static Cache<Integer, String> cache = CacheBuilder.newBuilder()
			//设置cache的初始大小为10,要合理设置该值
			.initialCapacity(10)
			//设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作
			.concurrencyLevel(5)
			//设置cache中的数据在写入之后的存活时间为10秒
			.expireAfterWrite(10, TimeUnit.SECONDS)
			//构建cache实例
			.build();

使用无参的build()方法,它将返回Cache类型的构建对象 

缓存的并发级别

Guava提供了设置并发级别的api,使得缓存支持并发的写入和读取。同 ConcurrentHashMap 类似Guava cache的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器cpu核心数是一个比较不错的选择。

CacheBuilder.newBuilder()
		// 设置并发级别为cpu核心数
		.concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
		.build();

缓存的初始容量设置

我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

CacheBuilder.newBuilder()
		// 设置初始容量为100
		.initialCapacity(100)
		.build();

五、怎么用?

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

案例一:构建Cache

public class MainTest {

    final static Cache<Integer, String> cache = CacheBuilder.newBuilder()
            //设置cache的初始大小为10,要合理设置该值
            .initialCapacity(10)
            //设置并发数为5,即同一时间最多只能有5个线程往cache执行写入操作
            .concurrencyLevel(5)
            //设置cache中的数据在写入之后的存活时间为10秒
            .expireAfterWrite(10, TimeUnit.SECONDS)
            //构建cache实例
            .build();


    public static void main(String[] args) throws Exception {
        cache.put(1, "Hi");

        for(int i=0 ;i<100 ;i++) {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            System.out.println(sdf.format(new Date())
                    + "  key:1 ,value:"+cache.getIfPresent(1));
            Thread.sleep(1000);
        }
    }
}

运行结果:

15:09:13  key:1 ,value:Hi
15:09:14  key:1 ,value:Hi
15:09:15  key:1 ,value:Hi
15:09:16  key:1 ,value:Hi
15:09:17  key:1 ,value:Hi
15:09:18  key:1 ,value:Hi
15:09:19  key:1 ,value:Hi
15:09:20  key:1 ,value:Hi
15:09:21  key:1 ,value:Hi
15:09:22  key:1 ,value:Hi
15:09:23  key:1 ,value:null
15:09:24  key:1 ,value:null
15:09:25  key:1 ,value:null 

案例二:构建LoadingCache

LoadingCache是Cache的子接口,相比较于Cache,当从LoadingCache中读取一个指定key的记录时,如果该记录不存在,则LoadingCache可以自动执行加载数据到缓存的操作。

在调用CacheBuilder的build方法时,必须传递一个CacheLoader类型的参数,CacheLoader的load方法需要我们提供实现。

package com.example.anonotationnormal.guava;

import com.google.common.cache.*;

import java.util.concurrent.TimeUnit;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/11 4:39 下午
 * @description
 */

public class GuavaCacheDemo {
    public static void main(String[] args){

        //提供缓存加载器
        CacheLoader<String, String> loader = new CacheLoader<String, String> () {
            @Override
            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>() {
            @Override
            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 returned null for key key.

[key0:value0] is put into cache!
[key1:value1] is put into cache!
[key2:value2] is put into cache!
[key3:value3] is put into cache!
[key4:value4] is put into cache!
[key5:value5] is put into cache!
[key6:value6] is put into cache!
[key0:value0] is evicted!
[key7:value7] is put into cache!
[key1:value1] is evicted!
[key8:value8] is put into cache!
[key2:value2] is evicted!
[key9:value9] is put into cache!
value6
com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key key.
    at com.google.common.cache.LocalCache$Segment.getAndRecordStats(LocalCache.java:2350)
    at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2320)
    at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2282)
    at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2197)
    at com.google.common.cache.LocalCache.get(LocalCache.java:3937)
    at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941)
    at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824)
    at com.example.anonotationnormal.guava.GuavaCacheDemo1.main(GuavaCacheDemo1.java:51)

修改代码,避免异常

public String load(String key) throws Exception {
    Thread.sleep(1000);
    if("key".equals(key)) {
        return "value_key";     //原有为null,这里进行调整    
    }
    System.out.println(key + " is loaded from a cacheLoader!");
    return key + "'s value";
}

 运行结果:

[key0:value0] is put into cache!
[key1:value1] is put into cache!
[key2:value2] is put into cache!
[key3:value3] is put into cache!
[key4:value4] is put into cache!
[key5:value5] is put into cache!
[key6:value6] is put into cache!
[key0:value0] is evicted!
[key7:value7] is put into cache!
[key1:value1] is evicted!
[key8:value8] is put into cache!
[key2:value2] is evicted!
[key9:value9] is put into cache!
value6
[key3:value3] is evicted!
value_key

结论:

当调用LoadingCache的get方法时,如果缓存不存在对应key的记录,则CacheLoader中的load方法会被自动调用从外存加载数据,load方法的返回值会作为key对应的value存储到LoadingCache中,并从get方法返回。 

案例三:Callable

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

package com.example.anonotationnormal.guava;

import com.google.common.cache.*;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

/**
 * @author :luoyu
 * @version :1.0
 * @date : 2021/10/11 4:39 下午
 * @description
 */

public class GuavaCacheDemo {
    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>() {
                @Override
                public String call() throws Exception {
                    return "134";
                }
            }));

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

}

运行结果:

null
134
45

六、清楚缓存策略

任何Cache的容量都是有限的,而缓存清除策略就是决定数据在什么时候应该被清理掉。GuavaCache提了以下几种清除策略: 

基于存活时间的清除(Timed Eviction)

这应该是最常用的清除策略,在构建Cache实例的时候,CacheBuilder提供两种基于存活时间的构建方法:
(1)expireAfterAccess(long, TimeUnit):缓存项在创建后,在给定时间内没有被读/写访问,则清除。
(2)expireAfterWrite(long, TimeUnit):缓存项在创建后,在给定时间内没有被写访问(创建或覆盖),则清除。
expireAfterWrite()方法有些类似于redis中的expire命令,但显然它只能设置所有缓存都具有相同的存活时间。若遇到一些缓存数据的存活时间为1分钟,一些为5分钟,那只能构建两个Cache实例了。

显式清除


任何时候,你都可以显式地清除缓存项,而不是等到它被回收,Cache接口提供了如下API:
(1)个别清除:Cache.invalidate(key)
(2)批量清除:Cache.invalidateAll(keys)
(3)清除所有缓存项:Cache.invalidateAll()

基于容量的清除

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

内部实现采用LRU算法

基于引用回收

很好的利用了Java虚拟机的垃圾回收机制

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

清除什么时候发生?


也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
 

public class MainTest {
        static Cache<Integer, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .build();

        public static void main(String[] args) throws Exception {
            new Thread() { //monitor
                @Override
                public void run() {
                    while(true) {
                        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                        System.out.println(sdf.format(new Date()) +" size: "+cache.size());
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                        }
                    }
                };
            }.start();
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            cache.put(1, "Hi");
            System.out.println("write key:1 ,value:"+cache.getIfPresent(1));
            Thread.sleep(10000);
            // when write ,key:1 clear
            cache.put(2, "bbb");
            System.out.println("write key:2 ,value:"+cache.getIfPresent(2));
            Thread.sleep(10000);
            // when read other key ,key:2 do not clear
            System.out.println(sdf.format(new Date())
                    +" after write, key:1 ,value:"+cache.getIfPresent(1));
            Thread.sleep(2000);
            // when read same key ,key:2 clear
            System.out.println(sdf.format(new Date())
                    +" final, key:2 ,value:"+cache.getIfPresent(2));
        }
}

运行结果:

write key:1 ,value:Hi
16:10:49 size: 1
16:10:51 size: 1
16:10:53 size: 1
16:10:55 size: 1
16:10:57 size: 1
write key:2 ,value:bbb
16:10:59 size: 1
16:11:01 size: 1
16:11:03 size: 1
16:11:05 size: 1
16:11:07 after write, key:1 ,value:null
16:11:07 size: 1
16:11:09 final, key:2 ,value:null
16:11:09 size: 0
16:11:11 size: 0
16:11:13 size: 0
 

通过分析发现:
(1)缓存项<1,"Hi">的存活时间是5秒,但经过5秒后并没有被清除,因为还是size=1
(2)发生写操作cache.put(2, "bbb")后,缓存项<1,"Hi">被清除,因为size=1,而不是size=2
(3)发生读操作cache.getIfPresent(1)后,缓存项<2,"bbb">没有被清除,因为还是size=1,看来读操作确实不一定会发生清除
(4)发生读操作cache.getIfPresent(2)后,缓存项<2,"bbb">被清除,因为读的key就是2

这在GuavaCache被称为“延迟删除”,即删除总是发生得比较“晚”,这也是GuavaCache不同于其他Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如 基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动线程,不管是实现,还是使用起来都让人觉得简单(轻量)。
如果你还是希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。

七、总结


请一定要记住GuavaCache的实现代码中没有启动任何线程!!Cache中的所有维护操作,包括清除缓存、写入缓存等,都是通过调用线程来操作的。这在需要低延迟服务场景中使用时尤其需要关注,可能会在某个调用的响应时间突然变大。
GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCache。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值