原文地址 http://blog.csdn.net/guozebo/article/details/51590517

前言

在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache。之前用spring cache的时候集成的是ehcache,但接触到GuavaCache之后,被它的简单、强大、及轻量级所吸引。它不需要配置文件,使用起来和ConcurrentHashMap一样简单,而且能覆盖绝大多数使用cache的场景需求!

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

  1. <dependency>
  2. <groupId>com.google.guava</groupId>
  3. <artifactId>guava</artifactId>
  4. <version>19.0</version>
  5. </dependency>

Cache接口及其实现

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

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

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

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

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

接下来看看Cache提供哪些方法(只列了部分常用的):

  1. /**
  2. * 该接口的实现被认为是线程安全的,即可在多线程中调用
  3. * 通过被定义单例使用
  4. */
  5. public interface Cache<K, V> {
  6. /**
  7. * 通过key获取缓存中的value,若不存在直接返回null
  8. */
  9. V getIfPresent(Object key);
  10. /**
  11. * 通过key获取缓存中的value,若不存在就通过valueLoader来加载该value
  12. * 整个过程为 "if cached, return; otherwise create, cache and return"
  13. * 注意valueLoader要么返回非null值,要么抛出异常,绝对不能返回null
  14. */
  15. V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
  16. /**
  17. * 添加缓存,若key存在,就覆盖旧值
  18. */
  19. void put(K key, V value);
  20. /**
  21. * 删除该key关联的缓存
  22. */
  23. void invalidate(Object key);
  24. /**
  25. * 删除所有缓存
  26. */
  27. void invalidateAll();
  28. /**
  29. * 执行一些维护操作,包括清理缓存
  30. */
  31. void cleanUp();
  32. }

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

  1. public static void main(String[] args) throws Exception {
  2. cache.put(1, "Hi");
  3. for(int i=0 ;i<100 ;i++) {
  4. SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
  5. System.out.println(sdf.format(new Date())
  6. + "  key:1 ,value:"+cache.getIfPresent(1));
  7. Thread.sleep(1000);
  8. }
  9. }

清除缓存的策略

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

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

这应该是最常用的清除策略,在构建Cache实例的时候,CacheBuilder提供两种基于存活时间的构建方法:

(1)expireAfterAccess(long, TimeUnit):缓存项在创建后,在给定时间内没有被读/写访问,则清除。
(2)expireAfterWrite(long, TimeUnit):缓存项在创建后,在给定时间内没有被写访问(创建或覆盖),则清除。

expireAfterWrite()方法有些类似于redis中的expire命令,但显然它只能设置所有缓存都具有相同的存活时间。若遇到一些缓存数据的存活时间为1分钟,一些为5分钟,那只能构建两个Cache实例了。
 

基于容量的清除(size-based eviction)

在构建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()

基于引用的清除(Reference-based Eviction)

在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过一般不轻易使用这个特性。

(1)CacheBuilder.weakKeys():使用弱引用存储键
(2)CacheBuilder.weakValues():使用弱引用存储值
(3)CacheBuilder.softValues():使用软引用存储值

 

清除什么时候发生?

也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
  1. public class CacheService {
  2. static Cache<Integer, String> cache = CacheBuilder.newBuilder()
  3. .expireAfterWrite(5, TimeUnit.SECONDS)
  4. .build();
  5. public static void main(String[] args) throws Exception {
  6. new Thread() { //monitor
  7. public void run() {
  8. while(true) {
  9. SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
  10. System.out.println(sdf.format(new Date()) +" size: "+cache.size());
  11. try {
  12. Thread.sleep(2000);
  13. } catch (InterruptedException e) {
  14. }
  15. }
  16. };
  17. }.start();
  18. SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
  19. cache.put(1, "Hi");
  20. System.out.println("write key:1 ,value:"+cache.getIfPresent(1));
  21. Thread.sleep(10000);
  22. // when write ,key:1 clear
  23. cache.put(2, "bbb");
  24. System.out.println("write key:2 ,value:"+cache.getIfPresent(2));
  25. Thread.sleep(10000);
  26. // when read other key ,key:2 do not clear
  27. System.out.println(sdf.format(new Date())
  28. +" after write, key:1 ,value:"+cache.getIfPresent(1));
  29. Thread.sleep(2000);
  30. // when read same key ,key:2 clear
  31. System.out.println(sdf.format(new Date())
  32. +" final, key:2 ,value:"+cache.getIfPresent(2));
  33. }
  34. }
控制台输出:
  1. 00:34:17 size: 0
  2. write key:1 ,value:Hi
  3. 00:34:19 size: 1
  4. 00:34:21 size: 1
  5. 00:34:23 size: 1
  6. 00:34:25 size: 1
  7. write key:2 ,value:bbb
  8. 00:34:27 size: 1
  9. 00:34:29 size: 1
  10. 00:34:31 size: 1
  11. 00:34:33 size: 1
  12. 00:34:35 size: 1
  13. 00:34:37 after write, key:1 ,value:null
  14. 00:34:37 size: 1
  15. 00:34:39 final, key:2 ,value:null
  16. 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可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。
 
 

总结

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

参考

官方github

官方文档中文翻译

GuavaCache简介(一)的更多相关文章

  1. GuavaCache简介(一)是轻量级的框架 少量数据,并且 过期时间相同 可以用 GuavaCache

    还有一篇文章是讲解redis 如何删除过期数据的,参考:Redis的内存回收策略和内存上限(阿里) 划重点:在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清 ...

  2. Guava Cache本地缓存

    Guava介绍 Guava是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库. 这个库是为了方便编码,并减少编码错误. 这个库提供用于集合,缓存,支持原语,并发性,常见注解,字 ...

  3. Redis的内存回收策略和内存上限(阿里)

    还有一篇文章 讲解guava如何删除过期数据的,与redis不同,guava没有维护线程删除过期key,只是在设置 key 或者 读取key的时候,顺带删除参考:GuavaCache简介(一)是轻量级 ...

  4. GuavaCache学习笔记三:底层源码阅读

    申明:转载自 https://www.cnblogs.com/dennyzhangdd/p/8981982.html 感谢原博主的分享,看到这个写的真好,直接转载来,学习了. 另外也推荐另外一篇Gua ...

  5. ASP.NET Core 1.1 简介

    ASP.NET Core 1.1 于2016年11月16日发布.这个版本包括许多伟大的新功能以及许多错误修复和一般的增强.这个版本包含了多个新的中间件组件.针对Windows的WebListener服 ...

  6. MVVM模式和在WPF中的实现(一)MVVM模式简介

    MVVM模式解析和在WPF中的实现(一) MVVM模式简介 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在 ...

  7. Cassandra简介

    在前面的一篇文章<图形数据库Neo4J简介>中,我们介绍了一种非常流行的图形数据库Neo4J的使用方法.而在本文中,我们将对另外一种类型的NoSQL数据库——Cassandra进行简单地介 ...

  8. REST简介

    一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式.”但是在要求详细讲述它所提出的各个约束,以及如何开始搭建REST服务时,却很少有人能够清晰地说出它到底是什么,需要遵守什么样的准则. ...

  9. Microservice架构模式简介

    在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书<Building Microservices>.该书描述了如何按照Mic ...

随机推荐

  1. js生成邀请码(2)

    //生成邀请码方法一 /*function createInviteCode() { var s = [],a=6,b=10; var chars = "123456789QWERTYUIP ...

  2. 1106SQLserver基础--变量、运算符的使用,if...else,while语句

    数据库---变量(对数据库中的数据没有任何影响) 作用:临时存储数据的作用,起一个衔接的作用,为了方便理解存储过程. 例:Declare @hello varchar(20) Set @hello=’ ...

  3. mysql如何开启远程连接(默认未开启,即使密码正确,仍然无法访问)

    mysql如何开启远程连接 | 浏览:1846 | 更新:2015-03-11 20:19 1 2 3 4 5 6 分步阅读 百度经验:jingyan.baidu.com 大家在公司工作中,经常会遇到 ...

  4. 来自T00ls的帖子-XSS的奇技淫巧

    T00LS在前段时间开启了markdown支持,这个漏洞也正是markdown的问题导致. Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定 ...

  5. eclipse中maven的配置与使用

    以eclipse Juno版本为例 1.插件安装 eclipse==>help====>install new software===>add name :m2e location: ...

  6. Java的JAR包, EAR包 ,WAR包 都是干什么的,有什么区别

    JAR包:打成JAR包的代码,一般作为工具类,在项目中,会应用到N多JAR工具包: WAR包:JAVA WEB工程,都是打成WAR包,进行发布,如果我们的服务器选择TOMCAT等轻量级服务器,一般就打 ...

  7. Android中同一个ImageView中根据状态显示不同图片

    一般: if(条件1) { image.setBackground(R.id.xxx1); } else if (条件2) { image.setBackground(R.id.xxx2); } 实际 ...

  8. group()、start()、end()、span()

  9. find查找、split分隔、replace替换

    #!/usr/bin/env python r = "asada" ret = r.find("d") print(ret)#返回所在位置的索引 ret =r. ...

  10. 第一个Dockerfile

    1. 创建docker目录 $ mkdir docker && cd docker 2. 编写Dockerfile $ vim Dockerfile [docker/Dockfile] ...