LoadingCache源碼剖析之緩存加載實(shí)現(xiàn)

隨著互聯(lián)網(wǎng)的發(fā)展,數(shù)據(jù)量不斷增長(zhǎng),用戶對(duì)性能要求的不斷提升,在開發(fā)項(xiàng)目中使用緩存已經(jīng)是必不可少的一種手段了。我們會(huì)將一些很少或者較少變化,對(duì)及時(shí)性要求不高的數(shù)據(jù)存放在緩存中,以減少數(shù)據(jù)庫(kù)的壓力和負(fù)載。常用的緩存分為堆內(nèi)緩存,堆外緩存,以及分布式緩存。而談到堆內(nèi)緩存,比較常用的就是Guava里提供的LoadingCache。

本文中會(huì)從源碼角度來(lái)分析LoadingCache中數(shù)據(jù)是如何被加載到緩存中,如何在多線程的場(chǎng)景下保證只有一個(gè)線程會(huì)去加載緩存數(shù)據(jù)。這一點(diǎn)在實(shí)際項(xiàng)目中是至關(guān)重要的,試想一下如果有100個(gè)線程同時(shí)到達(dá),并且同時(shí)去數(shù)據(jù)庫(kù)里讀取數(shù)據(jù)并加載到緩存,很有可能就會(huì)發(fā)生緩存擊穿,造成數(shù)據(jù)庫(kù)負(fù)載急劇上升,更有甚者可能會(huì)直接搞掛數(shù)據(jù)庫(kù)。

直接來(lái)一段Code Snippet

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
   .maximumSize(1000)
   .expireAfterWrite(10, TimeUnit.MINUTES)
   .removalListener(MY_LISTENER)
   .build(
       new CacheLoader<Key, Graph>() {
         public Graph load(Key key) throws AnyException {
           return createExpensiveGraph(key);
         }
       });

上面這段代碼是最常見的使用LoadingCache的方式

  • 創(chuàng)建了一個(gè)緩存實(shí)例
  • 指定最多可以存1000個(gè)緩存項(xiàng)
  • 在加載緩存后10分鐘過(guò)期
  • 注冊(cè)了一個(gè)自定義的CacheLoader來(lái)告訴LoadingCache如何在緩存失效后加載緩存。

但是這段代碼有一個(gè)非常致命的問(wèn)題就是當(dāng)緩存不存在或者過(guò)期的情況下,LoadingCache只會(huì)允許一個(gè)請(qǐng)求去加載緩存,其他并發(fā)請(qǐng)求會(huì)阻塞在那邊直至緩存加載完畢,那如果加載緩存過(guò)程比較慢的話,就會(huì)造成并發(fā)請(qǐng)求被阻塞,影響服務(wù)的整體性能,下面的這段代碼模擬了這種情況。

模擬并發(fā)請(qǐng)求的代碼

控制臺(tái)輸出

那LoadingCache究竟是如何實(shí)現(xiàn)上文所說(shuō)的這種機(jī)制的呢?讓我們一起順著cache.get("anyKey")語(yǔ)句來(lái)探索下源碼。


LocalCache.get

可以看到在這個(gè)方法里調(diào)用了localCache.getOrLoad(key)方法,localCache的類型是LocalCache,它繼承了AbstractMap并且實(shí)現(xiàn)了ConcurrentMap接口??吹竭@里大家可能會(huì)問(wèn)不是很多地方都說(shuō)實(shí)現(xiàn)一個(gè)緩存只要使用LinkedHashMap就可以了嗎?為什么還要重新去實(shí)現(xiàn)一遍呢?其實(shí)這里的關(guān)鍵在于LinkedHashMap并不是線程安全的,即使你在外部調(diào)用的地方去加上鎖,那鎖也是非常粗粒度的,然而LocalCache借鑒了ConcurrentHashMap的實(shí)現(xiàn)方式在內(nèi)部使用了分段鎖,大大提高了性能。這一塊源碼在這里就不展開了,如果大家有興趣的話可以自己去探究下。
在繼續(xù)往里跟,可以看到調(diào)用了一個(gè)重載的get方法,還傳了一個(gè)defaultLoader,這個(gè)defaultLoader就是我們?cè)诔跏蓟疞oadingCache時(shí)定義的那個(gè)CacheLoader,告訴LoadingCache如何去加載緩存。


LocalCache.get

這個(gè)重載的get方法里首先先計(jì)算出緩存key的hash code,然后找到hash code所在的Segment,再?gòu)腟egment里嘗試獲取緩存值。
LocalCache.get

繼續(xù)來(lái)看segmentFor(hash).get(key,hash,loader)中的get方法


Segment.get

現(xiàn)在來(lái)看一下上文提到的waitForLoadingValue方法,這個(gè)方法會(huì)調(diào)用Future的阻塞get方法去獲取緩存值,如果另外一個(gè)線程在加載緩存,當(dāng)前線程會(huì)阻塞直到緩存加載完畢,如下圖。
Uninterruptibles.getUninterruptibly

再看一下lockedGetOrLoad方法,這個(gè)方法中主要做的就是加鎖,然后看緩存項(xiàng)是否存在,或者是否已經(jīng)被其他線程加載,如果沒(méi)有的話,就嘗試用一開始用戶傳進(jìn)來(lái)的loader加載緩存。
LocalCache.lockedGetOrLoad

所以從LoadingCache的源碼中可以看到,LoadingCache是會(huì)保證同一時(shí)間只有一個(gè)線程去加載緩存,從而防止緩存擊穿的問(wèn)題發(fā)生,但是如果緩存加載需要比較長(zhǎng)的時(shí)間的話,會(huì)導(dǎo)致其他的線程都阻塞在那邊,影響系統(tǒng)的可用性,在下篇文章中會(huì)介紹如何利用LoadingCache的refresh機(jī)制來(lái)解決這個(gè)問(wèn)題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容