一、Guava Cache簡(jiǎn)介
1、簡(jiǎn)介
Guava Cache是本地緩存,數(shù)據(jù)讀寫都在一個(gè)進(jìn)程內(nèi),相對(duì)于分布式緩存redis,不需要網(wǎng)絡(luò)傳輸?shù)倪^(guò)程,訪問(wèn)速度很快,同時(shí)也受到 JVM 內(nèi)存的制約,無(wú)法在數(shù)據(jù)量較多的場(chǎng)景下使用。
基于以上特點(diǎn),本地緩存的主要應(yīng)用場(chǎng)景為以下幾種:
- 對(duì)于訪問(wèn)速度有較大要求
- 存儲(chǔ)的數(shù)據(jù)不經(jīng)常變化
- 數(shù)據(jù)量不大,占用內(nèi)存較小
- 需要訪問(wèn)整個(gè)集合
- 能夠容忍數(shù)據(jù)不是實(shí)時(shí)的
2、對(duì)比
二、Guava Cache使用
下面介紹如何使用
1、先引入jar
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0-jre</version>
</dependency>
案例1
1、創(chuàng)建Cache對(duì)象,在使用中,我們只需要操作loadingCache對(duì)象就可以了。
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
.initialCapacity(5)//內(nèi)部哈希表的最小容量,也就是cache的初始容量。
.concurrencyLevel(3)//并發(fā)等級(jí),也可以定義為同時(shí)操作緩存的線程數(shù),這個(gè)影響segment的數(shù)組長(zhǎng)度,原理是當(dāng)前數(shù)組長(zhǎng)度為1如果小于并發(fā)等級(jí)且素組長(zhǎng)度乘以20小于最大緩存數(shù)也就是10000,那么數(shù)組長(zhǎng)度就+1,依次循環(huán)
.maximumSize(10000)//cache的最大緩存數(shù)。應(yīng)該是數(shù)組長(zhǎng)度+鏈表上所有的元素的總數(shù)
.expireAfterWrite(20L, TimeUnit.SECONDS)//過(guò)期時(shí)間,過(guò)期就會(huì)觸發(fā)load方法
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
//緩存不存在,會(huì)進(jìn)到load方法,該方法返回值就是最終要緩存的數(shù)據(jù)。
log.info("進(jìn)入load緩存");
return "手機(jī)號(hào)";
}
});
2、通過(guò)緩存獲取數(shù)據(jù)
//獲取緩存,如果數(shù)據(jù)不存在,觸發(fā)load方法。
loadingCache.get(key);
案例2:使用reload功能
1、生成緩存對(duì)象
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
.initialCapacity(5)
.concurrencyLevel(3)
.maximumSize(10000)
.expireAfterWrite(20L, TimeUnit.SECONDS)//超這個(gè)時(shí)間,觸發(fā)的是load方法
.refreshAfterWrite(5L, TimeUnit.SECONDS) //刷新,超過(guò)觸發(fā)的是reload方法
//.expireAfterAccess(...): //當(dāng)緩存項(xiàng)在指定的時(shí)間段內(nèi)沒(méi)有被讀或?qū)懢蜁?huì)被回收。
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
//緩存不存在或者緩存超過(guò)expireAfterWrite設(shè)置的時(shí)間,進(jìn)到load方法
log.info("進(jìn)入load緩存");
return "手機(jī)號(hào)";
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
//超過(guò)refreshAfterWrite時(shí)間,但是沒(méi)有超過(guò)expireAfterWrite時(shí)間,進(jìn)到reload方法
log.info("進(jìn)入reload緩存");
//這里是異步執(zhí)行任務(wù)
return executorService.submit(() -> {
Thread.sleep(1000L);
return "relad手機(jī)號(hào)";
});
}
});
1、expireAfterWrite、refreshAfterWrite可以同時(shí)一起使用當(dāng)然,不同組合應(yīng)對(duì)不同場(chǎng)景。
2、需要說(shuō)明,當(dāng)緩存時(shí)間當(dāng)超過(guò)refreshAfterWrite時(shí)間,但是小于expireAfterWrite設(shè)置的時(shí)間,請(qǐng)求進(jìn)來(lái)執(zhí)行的是reload方法,當(dāng)時(shí)間超過(guò)expireAfterWrite時(shí)間,那么執(zhí)行的是load方法。
2、使用緩存對(duì)象
String value = loadingCache.get(key); //獲取緩存
loadingCache.invalidate(key); //刪除具體某個(gè)key的緩存
loadingCache.invalidateAll(Arrays.asList("key1","key2","key3"));//刪除多個(gè)
loadingCache.invalidateAll(); //刪除所有
三、源碼
緩存對(duì)象底層是LocalLoadingCache類,里面有個(gè)很重要的屬性segments,緩存數(shù)據(jù)都存在這個(gè)里面
//1、緩存對(duì)象
LocalLoadingCache{
//segments是一個(gè)數(shù)組,每一個(gè)元素都是Segment類型
final Segment<K, V>[] segments;
}
//2、下面介紹下Segment這個(gè)類有哪些重要的屬性
class Segment<K, V> extends ReentrantLock{ //繼承了重入鎖
//首先Segment里面有一個(gè)屬性table,這個(gè)table是AtomicReferenceArray類型
AtomicReferenceArray<ReferenceEntry<K, V>> table;
}
//3、下面看下AtomicReferenceArray到底有什么
AtomicReferenceArray{
//其實(shí)就是包裹了一個(gè)數(shù)組。每個(gè)元素都ReferenceEntry類型
private final Object[] array;
}
AtomicReferenceArray特別之處在于下
提供了可以原子讀取、寫入,底層引用數(shù)組的操作,并且還包含高級(jí)原子操作。比較特別的就是put操作,就是我們?cè)诮o該數(shù)組某個(gè)元素設(shè)置值的時(shí)候可以使用比較的方式來(lái)設(shè)置值。
例如:AtomicReferenceArray.compareAndSet(2,10,20)
2下標(biāo)位置,10是新的值,20是原來(lái)期望的值,只有原來(lái)的值為20才會(huì)更新為10。
在回到上面AtomicReferenceArray里面的屬性array里面每一個(gè)元素都是ReferenceEntry類型,ReferenceEntry的實(shí)現(xiàn)類是StrongAccessWriteEntry
StrongAccessWriteEntry{
final K key;
//value存到了ValueReference對(duì)象里面,只是ValueReference包裝了一下,這個(gè)在并發(fā)的時(shí)候會(huì)用到。
volatile ValueReference<K, V> value;
final int hash;
final ReferenceEntry<K, V> next; //針對(duì)hash碰撞時(shí)拓展的鏈表。
}
結(jié)構(gòu)圖如下:

四、下面介紹常用功能及其原理
1、獲取數(shù)據(jù)
1、通過(guò)key生成hash,根據(jù)hash從segments這個(gè)數(shù)組中得到具體下標(biāo),該元素是Segment類型
2、從Segment里面的table里面獲取,也就是AtomicReferenceArray的array從里面獲取數(shù)據(jù),此時(shí)拿到的是一個(gè)key所對(duì)應(yīng)的StrongAccessWriteEntry對(duì)象。
3、StrongAccessWriteEntry里面會(huì)存下該hash碰撞所對(duì)應(yīng)的其他key-value數(shù)據(jù)集合,StrongAccessWriteEntry對(duì)象保存了這個(gè)元素所對(duì)應(yīng)的hash,key,和value,next,還有過(guò)期時(shí)間,如果過(guò)期了也會(huì)返回return, 如果沒(méi)有過(guò)期,會(huì)進(jìn)行key對(duì)比,只有一致才會(huì)返回。
4、獲取的時(shí)候,如果數(shù)據(jù)不存在就會(huì)調(diào)用下面的put方法。獲取數(shù)據(jù)時(shí)是不使用lock()的。
2、put數(shù)據(jù)
在put前先lock(),為什么可以使用鎖,因?yàn)槔^承了ReentrantLock
Segment<K, V> extends ReentrantLock {
.....
}
首先是通過(guò)Load方法拿到數(shù)據(jù),拿到后再通過(guò)storeLoadedValue方法來(lái)把結(jié)果寫到緩存數(shù)據(jù)里面去,在寫入的時(shí)候,也是用到了鎖lock(); 所以這里就雙重鎖了。
先是通過(guò)hash結(jié)合算法,得到下標(biāo),在根據(jù)下標(biāo)從AtomicReferenceArray數(shù)組中獲取元素,那么這個(gè)元素ReferenceEntry是一個(gè)有next的鏈表,所以我們要遍歷,這個(gè)鏈表,如果有key一致的,我么就要把他覆蓋掉。如果沒(méi)有就使用set方法設(shè)置值。
3、刪除數(shù)據(jù)
刪除數(shù)據(jù)也是一樣,先lock();
拿到找到這個(gè)元素所在的位置,然后刪除掉
4、過(guò)期策略(重點(diǎn))
過(guò)期配置主要包含著三個(gè)expireAfterWrite、refreshAfterWrite、expireAfterAccess,下面分別介紹下這個(gè)三個(gè)作用
expireAfterWrite:當(dāng)緩存項(xiàng)在指定時(shí)間后就會(huì)被回收(主動(dòng)),需要等待獲取新值才會(huì)返回。
expireAfterAccess: 當(dāng)緩存項(xiàng)在指定的時(shí)間段內(nèi)沒(méi)有被讀或?qū)懢蜁?huì)被回收。
refreshAfterWrite:設(shè)置緩存刷新時(shí)間。舉個(gè)例子:第一次請(qǐng)求進(jìn)來(lái),執(zhí)行l(wèi)oad把數(shù)據(jù)加載到內(nèi)存中(同步過(guò)程),假設(shè)指定的刷新時(shí)間是10秒,前10秒內(nèi)都是從cache里讀取數(shù)據(jù)。過(guò)了10秒后,沒(méi)有請(qǐng)求進(jìn)來(lái),不會(huì)移除key。再有請(qǐng)求過(guò)來(lái),才則執(zhí)行reload(異步調(diào)用),在后臺(tái)異步刷新的過(guò)程中,所以本次訪問(wèn)到的是舊值。刷新過(guò)程中只有一個(gè)線程在執(zhí)行刷新操作,不會(huì)出現(xiàn)多個(gè)線程同時(shí)刷新同一個(gè)key的緩存。在吞吐量很低的情況下,如很長(zhǎng)一段時(shí)間內(nèi)沒(méi)有請(qǐng)求,再次請(qǐng)求有可能會(huì)得到一個(gè)舊值(這個(gè)舊值可能還是很久之前的數(shù)據(jù)),這就會(huì)有問(wèn)題。(可以使用expireAfterWrite和refreshAfterWrite搭配使用解決這個(gè)問(wèn)題)。在設(shè)置了expireAfterWrite后,如果超過(guò)expire時(shí)間,走的就是load方法,這是實(shí)時(shí)去獲取數(shù)據(jù)。
//是否更新過(guò)期時(shí)間判斷條件,只要?jiǎng)?chuàng)建緩存對(duì)象時(shí)設(shè)置了"刷新時(shí)間"、"過(guò)期時(shí)間"都會(huì)更新時(shí)間
void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {
drainRecencyQueue();
totalWeight += weight;
if (map.recordsAccess()) { //這個(gè)判斷條件是expireAfterAccess>0
entry.setAccessTime(now); //設(shè)置過(guò)期時(shí)間
}
if (map.recordsWrite()) { //這個(gè)判斷條件是expireAfterWrite>0
entry.setWriteTime(now); //設(shè)置過(guò)期時(shí)間
}
accessQueue.add(entry);
writeQueue.add(entry);
}
如果存在設(shè)置了expireAfterAccess時(shí)間每次讀的時(shí)候,都會(huì)更新過(guò)期時(shí)間
if (map.recordsAccess()) {
entry.setAccessTime(now);
}
那么在獲取數(shù)據(jù)的時(shí)候,是如何判斷是否執(zhí)行reload的,源碼里面有明確的判斷
V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {
if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)) {
V newValue = refresh(key, hash, loader, true);
}
}
//map.refreshes()主要是判斷 refreshNanos > 0
//也就是代碼中build的refreshAfterWrite(2L, TimeUnit.MINUTES)代碼。設(shè)置了refreshNanos的時(shí)間。
場(chǎng)景1: 當(dāng)前時(shí)間減去緩存到期時(shí)間結(jié)果大于過(guò)期時(shí)間,才會(huì)執(zhí)行refresh方法,就會(huì)從reload里面獲取數(shù)據(jù)。
場(chǎng)景2:當(dāng)然還有一種情況就是既設(shè)置了refreshAfterWrite,又設(shè)置了expireAfterWrite,這個(gè)情況是,優(yōu)先判斷,數(shù)據(jù)是否過(guò)期了,如果并且過(guò)期時(shí)間超了,那么就執(zhí)行l(wèi)oad方法,如果沒(méi)有超過(guò)過(guò)期時(shí)間,超過(guò)了refresh的過(guò)期時(shí)間,那么就執(zhí)行reload方法,代碼如下
//判斷是否過(guò)期
if (map.isExpired(entry, now)) {
return null;
}
//看下isExpired的邏輯
boolean isExpired(ReferenceEntry<K, V> entry, long now) {
//這里忽略
if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
return true;
}
//判斷是否設(shè)置了expireAfterWriteNanos時(shí)間,且當(dāng)前時(shí)間減去過(guò)期時(shí)間是否超過(guò)expireAfterWriteNanos,超過(guò)則說(shuō)明數(shù)據(jù)已經(jīng)過(guò)期了。
if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
return true;
}
return false;
}
2、解決緩存擊穿
緩存擊穿:假如在緩存過(guò)期的那一瞬間,有大量的并發(fā)請(qǐng)求過(guò)來(lái)。他們都會(huì)因緩存失效而去加載執(zhí)行db操作,可能會(huì)給db造成毀滅性打擊。
解決方案: 采用expireAfterWrite+refreshAfterWrite 組合設(shè)置來(lái)防止緩存擊穿,expire則通過(guò)一個(gè)加鎖的方式,只允許一個(gè)線程去回源,有效防止了緩存擊穿,但是可以從源代碼看出,在有效防止緩存擊穿的同時(shí),會(huì)發(fā)現(xiàn)多線程的請(qǐng)求同樣key的情況下,一部分線程在waitforvalue,而另一部分線程在reentantloack的阻塞中。
//當(dāng)數(shù)據(jù)過(guò)期了,先拿到數(shù)據(jù)的狀態(tài),如果是正在執(zhí)行l(wèi)oad方法,則其他線程就先等待,
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
//關(guān)鍵在于valueReference對(duì)象,他有2種兩類,不同類型代表不同狀態(tài)。
1、一開始創(chuàng)建的時(shí)候valueReference是LoadingValueReference類型對(duì)象。這個(gè)在剛創(chuàng)建entity的時(shí)候會(huì)用到。也就是load方法被執(zhí)行前LoadingValueReference固定是返回true
2、當(dāng)load方法被加載完valueReference類型就變成StrongValueReference。load執(zhí)行完后,更新entity的類型。StrongValueReference的isLoading方法固定是false
3、當(dāng)數(shù)據(jù)過(guò)期時(shí)
3、刷新時(shí)拿到的不一定是最新數(shù)據(jù)
因?yàn)槿绻驗(yàn)檫^(guò)期執(zhí)行刷新的方法也就是reload方法,那么從緩存里面拿到的數(shù)據(jù)不一定是新數(shù)據(jù),可能是老數(shù)據(jù),為什么,因?yàn)樗⑿聲r(shí)異步觸發(fā)reload,不像load同步這種,源碼如果reload的返回null,那么會(huì)優(yōu)先使用oldValue數(shù)據(jù)。
V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {
if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)) {
V newValue = refresh(key, hash, loader, true); //執(zhí)行刷新的方法,也就reload方法,下面看下refresh做了什么操作
if (newValue != null) {
return newValue;
}
}
return oldValue;
}
refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
//通過(guò)異步去執(zhí)行reload方法,注意是異步,此時(shí)沒(méi)有完成,那么直接返回null,那么上面的scheduleRefresh方法直接返回的是oldValue,也就是老數(shù)據(jù)。
ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
if (result.isDone()) {
try {
return Uninterruptibles.getUninterruptibly(result);
} catch (Throwable t) {
}
}
return null;
}
所以緩存失效第一次數(shù)據(jù)不一定是最新的數(shù)據(jù)??赡苁抢系臄?shù)據(jù),因?yàn)槭钱惒綀?zhí)行reload方法不知道耗時(shí)會(huì)有多久,所以主線程不會(huì)一直去等子線程完成。關(guān)注下,主線程在子線程執(zhí)行reload會(huì)等多久?
4、總結(jié)
1、refreshAfterWrites是異步去刷新緩存的方法,可能會(huì)使用過(guò)期的舊值快速響應(yīng)。
2、expireAfterWrites緩存失效后線程需要同步等待加載結(jié)果,可能會(huì)造成請(qǐng)求大量堆積的問(wèn)題。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-731863.html
四、注意點(diǎn)
在重寫load的時(shí)候,如果數(shù)據(jù)是空要寫成"",不能是null,因?yàn)樵趐ut的時(shí)候,會(huì)判斷返回的值如果是null就會(huì)拋出下面異常文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-731863.html
@Override
public String load(String key) {
return ...
}
//當(dāng)load返回為空時(shí)會(huì)拋出異常
if (value == null) {
throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}
到了這里,關(guān)于Guava Cache介紹-面試用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!