GuavaCache实现

参考:

https://blog.csdn.net/guozebo/article/details/51590517

https://www.cnblogs.com/dennyzhangdd/p/8981982.html (guavaCache过期策略详解,有例子)

一 分类

本地缓存的实现方式 :

  1.  ehcache

  2. GuavaCache

分布式缓存的实现方式 : 

  1. redis
  2. memcached

二 GuavaCache介绍

GuavaCache是google开源java类库Guava的其中一个模块,在maven工程下使用可在pom文件加入如下依赖:

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

三 Cache接口及其实现

先说说一般的cache都会实现的基础功能包括:

提供一个存储缓存的容器,该容器实现了存放(Put)和读取(Get)缓存的接口供外部调用。 缓存通常以<key,value>的形式存在,通过key来从缓存中获取value。当然容器的大小往往是有限的(受限于内存大小),需要为它设置清除缓存的策略。

在GuavaCache中缓存的容器被定义为接口Cache<K, V>的实现类,这些实现类都是线程安全的,因此通常定义为一个单例。并且接口Cache是泛型,很好的支持了不同类型的key和value。作为示例,我们构建一个key为Integer、value为String的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();

据说GuavaCache的实现是基于ConcurrentHashMap的,因此上面的构造过程所调用的方法,通过查看其官方文档也能看到一些类似的原理。比如通过initialCapacity(5)定义初始值大小,要是定义太大就好浪费内存空间,要是太小,需要扩容的时候就会像map一样需要resize,这个过程会产生大量需要gc的对象,还有比如通过concurrencyLevel(5)来限制写入操作的并发数,这和ConcurrentHashMap的锁机制也是类似的(ConcurrentHashMap读不需要加锁,写入需要加锁,每个segment都有一个锁)。
 

接下来看看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();
}

使用过程还是要认真查看官方的文档,以下Demo简单的展示了Cache的写入,读取,和过期清除策略是否生效:

	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);
		}
	}

四 清除缓存策略

  1. 基于存活时间清除
  2. 基于容量清除
  3. 显式清除
  4. 基于引用清除

基于存活时间清除

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

定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收

基于容量清除

在构建Cache实例的时候,通过CacheBuilder.maximumSize(long)方法可以设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存。
以上是这种方式是以缓存的“数量”作为容量的计算方式,还有另外一种基于“权重”的计算方式。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights)。你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。

显式清除

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

 基于引用清除

在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过一般不轻易使用这个特性。
(1)CacheBuilder.weakKeys():使用弱引用存储键
(2)CacheBuilder.weakValues():使用弱引用存储值
(3)CacheBuilder.softValues():使用软引用存储值

五 清除何时发生

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

public class CacheService {
	static Cache<Integer, String> cache = CacheBuilder.newBuilder()
			.expireAfterWrite(5, TimeUnit.SECONDS)
			.build();
	
	public static void main(String[] args) throws Exception {
		new Thread() { //monitor
			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));
	}
}

控制台输出 : 

00:34:17 size: 0
write key:1 ,value:Hi
00:34:19 size: 1
00:34:21 size: 1
00:34:23 size: 1
00:34:25 size: 1
write key:2 ,value:bbb
00:34:27 size: 1
00:34:29 size: 1
00:34:31 size: 1
00:34:33 size: 1
00:34:35 size: 1
00:34:37 after write, key:1 ,value:null
00:34:37 size: 1
00:34:39 final, key:2 ,value:null
00:34:39 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可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。

六 LoadingCache和Cache对象调用get方法的区别


创建loadingCache类型的对象

@Test
    public void testMethod() throws ExecutionException {

        LoadingCache<String,Integer> infos = CacheBuilder.newBuilder()
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String s) throws Exception {
                        return 1;
                    }
                });
        
        //如果本地缓存中不存在a , 那么将回调load方法 , 获取值(比如从数据库中获取...)
        //LoadingCache对象可以直接调用get方法
        //这句话相当于给a赋值为1
        infos.get("a");
        
    }

 创建Cache类型的对象

    @Test
    public void testMethod() throws ExecutionException {

        Cache<String,Integer> infos = CacheBuilder.newBuilder()
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build();

        //cache对象调用get方法需要第二个参数,即匿名内部类,重写call方法
        //call方法中写具体的业务
        infos.get("a", new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });

        System.out.println(infos.getIfPresent("a"));

    }
发布了58 篇原创文章 · 获赞 3 · 访问量 5687

猜你喜欢

转载自blog.csdn.net/hc1428090104/article/details/103615090