摘要: 学习Google内部使用的工具包Guava,在Java项目中轻松地增长缓存,提升程序获取数据的效率。
1、什么是缓存?
根据科普中国的定义,缓存就是数据交换的缓冲区(称做Cache),当某一硬件要读取数据时,会首先从缓存中查找须要的数据,若是找到了则直接执行,找不到的话则从内存中找。因为缓存的运行速度比内存快得多,故缓存的做用就是帮助硬件更快地运行。java
在这里,咱们借用了硬件缓存的概念,当在Java程序中计算或查询数据的代价很高,而且对一样的计算或查询条件须要不止一次获取数据的时候,就应当考虑使用缓存。换句话说,缓存就是以空间换时间,大部分应用在各类IO,数据库查询等耗时较长的应用当中。mysql
2、缓存原理
当获取数据时,程序将先从一个存储在内存中的数据结构中获取数据。若是数据不存在,则在磁盘或者数据库中获取数据并存入到数据结构当中。以后程序须要再次获取数据时,则会先查询这个数据结构。从内存中获取数据时间明显小于经过IO获取数据,这个数据结构就是缓存的实现。git
这里引入一个概念,缓存命中率:从缓存中获取到数据的次数/所有查询次数,命中率越高说明这个缓存的效率好。因为机器内存的限制,缓存通常只能占据有限的内存大小,缓存须要不按期的删除一部分数据,从而保证不会占据大量内存致使机器崩溃。github
如何提升命中率呢?那就得从删除一部分数据着手了。目前有三种删除数据的方式,分别是:FIFO(先进先出)、LFU(按期淘汰最少使用次数)、LRU(淘汰最长时间未被使用)。sql
3、GuavaCache工做方式
GuavaCache的工做流程:获取数据->若是存在,返回数据->计算获取数据->存储返回。因为特定的工做流程,使用者必须在建立Cache或者获取数据时指定不存在数据时应当怎么获取数据。GuavaCache采用LRU的工做原理,使用者必须指定缓存数据的大小,当超过缓存大小时,一定引起数据删除。GuavaCache还可让用户指定缓存数据的过时时间,刷新时间等等不少有用的功能。数据库
4、GuavaCache使用Demo
4.1 简单使用
有人说我就想简简单单的使用cache,就像Map那样方便就行。接下来展现一段简单的使用方式。缓存
首先定义一个须要存储的Bean,对象Man:数据结构
/**异步
*/
public class Man {async
//身份证号 private String id; //姓名 private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Man{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}'; }
}
接下来咱们写一个Demo:
import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;
/**
*/
public class GuavaCachDemo {
private LoadingCache<String,Man> loadingCache; //loadingCache public void InitLoadingCache() { //指定一个若是数据不存在获取数据的方法 CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() { @Override public Man load(String key) throws Exception { //模拟mysql操做 Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("LoadingCache测试 从mysql加载缓存ing...(2s)"); Thread.sleep(2000); logger.info("LoadingCache测试 从mysql加载缓存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其余人"); if (key.equals("001")) { tmpman.setName("张三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }; //缓存数量为1,为了展现缓存删除效果 loadingCache = CacheBuilder.newBuilder().maximumSize(1).build(cacheLoader); } //获取数据,若是不存在返回null public Man getIfPresentloadingCache(String key){ return loadingCache.getIfPresent(key); } //获取数据,若是数据不存在则经过cacheLoader获取数据,缓存并返回 public Man getCacheKeyloadingCache(String key){ try { return loadingCache.get(key); } catch (ExecutionException e) { e.printStackTrace(); } return null; } //直接向缓存put数据 public void putloadingCache(String key,Man value){ Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("put key :{} value : {}",key,value.getName()); loadingCache.put(key,value); }
}
接下来,咱们写一些测试方法,检测一下
public class Test {
public static void main(String[] args){ GuavaCachDemo cachDemo = new GuavaCachDemo() System.out.println("使用loadingCache"); cachDemo.InitLoadingCache(); System.out.println("使用loadingCache get方法 第一次加载"); Man man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加载"); man = cachDemo.getIfPresentloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加载"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过,可是已经被剔除掉,验证从新加载"); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加载过"); man = cachDemo.getIfPresentloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get"); Man newMan = new Man(); newMan.setId("001"); newMan.setName("额外添加"); cachDemo.putloadingCache("001",newMan); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); }
}
测试结果以下:
150850_81Jv_1983603.png
4.2 高级特性
因为目前使用有局限性,接下来只讲我用到的一些方法。
我来演示一下GuavaCache自带的两个Cache
GuavaCacheDemo.java
import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;
/**
*/
public class GuavaCachDemo {
private Cache<String, Man> cache; private LoadingCache<String,Man> loadingCache; private RemovalListener<String, Man> removalListener; public void Init(){ //移除key-value监听器 removalListener = new RemovalListener<String, Man>(){ public void onRemoval(RemovalNotification<String, Man> notification) { Logger logger = LoggerFactory.getLogger("RemovalListener"); logger.info(notification.getKey()+"被移除"); //能够在监听器中获取key,value,和删除缘由 notification.getValue(); notification.getCause();//EXPLICIT、REPLACED、COLLECTED、EXPIRED、SIZE }}; //可使用RemovalListeners.asynchronous方法将移除监听器设为异步方法 //removalListener = RemovalListeners.asynchronous(removalListener, new ThreadPoolExecutor(1,1,1000, TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(1))); } //loadingCache public void InitLoadingCache() { //指定一个若是数据不存在获取数据的方法 CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() { @Override public Man load(String key) throws Exception { //模拟mysql操做 Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("LoadingCache测试 从mysql加载缓存ing...(2s)"); Thread.sleep(2000); logger.info("LoadingCache测试 从mysql加载缓存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其余人"); if (key.equals("001")) { tmpman.setName("张三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }; //缓存数量为1,为了展现缓存删除效果 loadingCache = CacheBuilder.newBuilder(). //设置2分钟没有获取将会移除数据 expireAfterAccess(2, TimeUnit.MINUTES). //设置2分钟没有更新数据则会移除数据 expireAfterWrite(2, TimeUnit.MINUTES). //每1分钟刷新数据 refreshAfterWrite(1,TimeUnit.MINUTES). //设置key为弱引用 weakKeys().
// weakValues().//设置存在时间和刷新时间后不能再次设置
// softValues().//设置存在时间和刷新时间后不能再次设置
maximumSize(1). removalListener(removalListener). build(cacheLoader); } //获取数据,若是不存在返回null public Man getIfPresentloadingCache(String key){ return loadingCache.getIfPresent(key); } //获取数据,若是数据不存在则经过cacheLoader获取数据,缓存并返回 public Man getCacheKeyloadingCache(String key){ try { return loadingCache.get(key); } catch (ExecutionException e) { e.printStackTrace(); } return null; } //直接向缓存put数据 public void putloadingCache(String key,Man value){ Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("put key :{} value : {}",key,value.getName()); loadingCache.put(key,value); } public void InitDefault() { cache = CacheBuilder.newBuilder(). expireAfterAccess(2, TimeUnit.MINUTES). expireAfterWrite(2, TimeUnit.MINUTES).
// refreshAfterWrite(1,TimeUnit.MINUTES).//没有cacheLoader的cache不能设置刷新,由于没有指定获取数据的方式
weakKeys().
// weakValues().//设置存在时间和刷新时间后不能再次设置
// softValues().//设置存在时间和刷新时间后不能再次设置
maximumSize(1). removalListener(removalListener). build(); } public Man getIfPresentCache(String key){ return cache.getIfPresent(key); } public Man getCacheKeyCache(final String key) throws ExecutionException { return cache.get(key, new Callable<Man>() { public Man call() throws Exception { //模拟mysql操做 Logger logger = LoggerFactory.getLogger("Cache"); logger.info("Cache测试 从mysql加载缓存ing...(2s)"); Thread.sleep(2000); logger.info("Cache测试 从mysql加载缓存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其余人"); if (key.equals("001")) { tmpman.setName("张三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }); } public void putCache(String key,Man value){ Logger logger = LoggerFactory.getLogger("Cache"); logger.info("put key :{} value : {}",key,value.getName()); cache.put(key,value); }
}
在这个demo中,分别采用了Guava自带的两个Cache:LocalLoadingCache和LocalManualCache。而且添加了监听器,当数据被删除后会打印日志。
Main:
public static void main(String[] args){
GuavaCachDemo cachDemo = new GuavaCachDemo(); cachDemo.Init(); System.out.println("使用loadingCache"); cachDemo.InitLoadingCache(); System.out.println("使用loadingCache get方法 第一次加载"); Man man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加载"); man = cachDemo.getIfPresentloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加载"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加载过,可是已经被剔除掉,验证从新加载"); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加载过"); man = cachDemo.getIfPresentloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get"); Man newMan = new Man(); newMan.setId("001"); newMan.setName("额外添加"); cachDemo.putloadingCache("001",newMan); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); /////////////////////////////////// System.out.println("\n\n使用Cache"); cachDemo.InitDefault(); System.out.println("使用Cache get方法 第一次加载"); try { man = cachDemo.getCacheKeyCache("001"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(man); System.out.println("\n使用Cache getIfPresent方法 第一次加载"); man = cachDemo.getIfPresentCache("002"); System.out.println(man); System.out.println("\n使用Cache get方法 第一次加载"); try { man = cachDemo.getCacheKeyCache("002"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(man); System.out.println("\n使用Cache get方法 已加载过"); try { man = cachDemo.getCacheKeyCache("002"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(man); System.out.println("\n使用Cache get方法 已加载过,可是已经被剔除掉,验证从新加载"); try { man = cachDemo.getCacheKeyCache("001"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(man); System.out.println("\n使用Cache getIfPresent方法 已加载过"); man = cachDemo.getIfPresentCache("001"); System.out.println(man); System.out.println("\n使用Cache put方法 再次get"); Man newMan1 = new Man(); newMan1.setId("001"); newMan1.setName("额外添加"); cachDemo.putloadingCache("001",newMan1); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man);
}
测试结果以下:
152412_Afd2_1983603.png
152425_uKCJ_1983603.png
由上述结果能够代表,GuavaCache能够在数据存储到达指定大小后删除数据结构中的数据。咱们能够设置按期删除而达到按期从数据库、磁盘等其余地方更新数据等(再次访问时数据不存在从新获取)。也能够采用定时刷新的方式更新数据。
还能够设置移除监听器对被删除的数据进行一些操做。经过RemovalListeners.asynchronous(RemovalListener,Executor)方法将监听器设为异步,笔者经过实验发现,异步监听不会在删除数据时马上调用监听器方法。
5、GuavaCache结构初探
153356_Z1zV_1983603.png
类结构图
GuavaCache并不但愿咱们设置复杂的参数,而让咱们采用建造者模式建立Cache。GuavaCache分为两种Cache:Cache,LoadingCache。LoadingCache继承了Cache,他比Cache主要多了get和refresh方法。多这两个方法能干什么呢?
在第四节高级特性demo中,咱们看到builder生成不带CacheLoader的Cache实例。在类结构图中实际上是生成了LocalManualCache类实例。而带CacheLoader的Cache实例生成的是LocalLoadingCache。他能够定时刷新数据,由于获取数据的方法已经做为构造参数方法存入了Cache实例中。一样,在get时,不须要像LocalManualCache还须要传入一个Callable实例。
实际上,这两个Cache实现类都继承自LocalCache,大部分实现都是父类作的。
6、总结回顾
缓存加载:CacheLoader、Callable、显示插入(put)
缓存回收:LRU,定时(expireAfterAccess,expireAfterWrite),软弱引用,显示删除(Cache接口方法invalidate,invalidateAll)
监听器:CacheBuilder.removalListener(RemovalListener)
清理缓存时间:只有在获取数据时才或清理缓存LRU,使用者能够单起线程采用Cache.cleanUp()方法主动清理。
刷新:主动刷新方法LoadingCache.referesh(K)
信息统计:CacheBuilder.recordStats() 开启Guava Cache的统计功能。Cache.stats() 返回CacheStats对象。(其中包括命中率等相关信息)
获取当前缓存全部数据:cache.asMap(),cache.asMap().get(Object)会刷新数据的访问时间(影响的是:建立时设置的在多久没访问后删除数据)
LocalManualCache和LocalLoadingCache的选择
ManualCache能够在get时动态设置获取数据的方法,而LoadingCache能够定时刷新数据。如何取舍?我认为在缓存数据有不少种类的时候采用第一种cache。而数据单一,数据库数据会定时刷新时采用第二种cache。
参考资料:
http://www.cnblogs.com/peida/...
https://github.com/tiantianga...
http://www.blogjava.net/DLevi...