摘要: 學習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...