前言
提到緩存鱼冀,可能第一時間想到的就是Redis、Memcache等任连,這些都屬于是分布式緩存蚤吹,而在某些場景下我們可能并不需要分布式緩存,畢竟需要多引入維護(hù)一個中間件随抠,那么在數(shù)據(jù)量小裁着,且訪問頻繁,或者說一些不會變的靜態(tài)配置數(shù)據(jù)我們都可以考慮放置到本地緩存中拱她,那么我們平時是怎么做的呢二驰?相信大家在寫或者在讀有關(guān)本地緩存代碼時,都會看到如下實(shí)現(xiàn)方式:
private static final Map<K,V> LOCAL_CACHE = new ConcurrentHashMap<>();
的確這種方式簡單有效秉沼,但是帶來的弊端就是過于簡單桶雀,功能也就過于缺乏矿酵,而且如果使用不大,將帶來可怕的內(nèi)存溢出矗积,比如談起緩存全肮,那不得不提緩存淘汰策略、緩存過期策略等棘捣,但是不要著急辜腺,強(qiáng)大的Guava工具庫已經(jīng)為我們提供了簡單有效的Guava Cache。
值得注意的是乍恐,請不要被強(qiáng)大的Guava Cache迷惑雙眼评疗,如果你的緩存場景用不到這些緩存的特性,那么ConcurrentHashMap或許是你最好的選擇
Guava Cache
Guava Cache能力一覽
入門使用
key對應(yīng)的緩存值計算方式
緩存無非可能就是緩存那些耗時很長的計算(除了CPU型任務(wù)茵烈,I/O型也算)出來值壤巷,只有第一次從緩存中訪問指定key時,才會進(jìn)行真正的計算瞧毙,那么Guava Cache就提供三種緩存計算方式胧华,你也可以理解為緩存加載方式,它們分別是CacheLoader宙彪、Callable矩动、直接插入。
CacheLoader
CacheLoader方式释漆,簡單點(diǎn)說就是計算方式作用于所有key悲没,也就是說通過CacheLoader方法創(chuàng)建的Cache,不管你訪問哪個key男图,它的計算方式都是同一個示姿,來看示例:
@Test
public void guavaCacheTest001(){
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(2)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key+"真正計算了!");
return "cache-"+key;
}
});
System.out.println(loadingCache.getUnchecked("key1"));
System.out.println(loadingCache.getUnchecked("key1"));
System.out.println(loadingCache.getUnchecked("key2"));
System.out.println(loadingCache.getUnchecked("key2"));
}
對應(yīng)輸出:
key1真正計算了逊笆!
cache-key1
cache-key1
key2真正計算了栈戳!
cache-key2
cache-key2
在這個例子中,我們通過給CacheBuilder的build方法傳入一個CacheLoad的匿名類难裆,該CacheLoad的load方法邏輯為當(dāng)獲取某個緩存key時子檀,如果該key緩存中不存在,則將計算其緩存值的計算方式乃戈。從輸出我們可以看到褂痰,只有緩存第一次訪問時才真正執(zhí)行了值的計算行為,并且每個緩存key的計算方式都一樣症虑。
Callable
當(dāng)對CacheLoader有了認(rèn)識之后缩歪,你可能會想:如果我針對不同的緩存key的計算緩存值方式并不一樣,那該怎么辦暗尽匪蝙!苟翻,別急,Callable為你保駕護(hù)航:
@Test
public void testCallable() throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
Object cacheKey1 = cache.get("key1", () -> {
System.out.println("key1真正計算了");
return "key1計算方式1";
});
System.out.println(cacheKey1);
cacheKey1 = cache.get("key1",()->{
System.out.println("key1真正計算了");
return "key1計算方式1";
});
System.out.println(cacheKey1);
Object cacheKey2 = cache.get("key2", () -> {
System.out.println("key1真正計算了");
return "key1計算方式2";
});
System.out.println(cacheKey2);
cacheKey2 = cache.get("key2",()->{
System.out.println("key1真正計算了");
return "key1計算方式2";
});
System.out.println(cacheKey2);
}
輸出:
key1真正計算了
key1計算方式1
key1計算方式1
key1真正計算了
key1計算方式2
key1計算方式2
從例子中可以看到骗污,在調(diào)用get的時候,可以傳入一個Callable來為此緩存key提供專門的緩存值計算方式沈条。
直接插入
這種方式計算緩存值的邏輯不再由Guava Cache管理需忿,而是調(diào)用方可以調(diào)用put(key,value) 直接將要緩存的值插入。
@Test
public void testDirectInsert() throws ExecutionException {
Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
cache.put("key1","cache-key1");
System.out.println(cache.get("key1",()->"callable cache-key1"));
}
輸出:
cache-key1
緩存淘汰機(jī)制
有一個殘酷的事實(shí)就是蜡歹,往往我們沒有那么大的內(nèi)存去支撐我們的緩存屋厘,所以我們必須有效的利用起來我們這昂貴的內(nèi)存,即針對那些不常用的緩存及時剔除月而,那么Guava Cache為我們提供了三種緩存剔除機(jī)制:基于大小剔除汗洒、基于緩存時間剔除、基于引用剔除父款。
基于大小剔除
這里并不是指占用緩存大小溢谤,而是指緩存條目的數(shù)量,當(dāng)緩存key的數(shù)量達(dá)到指定數(shù)量時憨攒,將按照LRU針對緩存key進(jìn)行剔除世杀。
@Test
public void testSizeBasedEviction(){
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(3)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key+"真正計算了");
return "cached-" + key;
}
});
System.out.println("第一次訪問");
loadingCache.getUnchecked("key1");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key3");
System.out.println("第二次訪問");
loadingCache.getUnchecked("key1");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key3");
System.out.println("開始剔除");
loadingCache.getUnchecked("key4");
System.out.println("第三次訪問");
loadingCache.getUnchecked("key3");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key1");
}
輸出:
第一次訪問
key1真正計算了
key2真正計算了
key3真正計算了
第二次訪問
開始剔除
key4真正計算了
第三次訪問
key1真正計算了
在上面這個例子中,設(shè)置了最大緩存條目為3肝集,然后依次添加了三個緩存項瞻坝,并且依次進(jìn)行了訪問,可以看到當(dāng)?shù)谝淮卧L問時杏瞻,由于緩存中都沒值所刀,因此進(jìn)行了計算,第二次訪問時捞挥,由于緩存中都有值所以直接從緩存讀取浮创,到了開始剔除階段時,此時嘗試獲取之前沒訪問過的key4砌函,而由于最大緩存條目為3蒸矛,所以此時需要從緩存中剔除掉一個值,那么剔除誰呢胸嘴?遵循LRU算法雏掠,key1是最近最不常不使用的,所以剔除的就是key1了劣像,從我們第三次訪問輸出的結(jié)果就可以驗證乡话。
注意:如果maximumSize傳入0,則所有key都將不進(jìn)行緩存耳奕!
除了maximumSize指定緩存key最大數(shù)量绑青,也可以通過maximumWeight指定最大權(quán)重诬像,就是說,每個緩存的key都需要返回一個權(quán)重闸婴,如果所有緩存的key的權(quán)重之和大于了我們指定的最大權(quán)重瓜浸,那么將執(zhí)行LRU淘汰策略:
@Test
public void testWeightBasedEviction(){
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumWeight(6).weigher((key,value)->{
if (key.equals("key1")){
return 1;
}
if (key.equals("key2")){
return 2;
}
if (key.equals("key3")){
return 3;
}
if (key.equals("key4")){
return 1;
}
return 0;
})
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key+"真正計算了");
return "cached-" + key;
}
});
System.out.println("第一次訪問");
loadingCache.getUnchecked("key1");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key3");
System.out.println("第二次訪問");
loadingCache.getUnchecked("key1");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key3");
System.out.println("開始剔除");
loadingCache.getUnchecked("key4");
loadingCache.getUnchecked("key3");
loadingCache.getUnchecked("key2");
loadingCache.getUnchecked("key1");
}
輸出:
第一次訪問
key1真正計算了
key2真正計算了
key3真正計算了
第二次訪問
開始剔除
key4真正計算了
key1真正計算了
這個就不多解釋了吧,自己根據(jù)輸出想想...
基于時間剔除
Guava Cache針對CacheBuilder提供了兩個方法:expireAfterAccess(long, TimeUnit) 和 expireAfterWrite(long, TimeUnit)
- expireAfterAccess
顧名思義固灵,當(dāng)某個緩存key自最后一次訪問(讀取或者寫入)超過指定時間后磷杏,那么這個緩存key將失效。
@Test
public void testExpiredAfterAccess() throws InterruptedException {
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterAccess(3,TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key+"真正計算了");
return "cached-" + key;
}
});
System.out.println("第一次訪問(寫入)");
loadingCache.getUnchecked("key1");
System.out.println("第二次訪問");
loadingCache.getUnchecked("key1");
TimeUnit.SECONDS.sleep(3);
System.out.println("過3秒后訪問");
loadingCache.getUnchecked("key1");
}
輸出:
第一次訪問(寫入)
key1真正計算了
第二次訪問
過3秒后訪問
key1真正計算了
這個例子中庇楞,我們設(shè)置了緩存自最近一次訪問(或?qū)懭耄┏^3秒后榜配,將失效,通過輸出也可以看到確實(shí) 如此吕晌。
- expireAfterWrite
顧名思義蛋褥,當(dāng)緩存key自最近一次寫入(注意,這就是和expireAfterAccess的區(qū)別睛驳,expireAfterWrite強(qiáng)調(diào)寫烙心,不關(guān)心讀)超過一定時間則過期剔除:
@Test
public void testExpiredAfterWrite() throws InterruptedException {
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterWrite(3,TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key+"真正計算了");
return "cached-" + key;
}
});
for (int i = 0; i < 4; i++) {
System.out.println(new Date());
loadingCache.getUnchecked("key1"); //首次執(zhí)行的時候,為寫入
TimeUnit.SECONDS.sleep(1);
}
}
輸出:
Sat Oct 02 20:06:47 CST 2021
key1真正計算了
Sat Oct 02 20:06:48 CST 2021
Sat Oct 02 20:06:49 CST 2021
Sat Oct 02 20:06:50 CST 2021
key1真正計算了
同樣乏沸,這里根據(jù)程序和輸出應(yīng)該可以理解啦弃理!
基于引用剔除
Java有四大引用,強(qiáng)屎蜓、軟痘昌、弱、虛炬转、如果對這幾個引用不是很了解的可以先去看看我這篇文章:??Java四種引用類型:強(qiáng)辆苔、軟、弱扼劈、虛
Guava Cache提供了基于引用的剔除策略驻啤,看到這里,你是否想起來了ThreadLocal如何防止內(nèi)存泄露呢荐吵?骑冗,如果不知道沒關(guān)系,繼續(xù)看我上面貼的引用文章先煎。Guava Cache提供了三種基于引用剔除的策略:
- CacheBuilder.weakKeys()
當(dāng)我們使用了weakKeys() 后贼涩,Guava cache將以弱引用 的方式去存儲緩存key,那么根據(jù)弱引用的定義:當(dāng)發(fā)生垃圾回收時,不管當(dāng)前系統(tǒng)資源是否充足薯蝎,弱引用都會被回收遥倦,直接上例子:
@Test
public void testWeakKeys() throws InterruptedException {
LoadingCache<MyKey, String> loadingCache = CacheBuilder.newBuilder().weakKeys()
.build(new CacheLoader<MyKey, String>() {
@Override
public String load(MyKey key) throws Exception {
System.out.println(key.getKey()+"真正計算了");
return "cached-" + key.getKey();
}
});
MyKey key = new MyKey("key1");
System.out.println("第一次訪問");
loadingCache.getUnchecked(key);
System.out.println(loadingCache.asMap());
System.out.println("第二次訪問");
loadingCache.getUnchecked(key);
System.out.println(loadingCache.asMap());
System.out.println("key失去強(qiáng)引用GC后訪問");
key = null;
System.gc();
TimeUnit.SECONDS.sleep(3);
System.out.println(loadingCache.asMap());
}
@Data
private static class MyKey{
String key;
public MyKey(String key) {
this.key = key;
}
}
CacheBuilder.weakValues()
有了CacheBuilder.weakKeys()的基礎(chǔ),CacheBuilder.weakValues()的作用想必照貓畫虎應(yīng)該也知道了吧占锯?換湯不換藥袒哥,這次針對的是緩存值缩筛!CacheBuilder.softValues()
有了CacheBuilder.weakValues()的基礎(chǔ),CacheBuilder.softValues()的作用相比照貓畫虎應(yīng)該也知道了吧堡称?對瞎抛,你真棒,就是之前的弱引用換為了軟引用却紧,軟引用相比弱引用桐臊,被回收的條件就苛刻點(diǎn):當(dāng)發(fā)生垃圾回收時,只有當(dāng)系統(tǒng)資源不足時啄寡,才會回收!哩照。
主動剔除
上面講了被動剔除策略挺物,那么除了被動,我們也可以主動調(diào)用方法去清除緩存飘弧。
- Cache.invalidate(key)
- Cache.invalidateAll(keys)
- Cache.invalidateAll()
緩存失效監(jiān)聽器
有時候我們希望當(dāng)緩存失效被剔除的時候识藤,可以做一些善后事情,此時次伶,我們就可以通過CacheBuilder.removalListener(RemovalListener) 來指定一個緩存失效監(jiān)聽器痴昧,當(dāng)緩存失效時,將回調(diào)我們的監(jiān)聽器:
@Test
public void testRemovalListener() throws InterruptedException {
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().removalListener(notification -> {
System.out.println(String
.format("緩存 %s 因為 %s 失效了冠王,它的value是%s", notification.getKey(), notification.getCause(),
notification.getValue()));
}).expireAfterAccess(3, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key + "真正計算了");
return "cached-" + key;
}
});
System.out.println("第一次訪問(寫入)");
loadingCache.getUnchecked("key1");
System.out.println("第二次訪問");
loadingCache.getUnchecked("key1");
TimeUnit.SECONDS.sleep(3);
System.out.println("3秒后");
loadingCache.getUnchecked("key1");
}
輸出:
第一次訪問(寫入)
key1真正計算了
第二次訪問
3秒后
緩存 key1 因為 EXPIRED 失效了赶撰,它的value是cached-key1
key1真正計算了
Guava Cache什么進(jìn)行清理動作?
這個其實(shí)在上節(jié)實(shí)驗緩存剔除監(jiān)聽器的時候我就發(fā)現(xiàn)一個問題:如果緩存失效后柱彻,我不再進(jìn)行任何操作豪娜,那么這個緩存監(jiān)聽器就得不到調(diào)用!哟楷,從這里就可以看出瘤载,Guava cache并不是自己主動去清理那些失效緩存的,而是當(dāng)我們對緩存進(jìn)行了操作時卖擅,才會進(jìn)行檢查清理以及其他動作鸣奔。那么為什么呢?想想啊惩阶,如果要主動清除挎狸,那肯定要有一個一直運(yùn)行的后臺線程去執(zhí)行清理,多了個線程出來断楷,那么意味著不再是單線程程序了伟叛,涉及多線程就要考慮加鎖資源保護(hù)了,這無疑會消耗我們資源脐嫂,影響性能统刮,而主動清除又不是必須的紊遵,等你操作了再清除,一點(diǎn)也不晚侥蒙!
當(dāng)然Guava cache也提供給我們主動清理的方法:Cache.cleanUp(),那么有了這個方法之后暗膜,是否主動清理的操作就交給了我們,由我們自己去權(quán)衡鞭衩。
緩存刷新
CacheBuilder中提供了refreshAfterWrite 用來指定緩存key寫入多久后重新進(jìn)行計算并緩存:
@Test
public void testRefresh() throws InterruptedException {
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().refreshAfterWrite(1,TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println(key + "真正計算了");
return "cached-" + key;
}
});
for (int i = 0; i < 3; i++) {
loadingCache.getUnchecked("key1");
TimeUnit.SECONDS.sleep(2);
}
}
輸出
key1真正計算了
key1真正計算了
key1真正計算了
在這個例子中学搜,我們指定緩存key寫入后,超過1秒就會刷新论衍,然后我們每隔2秒訪問一次緩存key,可以看到每次都得到了重新計算瑞佩!
小結(jié)
本文通過大量代碼案例詳細(xì)介紹了Guava Cache的使用,當(dāng)然你以為會止步于此嗎坯台?由于篇幅的原因炬丸,本文為使用篇,接下來將推出原理篇蜒蕾,我們的目的是從這些大佬的源碼設(shè)計中吸取精華稠炬,所謂知己知彼~