GuavaCache本地缓存学习总结

https://my.oschina.net/u/2270476/blog/1805749

http://www.cnblogs.com/parryyang/p/5777019.html

https://www.jianshu.com/p/5299f5a11bd5

https://www.cnblogs.com/vikde/p/8045226.html

https://blog.csdn.net/fly910905/article/details/80976161

https://www.cnblogs.com/yuxiang1/p/9282952.html 解析Java分布式系统中的缓存架构(上)

google guava cache使用方法

1.简介

guava cache是google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中.实际项目开发中经常将一些比较公共或者常用的数据缓存起来方便快速访问.

内存缓存最常见的就是基于HashMap实现的缓存,为了解决并发问题也可能也会用到ConcurrentHashMap等并发集合,但是内存缓存需要考虑很多问题,包括并发问题、缓存过期机制、缓存移除机制、缓存命中统计率等.

guava cache已经考虑到这些问题,可以上手即用.通过CacheBuilder创建缓存、然后设置缓存的相关参数、设置缓存的加载方法等.在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如Redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。

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(); 
// 放入缓存 cache.put("key", "value");
// 获取缓存 String value = cache.getIfPresent("key");

LoadingCache

LoadingCache继承自Cache,在构建LoadingCache时,需要通过CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法构建:

LoadingCache,顾名思义,它能够通过CacheLoader自发的加载缓存:如果有缓存则返回;否则运算、缓存、然后返回
CacheBuilder.newBuilder()
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                // 缓存加载逻辑
                ...
            }
        });

2.缓存回收

Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

基于容量的回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。

定时回收(Timed Eviction)

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

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

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。

显式清除

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

移除监听器

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

清理什么时候发生?

使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。

缓存刷新

在Guava cache中支持定时刷新和显式刷新两种方式,其中只有LoadingCache能够进行定时刷新。

在进行缓存定时刷新时,我们需要指定缓存的刷新间隔,和一个用来加载缓存的CacheLoader,当达到刷新时间间隔后,下一次获取缓存时,会调用CacheLoader的load方法刷新缓存。例如构建个刷新频率为10分钟的缓存:

CacheBuilder.newBuilder()
        // 设置缓存在写入10分钟后,通过CacheLoader的load方法进行刷新
        .refreshAfterWrite(10, TimeUnit.SECONDS)
        // jdk8以后可以使用 Duration
        // .refreshAfterWrite(Duration.ofMinutes(10))
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                // 缓存加载逻辑
                ...
            }
        });

Guava Cache缓存回收:

  • 基于容量和定时的回收
  • LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(new CacheLoader<String, Object>() {
     
    @Override
    public Object load(String key) throws Exception {
    return generateValueByKey(key);
    }
    });
    try {
    System.out.println(caches.get("key-zorro"));
    } catch (ExecutionException e) {
    e.printStackTrace();
    }
  1. 如代码所示,新建了名为caches的一个缓存对象,maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上线时,则会进行缓存回收,回收最近没有使用或总体上很少使用的缓存项。需要注意的是在接近这个容量上限时就会发生,所以在定义这个值的时候需要视情况适量地增大一点。 
  2. 另外通过expireAfterWrite这个方法定义了缓存的过期时间,写入十分钟之后过期。 
    1.  
      CacheBuilder提供两种定时回收的方法:
    2.  
       
    3.  
      expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。
    4.  
      请注意这种缓存的回收顺序和基于大小回收一样。
    5.  
       
    6.  
      expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。
    7.  
      如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
  3. 在build方法里,传入了一个CacheLoader对象,重写了其中的load方法。当获取的缓存值不存在或已过期时,则会调用此load方法,进行缓存值的计算。 
  4. 这就是最简单也是我们平常最常用的一种使用方法。定义了缓存大小、过期时间及缓存值生成方法。
  5. 如果用其他的缓存方式,如redis,我们知道上面这种“如果有缓存则返回;否则运算、缓存、然后返回”的缓存模式是有很大弊端的。当高并发条件下同时进行get操作,而此时缓存值已过期时,会导致大量线程都调用生成缓存值的方法,比如从数据库读取。这时候就容易造成数据库雪崩。这也就是我们常说的“缓存穿透”。 
  6. 而Guava cache则对此种情况有一定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存穿透的危险。

Guava Cache定时刷新:

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

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
.maximumSize(100)
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, Object>() {
 
@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方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。

Guava Cache异步刷新:

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

ListeningExecutorService backgroundRefreshPools = 
                MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
 
                    @Override
                    public ListenableFuture<Object> reload(String key,
                            Object oldValue) throws Exception {
                        return backgroundRefreshPools.submit(new Callable<Object>() {
 
                            @Override
                            public Object call() throws Exception {
                                return generateValueByKey(key);
                            }
                        });
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

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

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

原文地址:https://www.cnblogs.com/wwmiert/p/10730035.html