緩存在應(yīng)用中是必不可少的斤程,經(jīng)常用的如redis角寸、memcache以及內(nèi)存緩存等。Guava是Google出的一個工具包忿墅,它里面的cache即是對本地內(nèi)存緩存的一種實(shí)現(xiàn)扁藕,支持多種緩存過期策略。?
Guava cache的緩存加載方式有兩種:
CacheLoader
Callable callback
具體兩種方式的介紹看官方文檔:http://ifeve.com/google-guava-cachesexplained/
接下來看看常見的一些使用方法疚脐。?
后面的示例實(shí)踐都是以CacheLoader方式加載緩存值亿柑。
LoadingCache caches = CacheBuilder.newBuilder()
? ? ? ? ? ? ? ? .maximumSize(100)
? ? ? ? ? ? ? ? .expireAfterWrite(10, TimeUnit.MINUTES)
? ? ? ? ? ? ? ? .build(new CacheLoader() {
? ? ? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? ? ? public Object load(String key) throws Exception {
? ? ? ? ? ? ? ? ? ? ? ? return generateValueByKey(key);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? });try {
? ? System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
? ? e.printStackTrace();
}
如代碼所示新建了名為caches的一個緩存對象,maximumSize定義了緩存的容量大小棍弄,當(dāng)緩存數(shù)量即將到達(dá)容量上線時望薄,則會進(jìn)行緩存回收,回收最近沒有使用或總體上很少使用的緩存項(xiàng)呼畸。需要注意的是在接近這個容量上限時就會發(fā)生痕支,所以在定義這個值的時候需要視情況適量地增大一點(diǎn)。?
另外通過expireAfterWrite這個方法定義了緩存的過期時間蛮原,寫入十分鐘之后過期卧须。?
在build方法里,傳入了一個CacheLoader對象,重寫了其中的load方法花嘶。當(dāng)獲取的緩存值不存在或已過期時笋籽,則會調(diào)用此load方法,進(jìn)行緩存值的計算椭员。?
這就是最簡單也是我們平常最常用的一種使用方法车海。定義了緩存大小、過期時間及緩存值生成方法隘击。
如果用其他的緩存方式侍芝,如redis,我們知道上面這種“如果有緩存則返回闸度;否則運(yùn)算竭贩、緩存、然后返回”的緩存模式是有很大弊端的莺禁。當(dāng)高并發(fā)條件下同時進(jìn)行g(shù)et操作留量,而此時緩存值已過期時,會導(dǎo)致大量線程都調(diào)用生成緩存值的方法哟冬,比如從數(shù)據(jù)庫讀取楼熄。這時候就容易造成數(shù)據(jù)庫雪崩。這也就是我們常說的“緩存穿透”浩峡。?
而Guava cache則對此種情況有一定控制可岂。當(dāng)大量線程用相同的key獲取緩存值時,只會有一個線程進(jìn)入load方法翰灾,而其他線程則等待缕粹,直到緩存值被生成。這樣也就避免了緩存穿透的危險纸淮。
如上的使用方法平斩,雖然不會有緩存穿透的情況,但是每當(dāng)某個緩存值過期時咽块,老是會導(dǎo)致大量的請求線程被阻塞绘面。而Guava則提供了另一種緩存策略,緩存值定時刷新:更新線程調(diào)用load方法更新該緩存侈沪,其他請求線程返回該緩存的舊值揭璃。這樣對于某個key的緩存來說,只會有一個線程被阻塞亭罪,用來生成緩存值瘦馍,而其他的線程都返回舊的緩存值,不會被阻塞皆撩。?
這里就需要用到Guava cache的refreshAfterWrite方法扣墩。如下所示:
LoadingCache caches = CacheBuilder.newBuilder()
? ? ? ? ? ? ? ? .maximumSize(100)
? ? ? ? ? ? ? ? .refreshAfterWrite(10, TimeUnit.MINUTES)
? ? ? ? ? ? ? ? .build(new CacheLoader() {
? ? ? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? ? ? public Object load(String key) throws Exception {
? ? ? ? ? ? ? ? ? ? ? ? return generateValueByKey(key);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? });try {
? ? System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
? ? e.printStackTrace();
}
如代碼所示哲银,每隔十分鐘緩存值則會被刷新扛吞。
此外需要注意一個點(diǎn)呻惕,這里的定時并不是真正意義上的定時。Guava cache的刷新需要依靠用戶請求線程滥比,讓該線程去進(jìn)行l(wèi)oad方法的調(diào)用亚脆,所以如果一直沒有用戶嘗試獲取該緩存值,則該緩存也并不會刷新盲泛。
如2中的使用方法濒持,解決了同一個key的緩存過期時會讓多個線程阻塞的問題,只會讓用來執(zhí)行刷新緩存操作的一個用戶線程會被阻塞寺滚。由此可以想到另一個問題柑营,當(dāng)緩存的key很多時,高并發(fā)條件下大量線程同時獲取不同key對應(yīng)的緩存村视,此時依然會造成大量線程阻塞官套,并且給數(shù)據(jù)庫帶來很大壓力。這個問題的解決辦法就是將刷新緩存值的任務(wù)交給后臺線程蚁孔,所有的用戶請求線程均返回舊的緩存值奶赔,這樣就不會有用戶線程被阻塞了。?
詳細(xì)做法如下:
ListeningExecutorService backgroundRefreshPools =
? ? ? ? ? ? ? ? MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
? ? ? ? LoadingCache caches = CacheBuilder.newBuilder()
? ? ? ? ? ? ? ? .maximumSize(100)
? ? ? ? ? ? ? ? .refreshAfterWrite(10, TimeUnit.MINUTES)
? ? ? ? ? ? ? ? .build(new CacheLoader() {
? ? ? ? ? ? ? ? ? ? @Override? ? ? ? ? ? ? ? ? ? public Object load(String key) throws Exception {
? ? ? ? ? ? ? ? ? ? ? ? return generateValueByKey(key);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? @Override? ? ? ? ? ? ? ? ? ? public ListenableFuture reload(String key,
? ? ? ? ? ? ? ? ? ? ? ? ? ? Object oldValue) throws Exception {
? ? ? ? ? ? ? ? ? ? ? ? return backgroundRefreshPools.submit(new Callable() {
? ? ? ? ? ? ? ? ? ? ? ? ? ? @Override? ? ? ? ? ? ? ? ? ? ? ? ? ? public Object call() throws Exception {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? return generateValueByKey(key);
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? });try {
? ? System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
? ? e.printStackTrace();
}
在上面的代碼中杠氢,我們新建了一個線程池站刑,用來執(zhí)行緩存刷新任務(wù)。并且重寫了CacheLoader的reload方法鼻百,在該方法中建立緩存刷新的任務(wù)并提交到線程池绞旅。?
注意此時緩存的刷新依然需要靠用戶線程來驅(qū)動,只不過和2不同之處在于該用戶線程觸發(fā)刷新操作之后温艇,會立馬返回舊的緩存值因悲。
可以看到防緩存穿透和防用戶線程阻塞都是依靠返回舊值來完成的。所以如果沒有舊值中贝,同樣會全部阻塞囤捻,因此應(yīng)視情況盡量在系統(tǒng)啟動時將緩存內(nèi)容加載到內(nèi)存中。
在刷新緩存時邻寿,如果generateValueByKey方法出現(xiàn)異承粒或者返回了null,此時舊值不會更新绣否。
題外話:在使用內(nèi)存緩存時誊涯,切記拿到緩存值之后不要在業(yè)務(wù)代碼中對緩存直接做修改,因?yàn)榇藭r拿到的對象引用是指向緩存真正的內(nèi)容的蒜撮。如果需要直接在該對象上進(jìn)行修改暴构,則在獲取到緩存值后拷貝一份副本跪呈,然后傳遞該副本,進(jìn)行修改操作取逾。