架構(gòu)之高并發(fā):緩存
高并發(fā)實現(xiàn)的三板斧:緩存,限流和降級。緩存在高并發(fā)系統(tǒng)中有者極其廣闊的應(yīng)用,需要重點(diǎn)掌握,本文重點(diǎn)介紹下緩存及其實現(xiàn)。
#緩存簡介
隨著互聯(lián)網(wǎng)的普及,內(nèi)容信息越來越復(fù)雜,用戶數(shù)和訪問量越來越大,我們的應(yīng)用需要支撐更多的并發(fā)量,同時我們的應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器所做的計算也越來越多。但是往往我們的應(yīng)用服務(wù)器資源是有限的,且技術(shù)變革是緩慢的,數(shù)據(jù)庫每秒能接受的請求次數(shù)也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供盡可能大的吞吐量? 一個有效的辦法就是引入緩存,打破標(biāo)準(zhǔn)流程,每個環(huán)節(jié)中請求可以從緩存中直接獲取目標(biāo)數(shù)據(jù)并返回,從而減少計算量,有效提升響應(yīng)速度,讓有限的資源服務(wù)更多的用戶。
如圖1所示,緩存的使用可以出現(xiàn)在1~4的各個環(huán)節(jié)中,每個環(huán)節(jié)的緩存方案與使用各有特點(diǎn)。

圖1 互聯(lián)網(wǎng)應(yīng)用一般流程
#關(guān)鍵詞-命中率
命中率 = 命中數(shù) / (命中數(shù) + 沒有命中數(shù))
影響緩存命中率的因素:
1.業(yè)務(wù)場景和業(yè)務(wù)需求
緩存通常適合讀多寫少的業(yè)務(wù)場景,反之的使用意義并不多,命中率會很低。業(yè)務(wù)需求也決定了實時性的要求,直接影響到過期時間和更新策略,實時性要求越低越適合緩存。
2.緩存的設(shè)計(策略和粒度)
通常情況下緩存的粒度越小,命中率越高。比如說緩存一個用戶信息的對象,只有當(dāng)這個用戶的信息發(fā)生變化的時候才更新緩存,而如果是緩存一個集合的話,集合中任何一個對象發(fā)生變化都要重新更新緩存。
當(dāng)數(shù)據(jù)發(fā)生變化時,直接更新緩存的值比移除緩存或者讓緩存過期它的命中率更高,不過這個時候系統(tǒng)的復(fù)雜度過高。
3.緩存的容量和基礎(chǔ)設(shè)施
緩存的容量有限就會容易引起緩存的失效和被淘汰。目前多數(shù)的緩存框架和中間件都采用LRU這個算法。同時采用緩存的技術(shù)選型也是至關(guān)重要的,比如采用本地內(nèi)置的應(yīng)用緩存,就比較容易出現(xiàn)單機(jī)瓶頸。而采用分布式緩存就更加容易擴(kuò)展。所以需要做好系統(tǒng)容量規(guī)劃,系統(tǒng)是否可擴(kuò)展。
最大空間
緩存最大空間一旦緩存中元素數(shù)量超過這個值(或者緩存數(shù)據(jù)所占空間超過其最大支持空間),那么將會觸發(fā)緩存啟動清空策略根據(jù)不同的場景合理的設(shè)置最大元素值往往可以一定程度上提高緩存的命中率,從而更有效的利用緩存。
#緩存介質(zhì)
雖然從硬件介質(zhì)上來看,無非就是內(nèi)存和硬盤兩種,但從技術(shù)上,可以分成內(nèi)存、硬盤文件、數(shù)據(jù)庫。
內(nèi)存:將緩存存儲于內(nèi)存中是最快的選擇,無需額外的I/O開銷,但是內(nèi)存的缺點(diǎn)是沒有持久化落地物理磁盤,一旦應(yīng)用異常break down而重新啟動,數(shù)據(jù)很難或者無法復(fù)原。
硬盤:一般來說,很多緩存框架會結(jié)合使用內(nèi)存和硬盤,在內(nèi)存分配空間滿了或是在異常的情況下,可以被動或主動的將內(nèi)存空間數(shù)據(jù)持久化到硬盤中,達(dá)到釋放空間或備份數(shù)據(jù)的目的。
數(shù)據(jù)庫:前面有提到,增加緩存的策略的目的之一就是為了減少數(shù)據(jù)庫的I/O壓力。現(xiàn)在使用數(shù)據(jù)庫做緩存介質(zhì)是不是又回到了老問題上了? 其實,數(shù)據(jù)庫也有很多種類型,像那些不支持SQL,只是簡單的key-value存儲結(jié)構(gòu)的特殊數(shù)據(jù)庫(如BerkeleyDB和Redis),響應(yīng)速度和吞吐量都遠(yuǎn)遠(yuǎn)高于我們常用的關(guān)系型數(shù)據(jù)庫等。
#緩存淘汰算法
FIFO/LFU/LRU/過期時間/隨機(jī)
FIFO:最先進(jìn)入緩存的數(shù)據(jù),在緩存空間不足時被清除,為了保證最新數(shù)據(jù)可用,保證實時性
LFU(Least Frequently Used):最近最不常用,基于訪問次數(shù),去除命中次數(shù)最少的元素,保證高頻數(shù)據(jù)有效性
LRU(Least Recently Used):最近最少使用,基于訪問時間,在被訪問過的元素中去除最久未使用的元素,保證熱點(diǎn)數(shù)據(jù)的有效性
#哪里用了緩存
一切地方。例如:
我們從硬盤讀數(shù)據(jù)的時候,其實操作系統(tǒng)還額外把附近的數(shù)據(jù)都讀到了內(nèi)存里
例如,CPU在從內(nèi)存里讀數(shù)據(jù)的時候,也額外讀了許多數(shù)據(jù)到各級cache里
各個輸入輸出之間用buffer保存一批數(shù)據(jù)統(tǒng)一發(fā)送和接受,而不是一個byte一個byte的處理
上面這是系統(tǒng)層面,在軟件系統(tǒng)設(shè)計層面,很多地方也用了緩存:
瀏覽器會緩存頁面的元素,這樣在重復(fù)訪問網(wǎng)頁時,就避開了要從互聯(lián)網(wǎng)上下載數(shù)據(jù)(例如大圖片)
web服務(wù)會把靜態(tài)的東西提前部署在CDN上,這也是一種緩存
數(shù)據(jù)庫會緩存查詢,所以同一條查詢第二次就是要比第一次快
內(nèi)存數(shù)據(jù)庫(如redis)選擇把大量數(shù)據(jù)存在內(nèi)存而非硬盤里,這可以看作是一個大型緩存,只是把整個數(shù)據(jù)庫緩存了起來
應(yīng)用程序把最近幾次計算的結(jié)果放在本地內(nèi)存里,如果下次到來的請求還是原請求,就跳過計算直接返回結(jié)果 ...
#緩存應(yīng)用和實現(xiàn)
緩存有各類特征,而且有不同介質(zhì)的區(qū)別,那么實際工程中我們怎么去對緩存分類呢? 在目前的應(yīng)用服務(wù)框架中,比較常見的是根據(jù)緩存與應(yīng)用的藕合度,分為local cache(本地緩存)和remote cache(分布式緩存):
本地緩存:指的是在應(yīng)用中的緩存組件,其最大的優(yōu)點(diǎn)是應(yīng)用和cache是在同一個進(jìn)程內(nèi)部,請求緩存非常快速,沒有過多的網(wǎng)絡(luò)開銷等,在單應(yīng)用不需要集群支持或者集群情況下各節(jié)點(diǎn)無需互相通知的場景下使用本地緩存較合適;同時,它的缺點(diǎn)也是應(yīng)為緩存跟應(yīng)用程序耦合,多個應(yīng)用程序無法直接的共享緩存,各應(yīng)用或集群的各節(jié)點(diǎn)都需要維護(hù)自己的單獨(dú)緩存,對內(nèi)存是一種浪費(fèi)。
分布式緩存:指的是與應(yīng)用分離的緩存組件或服務(wù),其最大的優(yōu)點(diǎn)是自身就是一個獨(dú)立的應(yīng)用,與本地應(yīng)用隔離,多個應(yīng)用可直接的共享緩存。
目前各種類型的緩存都活躍在成千上萬的應(yīng)用服務(wù)中,還沒有一種緩存方案可以解決一切的業(yè)務(wù)場景或數(shù)據(jù)類型,我們需要根據(jù)自身的特殊場景和背景,選擇最適合的緩存方案。緩存的使用是程序員、架構(gòu)師的必備技能,好的程序員能根據(jù)數(shù)據(jù)類型、業(yè)務(wù)場景來準(zhǔn)確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達(dá)到最優(yōu)的目的。
#緩存實現(xiàn)-本地緩存
編程直接實現(xiàn)緩存 個別場景下,我們只需要簡單的緩存數(shù)據(jù)的功能,而無需關(guān)注更多存取、清空策略等深入的特性時,直接編程實現(xiàn)緩存則是最便捷和高效的。
#成員變量或局部變量實現(xiàn)
簡單代碼示例如下:
publicvoidUseLocalCache(){//一個本地的緩存變量Map<String,Object>localCacheStoreMap=newHashMap<String,Object>();List<Object>infosList=this.getInfoList();for(Objectitem:infosList){if(localCacheStoreMap.containsKey(item)){//緩存命中 使用緩存數(shù)據(jù)// todo}else{// 緩存未命中 IO獲取數(shù)據(jù),結(jié)果存入緩存ObjectvalueObject=this.getInfoFromDB();localCacheStoreMap.put(valueObject.toString(),valueObject);}}}//示例privateList<Object>getInfoList(){returnnewArrayList<Object>();}//示例數(shù)據(jù)庫IO獲取privateObjectgetInfoFromDB(){returnnewObject();}
著作權(quán)歸@pdai所有
原文鏈接:https://pdai.tech/md/arch/arch-y-cache.html
以局部變量map結(jié)構(gòu)緩存部分業(yè)務(wù)數(shù)據(jù),減少頻繁的重復(fù)數(shù)據(jù)庫I/O操作。缺點(diǎn)僅限于類的自身作用域內(nèi),類間無法共享緩存。
#靜態(tài)變量實現(xiàn)
最常用的單例實現(xiàn)靜態(tài)資源緩存,代碼示例如下:
publicclassCityUtils{privatestaticfinalHttpClienthttpClient=ServerHolder.createClientWithPool();privatestaticMap<Integer,String>cityIdNameMap=newHashMap<Integer,String>();privatestaticMap<Integer,String>districtIdNameMap=newHashMap<Integer,String>();static{HttpGetget=newHttpGet("http://gis-in.sankuai.com/api/location/city/all");BaseAuthorizationUtils.generateAuthAndDateHeader(get,BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);try{StringresultStr=httpClient.execute(get,newBasicResponseHandler());JSONObjectresultJo=newJSONObject(resultStr);JSONArraydataJa=resultJo.getJSONArray("data");for(inti=0;i<dataJa.length();i++){JSONObjectitemJo=dataJa.getJSONObject(i);cityIdNameMap.put(itemJo.getInt("id"),itemJo.getString("name"));}}catch(Exceptione){thrownewRuntimeException("Init City List Error!",e);}}static{HttpGetget=newHttpGet("http://gis-in.sankuai.com/api/location/district/all");BaseAuthorizationUtils.generateAuthAndDateHeader(get,BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);try{StringresultStr=httpClient.execute(get,newBasicResponseHandler());JSONObjectresultJo=newJSONObject(resultStr);JSONArraydataJa=resultJo.getJSONArray("data");for(inti=0;i<dataJa.length();i++){JSONObjectitemJo=dataJa.getJSONObject(i);districtIdNameMap.put(itemJo.getInt("id"),itemJo.getString("name"));}}catch(Exceptione){thrownewRuntimeException("Init District List Error!",e);}}publicstaticStringgetCityName(intcityId){Stringname=cityIdNameMap.get(cityId);if(name==null){name="未知";}returnname;}publicstaticStringgetDistrictName(intdistrictId){Stringname=districtIdNameMap.get(districtId);if(name==null){name="未知";}returnname;}}}
O2O業(yè)務(wù)中常用的城市基礎(chǔ)基本信息判斷,通過靜態(tài)變量一次獲取緩存內(nèi)存中,減少頻繁的I/O讀取,靜態(tài)變量實現(xiàn)類間可共享,進(jìn)程內(nèi)可共享,緩存的實時性稍差。
為了解決本地緩存數(shù)據(jù)的實時性問題,目前大量使用的是結(jié)合ZooKeeper的自動發(fā)現(xiàn)機(jī)制,實時變更本地靜態(tài)變量緩存:
美團(tuán)內(nèi)部的基礎(chǔ)配置組件MtConfig,采用的就是類似原理,使用靜態(tài)變量緩存,結(jié)合ZooKeeper的統(tǒng)一管理,做到自動動態(tài)更新緩存,如圖2所示。
圖2 Mtconfig實現(xiàn)圖
這類緩存實現(xiàn),優(yōu)點(diǎn)是能直接在heap區(qū)內(nèi)讀寫,最快也最方便;缺點(diǎn)同樣是受heap區(qū)域影響,緩存的數(shù)據(jù)量非常有限,同時緩存時間受GC影響。主要滿足單機(jī)場景下的小數(shù)據(jù)量緩存需求,同時對緩存數(shù)據(jù)的變更無需太敏感感知,如上一般配置管理、基礎(chǔ)靜態(tài)數(shù)據(jù)等場景。
#Ehcache
Ehcache是現(xiàn)在最流行的純Java開源緩存框架,配置簡單、結(jié)構(gòu)清晰、功能強(qiáng)大,是一個非常輕量級的緩存實現(xiàn),我們常用的Hibernate里面就集成了相關(guān)緩存功能。

圖3 Ehcache框架圖
從圖3中我們可以了解到,Ehcache的核心定義主要包括:
cache manager:緩存管理器,以前是只允許單例的,不過現(xiàn)在也可以多實例了。
cache:緩存管理器內(nèi)可以放置若干cache,存放數(shù)據(jù)的實質(zhì),所有cache都實現(xiàn)了Ehcache接口,這是一個真正使用的緩存實例;通過緩存管理器的模式,可以在單個應(yīng)用中輕松隔離多個緩存實例,獨(dú)立服務(wù)于不同業(yè)務(wù)場景需求,緩存數(shù)據(jù)物理隔離,同時需要時又可共享使用。
element:單條緩存數(shù)據(jù)的組成單位。
system of record(SOR):可以取到真實數(shù)據(jù)的組件,可以是真正的業(yè)務(wù)邏輯、外部接口調(diào)用、存放真實數(shù)據(jù)的數(shù)據(jù)庫等,緩存就是從SOR中讀取或者寫入到SOR中去的。
在上層可以看到,整個Ehcache提供了對JSR、JMX等的標(biāo)準(zhǔn)支持,能夠較好的兼容和移植,同時對各類對象有較完善的監(jiān)控管理機(jī)制。它的緩存介質(zhì)涵蓋堆內(nèi)存(heap)、堆外內(nèi)存(BigMemory商用版本支持)和磁盤,各介質(zhì)可獨(dú)立設(shè)置屬性和策略。Ehcache最初是獨(dú)立的本地緩存框架組件,在后期的發(fā)展中,結(jié)合Terracotta服務(wù)陣列模型,可以支持分布式緩存集群,主要有RMI、JGroups、JMS和Cache Server等傳播方式進(jìn)行節(jié)點(diǎn)間通信,如圖3的左側(cè)部分描述。
整體數(shù)據(jù)流轉(zhuǎn)包括這樣幾類行為:
Flush:緩存條目向低層次移動。
Fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發(fā)現(xiàn)自己的該緩存條目已經(jīng)失效,就觸發(fā)了Fault行為。
Eviction:把緩存條目除去。
Expiration:失效狀態(tài)。
Pinning:強(qiáng)制緩存條目保持在某一層。
圖4反映了數(shù)據(jù)在各個層之間的流轉(zhuǎn),同時也體現(xiàn)了各層數(shù)據(jù)的一個生命周期。

圖4 緩存數(shù)據(jù)流轉(zhuǎn)圖(L1:本地內(nèi)存層;L2:Terracotta服務(wù)節(jié)點(diǎn)層)
Ehcache的配置使用如下:
<ehcache><!-- 指定一個文件目錄,當(dāng)Ehcache把數(shù)據(jù)寫到硬盤上時,將把數(shù)據(jù)寫到這個文件目錄下 --><diskStorepath="java.io.tmpdir"/><!-- 設(shè)定緩存的默認(rèn)數(shù)據(jù)過期策略 --><defaultCachemaxElementsInMemory="10000"eternal="false"overflowToDisk="true"timeToIdleSeconds="0"timeToLiveSeconds="0"diskPersistent="false"diskExpiryThreadIntervalSeconds="120"/><!--?
? ? 設(shè)定具體的命名緩存的數(shù)據(jù)過期策略
? ? cache元素的屬性:
? ? ? ? name:緩存名稱
? ? ? ? maxElementsInMemory:內(nèi)存中最大緩存對象數(shù)
? ? ? ? maxElementsOnDisk:硬盤中最大緩存對象數(shù),若是0表示無窮大
? ? ? ? eternal:true表示對象永不過期,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認(rèn)為false
? ? ? ? overflowToDisk:true表示當(dāng)內(nèi)存緩存的對象數(shù)目達(dá)到了maxElementsInMemory界限后,會把溢出的對象寫到硬盤緩存中。注意:如果緩存的對象要寫入到硬盤中的話,則該對象必須實現(xiàn)了Serializable接口才行。
? ? ? ? diskSpoolBufferSizeMB:磁盤緩存區(qū)大小,默認(rèn)為30MB。每個Cache都應(yīng)該有自己的一個緩存區(qū)。
? ? ? ? diskPersistent:是否緩存虛擬機(jī)重啟期數(shù)據(jù)
? ? ? ? diskExpiryThreadIntervalSeconds:磁盤失效線程運(yùn)行時間間隔,默認(rèn)為120秒
? ? ? ? timeToIdleSeconds: 設(shè)定允許對象處于空閑狀態(tài)的最長時間,以秒為單位。當(dāng)對象自從最近一次被訪問后,如果處于空閑狀態(tài)的時間超過了timeToIdleSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清空。只有當(dāng)eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地處于空閑狀態(tài)
? ? ? ? timeToLiveSeconds:設(shè)定對象允許存在于緩存中的最長時間,以秒為單位。當(dāng)對象自從被存放到緩存中后,如果處于緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過期,Ehcache將把它從緩存中清除。只有當(dāng)eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地存在于緩存中。timeToLiveSeconds必須大于timeToIdleSeconds屬性,才有意義
? ? ? ? memoryStoreEvictionPolicy:當(dāng)達(dá)到maxElementsInMemory限制時,Ehcache將會根據(jù)指定的策略去清理內(nèi)存??蛇x策略有:LRU(最近最少使用,默認(rèn)策略)、FIFO(先進(jìn)先出)、LFU(最少訪問次數(shù))。
--><cachename="CACHE1"maxElementsInMemory="1000"eternal="true"overflowToDisk="true"/><cachename="CACHE2"maxElementsInMemory="1000"eternal="false"timeToIdleSeconds="200"timeToLiveSeconds="4000"overflowToDisk="true"/></ehcache>
整體上看,Ehcache的使用還是相對簡單便捷的,提供了完整的各類API接口。需要注意的是,雖然Ehcache支持磁盤的持久化,但是由于存在兩級緩存介質(zhì),在一級內(nèi)存中的緩存,如果沒有主動的刷入磁盤持久化的話,在應(yīng)用異常down機(jī)等情形下,依然會出現(xiàn)緩存數(shù)據(jù)丟失,為此可以根據(jù)需要將緩存刷到磁盤,將緩存條目刷到磁盤的操作可以通過cache.flush()方法來執(zhí)行,需要注意的是,對于對象的磁盤寫入,前提是要將對象進(jìn)行序列化。
主要特性:
快速,針對大型高并發(fā)系統(tǒng)場景,Ehcache的多線程機(jī)制有相應(yīng)的優(yōu)化改善。
簡單,很小的jar包,簡單配置就可直接使用,單機(jī)場景下無需過多的其他服務(wù)依賴。
支持多種的緩存策略,靈活。
緩存數(shù)據(jù)有兩級:內(nèi)存和磁盤,與一般的本地內(nèi)存緩存相比,有了磁盤的存儲空間,將可以支持更大量的數(shù)據(jù)緩存需求。
具有緩存和緩存管理器的偵聽接口,能更簡單方便的進(jìn)行緩存實例的監(jiān)控管理。
支持多緩存管理器實例,以及一個實例的多個緩存區(qū)域。
注意:Ehcache的超時設(shè)置主要是針對整個cache實例設(shè)置整體的超時策略,而沒有較好的處理針對單獨(dú)的key的個性的超時設(shè)置(有策略設(shè)置,但是比較復(fù)雜,就不描述了),因此,在使用中要注意過期失效的緩存元素?zé)o法被GC回收,時間越長緩存越多,內(nèi)存占用也就越大,內(nèi)存泄露的概率也越大。
#Guava Cache
Guava Cache是Google開源的Java重用工具集庫Guava里的一款緩存工具,其主要實現(xiàn)的緩存功能有:
自動將entry節(jié)點(diǎn)加載進(jìn)緩存結(jié)構(gòu)中;
當(dāng)緩存的數(shù)據(jù)超過設(shè)置的最大值時,使用LRU算法移除;
具備根據(jù)entry節(jié)點(diǎn)上次被訪問或者寫入時間計算它的過期機(jī)制;
緩存的key被封裝在WeakReference引用內(nèi);
緩存的Value被封裝在WeakReference或SoftReference引用內(nèi);
統(tǒng)計緩存使用過程中命中率、異常率、未命中率等統(tǒng)計數(shù)據(jù)。
Guava Cache的架構(gòu)設(shè)計靈感來源于ConcurrentHashMap,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量數(shù)據(jù)的緩存,但是,如果結(jié)果可能隨時間改變或者是希望存儲的數(shù)據(jù)空間可控的話,自己實現(xiàn)這種數(shù)據(jù)結(jié)構(gòu)還是有必要的。
Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細(xì)粒度鎖,在保證線程安全的同時,支持高并發(fā)場景需求。Cache類似于Map,它是存儲鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等算法邏輯,需要一些額外信息來實現(xiàn)這些操作。對此,根據(jù)面向?qū)ο笏枷?,需要做方法與數(shù)據(jù)的關(guān)聯(lián)封裝。如圖5所示cache的內(nèi)存數(shù)據(jù)模型,可以看到,使用ReferenceEntry接口來封裝一個鍵值對,而用ValueReference來封裝Value值,之所以用Reference命令,是因為Cache要支持WeakReference Key和SoftReference、WeakReference value。
圖5 Guava Cache數(shù)據(jù)結(jié)構(gòu)圖
ReferenceEntry是對一個鍵值對節(jié)點(diǎn)的抽象,它包含了key和值的ValueReference抽象類,Cache由多個Segment組成,而每個Segment包含一個ReferenceEntry數(shù)組,每個ReferenceEntry數(shù)組項都是一條ReferenceEntry鏈,且一個ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry數(shù)組項中組成的鏈,在一個Segment中,所有ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue)(后面會介紹鏈的作用)。ReferenceEntry可以是強(qiáng)引用類型的key,也可以WeakReference類型的key,為了減少內(nèi)存使用量,還可以根據(jù)是否配置了expireAfterWrite、expireAfterAccess、maximumSize來決定是否需要write鏈和access鏈確定要創(chuàng)建的具體Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。
對于ValueReference,因為Cache支持強(qiáng)引用的Value、SoftReference Value以及WeakReference Value,因而它對應(yīng)三個實現(xiàn)類:StrongValueReference、SoftValueReference、WeakValueReference。為了支持動態(tài)加載機(jī)制,它還有一個LoadingValueReference,在需要動態(tài)加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達(dá)該key對應(yīng)的值已經(jīng)在加載了,如果其他線程也要查詢該key對應(yīng)的值,就能得到該引用,并且等待改值加載完成,從而保證該值只被加載一次,在該值加載完成后,將LoadingValueReference替換成其他ValueReference類型。ValueReference對象中會保留對ReferenceEntry的引用,這是因為在Value因為WeakReference、SoftReference被回收時,需要使用其key將對應(yīng)的項從Segment的table中移除。
WriteQueue和AccessQueue:為了實現(xiàn)最近最少使用算法,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個雙向鏈表,通過ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue鏈接而成,但是以Queue的形式表達(dá)。WriteQueue和AccessQueue都是自定義了offer、add(直接調(diào)用offer)、remove、poll等操作的邏輯,對offer(add)操作,如果是新加的節(jié)點(diǎn),則直接加入到該鏈的結(jié)尾,如果是已存在的節(jié)點(diǎn),則將該節(jié)點(diǎn)鏈接的鏈尾;對remove操作,直接從該鏈中移除該節(jié)點(diǎn);對poll操作,將頭節(jié)點(diǎn)的下一個節(jié)點(diǎn)移除,并返回。
了解了cache的整體數(shù)據(jù)結(jié)構(gòu)后,再來看下針對緩存的相關(guān)操作就簡單多了:
Segment中的evict清除策略操作,是在每一次調(diào)用操作的開始和結(jié)束時觸發(fā)清理工作,這樣比一般的緩存另起線程監(jiān)控清理相比,可以減少開銷,但如果長時間沒有調(diào)用方法的話,會導(dǎo)致不能及時的清理釋放內(nèi)存空間的問題。evict主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是因為WeakReference、SoftReference被垃圾回收時加入的,清理時只需要遍歷整個queue,將對應(yīng)的項從LocalCache中移除即可,這里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要從Cache中移除需要有key,因而ValueReference需要有對ReferenceEntry的引用,這個前面也提到過了。而對后面兩個Queue,只需要檢查是否配置了相應(yīng)的expire時間,然后從頭開始查找已經(jīng)expire的Entry,將它們移除即可。
Segment中的put操作:put操作相對比較簡單,首先它需要獲得鎖,然后嘗試做一些清理工作,接下來的邏輯類似ConcurrentHashMap中的rehash,查找位置并注入數(shù)據(jù)。需要說明的是當(dāng)找到一個已存在的Entry時,需要先判斷當(dāng)前的ValueRefernece中的值事實上已經(jīng)被回收了,因為它們可以是WeakReference、SoftReference類型,如果已經(jīng)被回收了,則將新值寫入。并且在每次更新時注冊當(dāng)前操作引起的移除事件,指定相應(yīng)的原因:COLLECTED、REPLACED等,這些注冊的事件在退出的時候統(tǒng)一調(diào)用Cache注冊的RemovalListener,由于事件處理可能會有很長時間,因而這里將事件處理的邏輯在退出鎖以后才做。最后,在更新已存在的Entry結(jié)束后都嘗試著將那些已經(jīng)expire的Entry移除。另外put操作中還需要更新writeQueue和accessQueue的語義正確性。
Segment帶CacheLoader的get操作:1. 先查找table中是否已存在沒有被回收、也沒有expire的entry,如果找到,并在CacheBuilder中配置了refreshAfterWrite,并且當(dāng)前時間間隔已經(jīng)操作這個事件,則重新加載值,否則,直接返回原有的值;2. 如果查找到的ValueReference是LoadingValueReference,則等待該LoadingValueReference加載結(jié)束,并返回加載的值;3. 如果沒有找到entry,或者找到的entry的值為null,則加鎖后,繼續(xù)在table中查找已存在key對應(yīng)的entry,如果找到并且對應(yīng)的entry.isLoading()為true,則表示有另一個線程正在加載,因而等待那個線程加載完成,如果找到一個非null值,返回該值,否則創(chuàng)建一個LoadingValueReference,并調(diào)用loadSync加載相應(yīng)的值,在加載完成后,將新加載的值更新到table中,即大部分情況下替換原來的LoadingValueReference。
Guava Cache提供Builder模式的CacheBuilder生成器來創(chuàng)建緩存的方式,十分方便,并且各個緩存參數(shù)的配置設(shè)置,類似于函數(shù)式編程的寫法,可自行設(shè)置各類參數(shù)選型。它提供三種方式加載到緩存中。分別是:
在構(gòu)建緩存的時候,使用build方法內(nèi)部調(diào)用CacheLoader方法加載數(shù)據(jù);
callable 、callback方式加載數(shù)據(jù);
使用粗暴直接的方式,直接Cache.put 加載數(shù)據(jù),但自動加載是首選的,因為它可以更容易的推斷所有緩存內(nèi)容的一致性。
build生成器的兩種方式都實現(xiàn)了一種邏輯:從緩存中取key的值,如果該值已經(jīng)緩存過了則返回緩存中的值,如果沒有緩存過可以通過某個方法來獲取這個值,不同的地方在于cacheloader的定義比較寬泛,是針對整個cache定義的,可以認(rèn)為是統(tǒng)一的根據(jù)key值load value的方法,而callable的方式較為靈活,允許你在get的時候指定load方法。使用示例如下:
/**
? ? * CacheLoader
? */publicvoidloadingCache(){LoadingCache<String,String>graphs=CacheBuilder.newBuilder().maximumSize(1000).build(newCacheLoader<String,String>(){@OverridepublicStringload(Stringkey)throwsException{System.out.println("key:"+key);if("key".equals(key)){return"key return result";}else{return"get-if-absent-compute";}}});StringresultVal=null;try{resultVal=graphs.get("key");}catch(ExecutionExceptione){e.printStackTrace();}System.out.println(resultVal);}/**
? ? *
? ? * Callable
? */publicvoidcallablex()throwsExecutionException{Cache<String,String>cache=CacheBuilder.newBuilder().maximumSize(1000).build();Stringresult=cache.get("key",newCallable<String>(){publicStringcall(){return"result";}});System.out.println(result);}
總體來看,Guava Cache基于ConcurrentHashMap的優(yōu)秀設(shè)計借鑒,在高并發(fā)場景支持和線程安全上都有相應(yīng)的改進(jìn)策略,使用Reference引用命令,提升高并發(fā)下的數(shù)據(jù)……訪問速度并保持了GC的可回收,有效節(jié)省空間;同時,write鏈和access鏈的設(shè)計,能更靈活、高效的實現(xiàn)多種類型的緩存清理策略,包括基于容量的清理、基于時間的清理、基于引用的清理等;編程式的build生成器管理,讓使用者有更多的自由度,能夠根據(jù)不同場景設(shè)置合適的模式。
#緩存實現(xiàn) - 分布式緩存
請參考:分布式系統(tǒng) - 分布式緩存及實現(xiàn)方案
#緩存實現(xiàn)方式 - 注解方式
#Spring注解緩存
Spring 3.1之后,引入了注解緩存技術(shù),其本質(zhì)上不是一個具體的緩存實現(xiàn)方案,而是一個對緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation,即能夠達(dá)到使用緩存對象和緩存方法的返回對象的效果。Spring的緩存技術(shù)具備相當(dāng)?shù)撵`活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業(yè)緩存集成。其特點(diǎn)總結(jié)如下:
少量的配置annotation注釋即可使得既有代碼支持緩存;
支持開箱即用,不用安裝和部署額外的第三方組件即可使用緩存;
支持Spring Express Language(SpEL),能使用對象的任何屬性或者方法來定義緩存的key和使用規(guī)則條件;
支持自定義key和自定義緩存管理者,具有相當(dāng)?shù)撵`活性和可擴(kuò)展性。
和Spring的事務(wù)管理類似,Spring Cache的關(guān)鍵原理就是Spring AOP,通過Spring AOP實現(xiàn)了在方法調(diào)用前、調(diào)用后獲取方法的入?yún)⒑头祷刂担M(jìn)而實現(xiàn)了緩存的邏輯。而Spring Cache利用了Spring AOP的動態(tài)代理技術(shù),即當(dāng)客戶端嘗試調(diào)用pojo的foo()方法的時候,給它的不是pojo自身的引用,而是一個動態(tài)生成的代理類。

圖12 Spring動態(tài)代理調(diào)用圖
如圖12所示,實際客戶端獲取的是一個代理的引用,在調(diào)用foo()方法的時候,會首先調(diào)用proxy的foo()方法,這個時候proxy可以整體控制實際的pojo.foo()方法的入?yún)⒑头祷刂?,比如緩存結(jié)果,比如直接略過執(zhí)行實際的foo()方法等,都是可以輕松做到的。Spring Cache主要使用三個注釋標(biāo)簽,即@Cacheable、@CachePut和@CacheEvict,主要針對方法上注解使用,部分場景也可以直接類上注解使用,當(dāng)在類上使用時,該類所有方法都將受影響。我們總結(jié)一下其作用和配置方法,如下表所示。
標(biāo)簽類型作用主要配置參數(shù)說明
@Cacheable主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存value:緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則默認(rèn)按照方法的所有參數(shù)進(jìn)行組合; condition:緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存
@CachePut主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存,和 @Cacheable 不同的是,它每次都會觸發(fā)真實方法的調(diào)用value:緩存的名稱,在 spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則默認(rèn)按照方法的所有參數(shù)進(jìn)行組合; condition:緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存
@CacheEvict主要針對方法配置,能夠根據(jù)一定的條件對緩存進(jìn)行清空value:緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以為空,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定,則默認(rèn)按照方法的所有參數(shù)進(jìn)行組合; condition:緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進(jìn)行緩存; allEntries:是否清空所有緩存內(nèi)容,默認(rèn)為 false,如果指定為 true,則方法調(diào)用后將立即清空所有緩存; beforeInvocation:是否在方法執(zhí)行前就清空,默認(rèn)為 false,如果指定為 true,則在方法還沒有執(zhí)行的時候就清空緩存,默認(rèn)情況下,如果方法執(zhí)行拋出異常,則不會清空緩存
可擴(kuò)展支持:Spring注解cache能夠滿足一般應(yīng)用對緩存的需求,但隨著應(yīng)用服務(wù)的復(fù)雜化,大并發(fā)高可用性能要求下,需要進(jìn)行一定的擴(kuò)展,這時對其自身集成的緩存方案可能不太適用,該怎么辦? Spring預(yù)先有考慮到這點(diǎn),那么怎樣利用Spring提供的擴(kuò)展點(diǎn)實現(xiàn)我們自己的緩存,且在不改變原來已有代碼的情況下進(jìn)行擴(kuò)展? 是否在方法執(zhí)行前就清空,默認(rèn)為false,如果指定為true,則在方法還沒有執(zhí)行的時候就清空緩存,默認(rèn)情況下,如果方法執(zhí)行拋出異常,則不會清空緩存。
這基本能夠滿足一般應(yīng)用對緩存的需求,但現(xiàn)實總是很復(fù)雜,當(dāng)你的用戶量上去或者性能跟不上,總需要進(jìn)行擴(kuò)展,這個時候你或許對其提供的內(nèi)存緩存不滿意了,因為其不支持高可用性,也不具備持久化數(shù)據(jù)能力,這個時候,你就需要自定義你的緩存方案了,還好,Spring也想到了這一點(diǎn)。
我們先不考慮如何持久化緩存,畢竟這種第三方的實現(xiàn)方案很多,我們要考慮的是,怎么利用Spring提供的擴(kuò)展點(diǎn)實現(xiàn)我們自己的緩存,且在不改原來已有代碼的情況下進(jìn)行擴(kuò)展。這需要簡單的三步驟,首先需要提供一個CacheManager接口的實現(xiàn)(繼承至AbstractCacheManager),管理自身的cache實例;其次,實現(xiàn)自己的cache實例MyCache(繼承至Cache),在這里面引入我們需要的第三方cache或自定義cache;最后就是對配置項進(jìn)行聲明,將MyCache實例注入CacheManager進(jìn)行統(tǒng)一管理。
#用戶自定義注解緩存(基于Spring注解)
以下是美團(tuán)酒店商家端使用自定義的緩存注解的方案
注解緩存的使用,可以有效增強(qiáng)應(yīng)用代碼的可讀性,同時統(tǒng)一管理緩存,提供較好的可擴(kuò)展性,為此,酒店商家端在Spring注解緩存基礎(chǔ)上,自定義了適合自身業(yè)務(wù)特性的注解緩存。
主要使用兩個標(biāo)簽,即@HotelCacheable、@HotelCacheEvict,其作用和配置方法見下表。
標(biāo)簽類型作用主要配置參數(shù)說明
@HotelCacheable主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存domain:作用域,針對集合場景,解決批量更新問題; domainKey:作用域?qū)?yīng)的緩存key; key:緩存對象key 前綴; fieldKey:緩存對象key,與前綴合并生成對象key; condition:緩存獲取前置條件,支持spel語法; cacheCondition:緩存刷入前置條件,支持spel語法; expireTime:超時時間設(shè)置
@HotelCacheEvict主要針對方法配置,能夠根據(jù)一定的條件對緩存進(jìn)行清空同上
增加作用域的概念,解決商家信息變更下,多重重要信息實時更新的問題。

圖13 域緩存處理圖
如圖13,按舊的方案,當(dāng)cache0發(fā)送變化時,為了保持信息的實時更新,需要手動刪除cache1、cache2、cache3等相關(guān)處的緩存數(shù)據(jù)。增加域緩存概念,cache0、cache1、cache2、cache3是以賬號ID為基礎(chǔ),相互存在影響約束的集合體,我們作為一個域集合,增加域緩存處理,當(dāng)cache0發(fā)送變化時,整體的賬號ID domain域已發(fā)生更新,自動影響cache1、cache2、cache3等處的緩存數(shù)據(jù)。將相關(guān)聯(lián)邏輯緩存統(tǒng)一化,有效提升代碼可讀性,同時更好服務(wù)業(yè)務(wù),賬號重點(diǎn)信息能夠?qū)崟r變更刷新,相關(guān)服務(wù)響應(yīng)速度提升。
另外,增加了cacheCondition緩存刷入前置判斷,有效解決商家業(yè)務(wù)多重外部依賴場景下,業(yè)務(wù)降級有損服務(wù)下,業(yè)務(wù)數(shù)據(jù)一致性保證,不因為緩存的增加影響業(yè)務(wù)的準(zhǔn)確性;自定義CacheManager緩存管理器,可以有效兼容公共基礎(chǔ)組件Medis、Cellar相關(guān)服務(wù),在對應(yīng)用程序不做改動的情況下,有效切換緩存方式;同時,統(tǒng)一的緩存服務(wù)AOP入口,結(jié)合接入Mtconfig統(tǒng)一配置管理,對應(yīng)用內(nèi)緩存做好降級準(zhǔn)備,一鍵關(guān)閉緩存。幾點(diǎn)建議:
上面介紹過Spring Cache的原理是基于動態(tài)生成的proxy代理機(jī)制來進(jìn)行切面處理,關(guān)鍵點(diǎn)是對象的引用問題,如果對象的方法是類里面的內(nèi)部調(diào)用(this引用)而不是外部引用的場景下,會導(dǎo)致proxy失敗,那么我們所做的緩存切面處理也就失效了。因此,應(yīng)避免已注解緩存的方法在類里面的內(nèi)部調(diào)用。
使用的key約束,緩存的key應(yīng)盡量使用簡單的可區(qū)別的元素,如ID、名稱等,不能使用list等容器的值,或者使用整體model對象的值。非public方法無法使用注解緩存實現(xiàn)。
總之,注釋驅(qū)動的Spring Cache能夠極大的減少我們編寫常見緩存的代碼量,通過少量的注釋標(biāo)簽和配置文件,即可達(dá)到使代碼具備緩存的能力,且具備很好的靈活性和擴(kuò)展性。但是我們也應(yīng)該看到,Spring Cache由于基于Spring AOP技術(shù),尤其是動態(tài)的proxy技術(shù),導(dǎo)致其不能很好的支持方法的內(nèi)部調(diào)用或者非public方法的緩存設(shè)置,當(dāng)然這些都是可以解決的問題。
#高并發(fā)緩存問題
#緩存一致性問題
當(dāng)數(shù)據(jù)時效性要求很高時,需要保證緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的保持一致,而且需要保證緩存節(jié)點(diǎn)和副本中的數(shù)據(jù)也保持一致,不能出現(xiàn)差異現(xiàn)象。這就比較依賴緩存的過期和更新策略。一般會在數(shù)據(jù)發(fā)生更改的時,主動更新緩存中的數(shù)據(jù)或者移除對應(yīng)的緩存。
#緩存并發(fā)問題
緩存過期后將嘗試從后端數(shù)據(jù)庫獲取數(shù)據(jù),這是一個看似合理的流程。但是,在高并發(fā)場景下,有可能多個請求并發(fā)的去從數(shù)據(jù)庫獲取數(shù)據(jù),對后端數(shù)據(jù)庫造成極大的沖擊,甚至導(dǎo)致 “雪崩”現(xiàn)象。此外,當(dāng)某個緩存key在被更新時,同時也可能被大量請求在獲取,這也會導(dǎo)致一致性的問題。那如何避免類似問題呢? 我們會想到類似“鎖”的機(jī)制,在緩存更新或者過期的情況下,先嘗試獲取到鎖,當(dāng)更新或者從數(shù)據(jù)庫獲取完成后再釋放鎖,其他的請求只需要犧牲一定的等待時間,即可直接從緩存中繼續(xù)獲取數(shù)據(jù)。
#緩存穿透問題
緩存穿透在有些地方也稱為“擊穿”。很多朋友對緩存穿透的理解是:由于緩存故障或者緩存過期導(dǎo)致大量請求穿透到后端數(shù)據(jù)庫服務(wù)器,從而對數(shù)據(jù)庫造成巨大沖擊。
這其實是一種誤解。真正的緩存穿透應(yīng)該是這樣的:
在高并發(fā)場景下,如果某一個key被高并發(fā)訪問,沒有被命中,出于對容錯性考慮,會嘗試去從后端數(shù)據(jù)庫中獲取,從而導(dǎo)致了大量請求達(dá)到數(shù)據(jù)庫,而當(dāng)該key對應(yīng)的數(shù)據(jù)本身就是空的情況下,這就導(dǎo)致數(shù)據(jù)庫中并發(fā)的去執(zhí)行了很多不必要的查詢操作,從而導(dǎo)致巨大沖擊和壓力。
可以通過下面的幾種常用方式來避免緩存?zhèn)鹘y(tǒng)問題:
緩存空對象
對查詢結(jié)果為空的對象也進(jìn)行緩存,如果是集合,可以緩存一個空的集合(非null),如果是緩存單個對象,可以通過字段標(biāo)識來區(qū)分。這樣避免請求穿透到后端數(shù)據(jù)庫。同時,也需要保證緩存數(shù)據(jù)的時效性。這種方式實現(xiàn)起來成本較低,比較適合命中不高,但可能被頻繁更新的數(shù)據(jù)。
單獨(dú)過濾處理
對所有可能對應(yīng)數(shù)據(jù)為空的key進(jìn)行統(tǒng)一的存放,并在請求前做攔截,這樣避免請求穿透到后端數(shù)據(jù)庫。這種方式實現(xiàn)起來相對復(fù)雜,比較適合命中不高,但是更新不頻繁的數(shù)據(jù)。
#緩存抖動問題
緩存抖動可以看做是一種比“雪崩”更輕微的故障,但是也會在一段時間內(nèi)對系統(tǒng)造成沖擊和性能影響。一般是由于緩存節(jié)點(diǎn)故障導(dǎo)致。業(yè)內(nèi)推薦的做法是通過一致性Hash算法來解決。這里不做過多闡述。
#緩存雪崩問題
緩存雪崩就是指由于緩存的原因,導(dǎo)致大量請求到達(dá)后端數(shù)據(jù)庫,從而導(dǎo)致數(shù)據(jù)庫崩潰,整個系統(tǒng)崩潰,發(fā)生災(zāi)難。導(dǎo)致這種現(xiàn)象的原因有很多種,上面提到的“緩存并發(fā)”,“緩存穿透”,“緩存顛簸”等問題,其實都可能會導(dǎo)致緩存雪崩現(xiàn)象發(fā)生。這些問題也可能會被惡意攻擊者所利用。還有一種情況,例如某個時間點(diǎn)內(nèi),系統(tǒng)預(yù)加載的緩存周期性集中失效了,也可能會導(dǎo)致雪崩。為了避免這種周期性失效,可以通過設(shè)置不同的過期時間,來錯開緩存過期,從而避免緩存集中失效。
從應(yīng)用架構(gòu)角度,我們可以通過限流、降級、熔斷等手段來降低影響,也可以通過多級緩存來避免這種災(zāi)難。
此外,從整個研發(fā)體系流程的角度,應(yīng)該加強(qiáng)壓力測試,盡量模擬真實場景,盡早的暴露問題從而防范。
#合理利用緩存
不合理使用緩存非但不能提高系統(tǒng)的性能,還會成為系統(tǒng)的累贅,甚至風(fēng)險。
#頻繁修改的數(shù)據(jù)
如果緩存中保存的是頻繁修改的數(shù)據(jù),就會出現(xiàn)數(shù)據(jù)寫入緩存后,應(yīng)用還來不及讀取緩存,數(shù)據(jù)就已經(jīng)失效,徒增系統(tǒng)負(fù)擔(dān)。一般來說,數(shù)據(jù)的讀寫比在2:1(寫入一次緩存,在數(shù)據(jù)更新前至少讀取兩次)以上,緩存才有意義。
#沒有熱點(diǎn)的訪問
如果應(yīng)用系統(tǒng)訪問數(shù)據(jù)沒有熱點(diǎn),不遵循二八定律,那么緩存就沒有意義。
#數(shù)據(jù)不一致與臟讀
一般會對緩存的數(shù)據(jù)設(shè)置失效時間,一旦超過失效時間,就要從數(shù)據(jù)庫中重新加載。因此要容忍一定時間的數(shù)據(jù)不一致,如賣家已經(jīng)編輯了商品屬性,但是需要過一段時間才能被買家看到。還有一種策略是數(shù)據(jù)更新立即更新緩存,不過這也會帶來更多系統(tǒng)開銷和事務(wù)一致性問題。
#緩存可用性
緩存會承擔(dān)大部分?jǐn)?shù)據(jù)庫訪問壓力,數(shù)據(jù)庫已經(jīng)習(xí)慣了有緩存的日子,所以當(dāng)緩存服務(wù)崩潰時,數(shù)據(jù)庫會因為完全不能承受如此大壓力而宕機(jī),導(dǎo)致網(wǎng)站不可用。這種情況被稱作緩存雪崩,發(fā)生這種故障,甚至不能簡單地重啟緩存服務(wù)器和數(shù)據(jù)庫服務(wù)器來恢復(fù)。
實踐中,有的網(wǎng)站通過緩存熱備份等手段提高緩存可用性:當(dāng)某臺緩存服務(wù)器宕機(jī)時,將緩存訪問切換到熱備服務(wù)器上。但這種設(shè)計有違緩存的初衷,緩存根本就不應(yīng)該當(dāng)做一個可靠的數(shù)據(jù)源來使用。
通過分布式緩存服務(wù)器集群,將緩存數(shù)據(jù)分布到集群多臺服務(wù)器上可在一定程度上改善緩存的可用性。當(dāng)一臺緩存服務(wù)器宕機(jī)時,只有部分緩存數(shù)據(jù)丟失,重新從數(shù)據(jù)庫加載這部分?jǐn)?shù)據(jù)不會產(chǎn)生很大的影響。
#緩存預(yù)熱(warm up)
緩存中存放的是熱點(diǎn)數(shù)據(jù),熱點(diǎn)數(shù)據(jù)又是緩存系統(tǒng)利用LRU(最近最久未用算法)對不斷訪問的數(shù)據(jù)篩選淘汰出來,這個過程需要花費(fèi)較長的時間。新系統(tǒng)的緩存系統(tǒng)如果沒有任何數(shù)據(jù),在重建緩存數(shù)據(jù)的過程中,系統(tǒng)的性能和數(shù)據(jù)庫負(fù)載都不太好,那么最好在緩存系統(tǒng)啟動時就把熱點(diǎn)數(shù)據(jù)加載好,這個緩存預(yù)加載手段叫緩存預(yù)熱。對于一些元數(shù)據(jù)如城市地名列表、類目信息,可以在啟動時加載數(shù)據(jù)庫中全部數(shù)據(jù)到緩存進(jìn)行預(yù)熱。
#避免緩存穿透
如果因為不恰當(dāng)?shù)臉I(yè)務(wù)、或者惡意攻擊持續(xù)高并發(fā)地請求某個不存在的數(shù)據(jù),由于緩存沒有保存該數(shù)據(jù),所有的請求都會落到數(shù)據(jù)庫上,會對數(shù)據(jù)庫造成壓力,甚至崩潰。一個簡單的對策是將不存在的數(shù)據(jù)也緩存起來(其value為null)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?更多技術(shù)資源敬請登錄www.ayshuju.com官網(wǎng)