架構(gòu)之高并發(fā):緩存
高并發(fā)實(shí)現(xiàn)的三板斧:緩存,限流和降級圈匆。緩存在高并發(fā)系統(tǒng)中有者極其廣闊的應(yīng)用,需要重點(diǎn)掌握捏雌,本文重點(diǎn)介紹下緩存及其實(shí)現(xiàn)跃赚。
#緩存簡介
隨著互聯(lián)網(wǎng)的普及,內(nèi)容信息越來越復(fù)雜腹忽,用戶數(shù)和訪問量越來越大来累,我們的應(yīng)用需要支撐更多的并發(fā)量砚作,同時(shí)我們的應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器所做的計(jì)算也越來越多。但是往往我們的應(yīng)用服務(wù)器資源是有限的嘹锁,且技術(shù)變革是緩慢的葫录,數(shù)據(jù)庫每秒能接受的請求次數(shù)也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供盡可能大的吞吐量? 一個(gè)有效的辦法就是引入緩存领猾,打破標(biāo)準(zhǔn)流程米同,每個(gè)環(huán)節(jié)中請求可以從緩存中直接獲取目標(biāo)數(shù)據(jù)并返回,從而減少計(jì)算量摔竿,有效提升響應(yīng)速度面粮,讓有限的資源服務(wù)更多的用戶。
如圖1所示继低,緩存的使用可以出現(xiàn)在1~4的各個(gè)環(huán)節(jié)中熬苍,每個(gè)環(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ù)需求也決定了實(shí)時(shí)性的要求粱胜,直接影響到過期時(shí)間和更新策略柄驻,實(shí)時(shí)性要求越低越適合緩存。
2.緩存的設(shè)計(jì)(策略和粒度)
通常情況下緩存的粒度越小焙压,命中率越高鸿脓。比如說緩存一個(gè)用戶信息的對象,只有當(dāng)這個(gè)用戶的信息發(fā)生變化的時(shí)候才更新緩存涯曲,而如果是緩存一個(gè)集合的話野哭,集合中任何一個(gè)對象發(fā)生變化都要重新更新緩存。
當(dāng)數(shù)據(jù)發(fā)生變化時(shí)幻件,直接更新緩存的值比移除緩存或者讓緩存過期它的命中率更高虐拓,不過這個(gè)時(shí)候系統(tǒng)的復(fù)雜度過高。
3.緩存的容量和基礎(chǔ)設(shè)施
緩存的容量有限就會容易引起緩存的失效和被淘汰傲武。目前多數(shù)的緩存框架和中間件都采用LRU這個(gè)算法。同時(shí)采用緩存的技術(shù)選型也是至關(guān)重要的城榛,比如采用本地內(nèi)置的應(yīng)用緩存揪利,就比較容易出現(xiàn)單機(jī)瓶頸。而采用分布式緩存就更加容易擴(kuò)展狠持。所以需要做好系統(tǒng)容量規(guī)劃疟位,系統(tǒng)是否可擴(kuò)展。
最大空間
緩存最大空間一旦緩存中元素?cái)?shù)量超過這個(gè)值(或者緩存數(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í),數(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/過期時(shí)間/隨機(jī)
FIFO:最先進(jìn)入緩存的數(shù)據(jù)超歌,在緩存空間不足時(shí)被清除,為了保證最新數(shù)據(jù)可用蒂教,保證實(shí)時(shí)性
LFU(Least Frequently Used):最近最不常用巍举,基于訪問次數(shù),去除命中次數(shù)最少的元素凝垛,保證高頻數(shù)據(jù)有效性
LRU(Least Recently Used):最近最少使用懊悯,基于訪問時(shí)間,在被訪問過的元素中去除最久未使用的元素梦皮,保證熱點(diǎn)數(shù)據(jù)的有效性
#哪里用了緩存
一切地方炭分。例如:
我們從硬盤讀數(shù)據(jù)的時(shí)候,其實(shí)操作系統(tǒng)還額外把附近的數(shù)據(jù)都讀到了內(nèi)存里
例如剑肯,CPU在從內(nèi)存里讀數(shù)據(jù)的時(shí)候捧毛,也額外讀了許多數(shù)據(jù)到各級cache里
各個(gè)輸入輸出之間用buffer保存一批數(shù)據(jù)統(tǒng)一發(fā)送和接受,而不是一個(gè)byte一個(gè)byte的處理
上面這是系統(tǒng)層面,在軟件系統(tǒng)設(shè)計(jì)層面呀忧,很多地方也用了緩存:
瀏覽器會緩存頁面的元素师痕,這樣在重復(fù)訪問網(wǎng)頁時(shí),就避開了要從互聯(lián)網(wǎng)上下載數(shù)據(jù)(例如大圖片)
web服務(wù)會把靜態(tài)的東西提前部署在CDN上而账,這也是一種緩存
數(shù)據(jù)庫會緩存查詢胰坟,所以同一條查詢第二次就是要比第一次快
內(nèi)存數(shù)據(jù)庫(如redis)選擇把大量數(shù)據(jù)存在內(nèi)存而非硬盤里,這可以看作是一個(gè)大型緩存福扬,只是把整個(gè)數(shù)據(jù)庫緩存了起來
應(yīng)用程序把最近幾次計(jì)算的結(jié)果放在本地內(nèi)存里腕铸,如果下次到來的請求還是原請求,就跳過計(jì)算直接返回結(jié)果 ...
#緩存應(yīng)用和實(shí)現(xiàn)
緩存有各類特征铛碑,而且有不同介質(zhì)的區(qū)別狠裹,那么實(shí)際工程中我們怎么去對緩存分類呢? 在目前的應(yīng)用服務(wù)框架中,比較常見的是根據(jù)緩存與應(yīng)用的藕合度汽烦,分為local cache(本地緩存)和remote cache(分布式緩存):
本地緩存:指的是在應(yīng)用中的緩存組件涛菠,其最大的優(yōu)點(diǎn)是應(yīng)用和cache是在同一個(gè)進(jìn)程內(nèi)部,請求緩存非称餐蹋快速俗冻,沒有過多的網(wǎng)絡(luò)開銷等,在單應(yīng)用不需要集群支持或者集群情況下各節(jié)點(diǎn)無需互相通知的場景下使用本地緩存較合適牍颈;同時(shí)迄薄,它的缺點(diǎn)也是應(yīng)為緩存跟應(yīng)用程序耦合,多個(gè)應(yīng)用程序無法直接的共享緩存煮岁,各應(yīng)用或集群的各節(jié)點(diǎn)都需要維護(hù)自己的單獨(dú)緩存讥蔽,對內(nèi)存是一種浪費(fèi)。
分布式緩存:指的是與應(yīng)用分離的緩存組件或服務(wù)画机,其最大的優(yōu)點(diǎn)是自身就是一個(gè)獨(dú)立的應(yīng)用冶伞,與本地應(yīng)用隔離,多個(gè)應(yīng)用可直接的共享緩存步氏。
目前各種類型的緩存都活躍在成千上萬的應(yīng)用服務(wù)中响禽,還沒有一種緩存方案可以解決一切的業(yè)務(wù)場景或數(shù)據(jù)類型,我們需要根據(jù)自身的特殊場景和背景荚醒,選擇最適合的緩存方案芋类。緩存的使用是程序員、架構(gòu)師的必備技能界阁,好的程序員能根據(jù)數(shù)據(jù)類型梗肝、業(yè)務(wù)場景來準(zhǔn)確判斷使用何種類型的緩存,如何使用這種緩存铺董,以最小的成本最快的效率達(dá)到最優(yōu)的目的。
#緩存實(shí)現(xiàn)-本地緩存
編程直接實(shí)現(xiàn)緩存 個(gè)別場景下,我們只需要簡單的緩存數(shù)據(jù)的功能精续,而無需關(guān)注更多存取坝锰、清空策略等深入的特性時(shí),直接編程實(shí)現(xiàn)緩存則是最便捷和高效的重付。
#成員變量或局部變量實(shí)現(xiàn)
簡單代碼示例如下:
publicvoidUseLocalCache(){//一個(gè)本地的緩存變量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)變量實(shí)現(xiàn)
最常用的單例實(shí)現(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)變量實(shí)現(xiàn)類間可共享纤子,進(jìn)程內(nèi)可共享,緩存的實(shí)時(shí)性稍差款票。
為了解決本地緩存數(shù)據(jù)的實(shí)時(shí)性問題控硼,目前大量使用的是結(jié)合ZooKeeper的自動發(fā)現(xiàn)機(jī)制,實(shí)時(shí)變更本地靜態(tài)變量緩存:
美團(tuán)內(nèi)部的基礎(chǔ)配置組件MtConfig艾少,采用的就是類似原理卡乾,使用靜態(tài)變量緩存,結(jié)合ZooKeeper的統(tǒng)一管理缚够,做到自動動態(tài)更新緩存幔妨,如圖2所示。
圖2 Mtconfig實(shí)現(xiàn)圖
這類緩存實(shí)現(xiàn)潮瓶,優(yōu)點(diǎn)是能直接在heap區(qū)內(nèi)讀寫陶冷,最快也最方便;缺點(diǎn)同樣是受heap區(qū)域影響毯辅,緩存的數(shù)據(jù)量非常有限埂伦,同時(shí)緩存時(shí)間受GC影響。主要滿足單機(jī)場景下的小數(shù)據(jù)量緩存需求思恐,同時(shí)對緩存數(shù)據(jù)的變更無需太敏感感知沾谜,如上一般配置管理、基礎(chǔ)靜態(tài)數(shù)據(jù)等場景胀莹。
#Ehcache
Ehcache是現(xiàn)在最流行的純Java開源緩存框架基跑,配置簡單、結(jié)構(gòu)清晰描焰、功能強(qiáng)大媳否,是一個(gè)非常輕量級的緩存實(shí)現(xiàn)栅螟,我們常用的Hibernate里面就集成了相關(guān)緩存功能。
圖3 Ehcache框架圖
從圖3中我們可以了解到篱竭,Ehcache的核心定義主要包括:
cache manager:緩存管理器力图,以前是只允許單例的,不過現(xiàn)在也可以多實(shí)例了掺逼。
cache:緩存管理器內(nèi)可以放置若干cache吃媒,存放數(shù)據(jù)的實(shí)質(zhì),所有cache都實(shí)現(xiàn)了Ehcache接口吕喘,這是一個(gè)真正使用的緩存實(shí)例赘那;通過緩存管理器的模式,可以在單個(gè)應(yīng)用中輕松隔離多個(gè)緩存實(shí)例氯质,獨(dú)立服務(wù)于不同業(yè)務(wù)場景需求募舟,緩存數(shù)據(jù)物理隔離,同時(shí)需要時(shí)又可共享使用病梢。
element:單條緩存數(shù)據(jù)的組成單位胃珍。
system of record(SOR):可以取到真實(shí)數(shù)據(jù)的組件,可以是真正的業(yè)務(wù)邏輯蜓陌、外部接口調(diào)用觅彰、存放真實(shí)數(shù)據(jù)的數(shù)據(jù)庫等,緩存就是從SOR中讀取或者寫入到SOR中去的钮热。
在上層可以看到填抬,整個(gè)Ehcache提供了對JSR、JMX等的標(biāo)準(zhǔn)支持隧期,能夠較好的兼容和移植飒责,同時(shí)對各類對象有較完善的監(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:從低層拷貝一個(gè)對象到高層季希。在獲取緩存的過程中褪那,某一層發(fā)現(xiàn)自己的該緩存條目已經(jīng)失效幽纷,就觸發(fā)了Fault行為。
Eviction:把緩存條目除去博敬。
Expiration:失效狀態(tài)霹崎。
Pinning:強(qiáng)制緩存條目保持在某一層。
圖4反映了數(shù)據(jù)在各個(gè)層之間的流轉(zhuǎn)冶忱,同時(shí)也體現(xiàn)了各層數(shù)據(jù)的一個(gè)生命周期。
圖4 緩存數(shù)據(jù)流轉(zhuǎn)圖(L1:本地內(nèi)存層境析;L2:Terracotta服務(wù)節(jié)點(diǎn)層)
Ehcache的配置使用如下:
<ehcache><!-- 指定一個(gè)文件目錄囚枪,當(dāng)Ehcache把數(shù)據(jù)寫到硬盤上時(shí),將把數(shù)據(jù)寫到這個(gè)文件目錄下 --><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表示對象永不過期链沼,此時(shí)會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認(rèn)為false
? ? ? ? overflowToDisk:true表示當(dāng)內(nèi)存緩存的對象數(shù)目達(dá)到了maxElementsInMemory界限后沛鸵,會把溢出的對象寫到硬盤緩存中括勺。注意:如果緩存的對象要寫入到硬盤中的話,則該對象必須實(shí)現(xiàn)了Serializable接口才行曲掰。
? ? ? ? diskSpoolBufferSizeMB:磁盤緩存區(qū)大小疾捍,默認(rèn)為30MB。每個(gè)Cache都應(yīng)該有自己的一個(gè)緩存區(qū)栏妖。
? ? ? ? diskPersistent:是否緩存虛擬機(jī)重啟期數(shù)據(jù)
? ? ? ? diskExpiryThreadIntervalSeconds:磁盤失效線程運(yùn)行時(shí)間間隔乱豆,默認(rèn)為120秒
? ? ? ? timeToIdleSeconds: 設(shè)定允許對象處于空閑狀態(tài)的最長時(shí)間,以秒為單位吊趾。當(dāng)對象自從最近一次被訪問后宛裕,如果處于空閑狀態(tài)的時(shí)間超過了timeToIdleSeconds屬性值,這個(gè)對象就會過期论泛,EHCache將把它從緩存中清空揩尸。只有當(dāng)eternal屬性為false,該屬性才有效屁奏。如果該屬性值為0岩榆,則表示對象可以無限期地處于空閑狀態(tài)
? ? ? ? timeToLiveSeconds:設(shè)定對象允許存在于緩存中的最長時(shí)間,以秒為單位了袁。當(dāng)對象自從被存放到緩存中后朗恳,如果處于緩存中的時(shí)間超過了 timeToLiveSeconds屬性值,這個(gè)對象就會過期载绿,Ehcache將把它從緩存中清除粥诫。只有當(dāng)eternal屬性為false,該屬性才有效崭庸。如果該屬性值為0怀浆,則表示對象可以無限期地存在于緩存中谊囚。timeToLiveSeconds必須大于timeToIdleSeconds屬性,才有意義
? ? ? ? memoryStoreEvictionPolicy:當(dāng)達(dá)到maxElementsInMemory限制時(shí)执赡,Ehcache將會根據(jù)指定的策略去清理內(nèi)存镰踏。可選策略有: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)行緩存實(shí)例的監(jiān)控管理昆雀。
支持多緩存管理器實(shí)例辱志,以及一個(gè)實(shí)例的多個(gè)緩存區(qū)域蝠筑。
注意:Ehcache的超時(shí)設(shè)置主要是針對整個(gè)cache實(shí)例設(shè)置整體的超時(shí)策略,而沒有較好的處理針對單獨(dú)的key的個(gè)性的超時(shí)設(shè)置(有策略設(shè)置揩懒,但是比較復(fù)雜什乙,就不描述了),因此已球,在使用中要注意過期失效的緩存元素?zé)o法被GC回收臣镣,時(shí)間越長緩存越多,內(nèi)存占用也就越大智亮,內(nèi)存泄露的概率也越大退疫。
#Guava Cache
Guava Cache是Google開源的Java重用工具集庫Guava里的一款緩存工具,其主要實(shí)現(xiàn)的緩存功能有:
自動將entry節(jié)點(diǎn)加載進(jìn)緩存結(jié)構(gòu)中鸽素;
當(dāng)緩存的數(shù)據(jù)超過設(shè)置的最大值時(shí),使用LRU算法移除亦鳞;
具備根據(jù)entry節(jié)點(diǎn)上次被訪問或者寫入時(shí)間計(jì)算它的過期機(jī)制馍忽;
緩存的key被封裝在WeakReference引用內(nèi);
緩存的Value被封裝在WeakReference或SoftReference引用內(nèi)燕差;
統(tǒng)計(jì)緩存使用過程中命中率遭笋、異常率、未命中率等統(tǒng)計(jì)數(shù)據(jù)徒探。
Guava Cache的架構(gòu)設(shè)計(jì)靈感來源于ConcurrentHashMap瓦呼,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量數(shù)據(jù)的緩存测暗,但是央串,如果結(jié)果可能隨時(shí)間改變或者是希望存儲的數(shù)據(jù)空間可控的話,自己實(shí)現(xiàn)這種數(shù)據(jù)結(jié)構(gòu)還是有必要的碗啄。
Guava Cache繼承了ConcurrentHashMap的思路质和,使用多個(gè)segments方式的細(xì)粒度鎖,在保證線程安全的同時(shí)稚字,支持高并發(fā)場景需求饲宿。Cache類似于Map,它是存儲鍵值對的集合胆描,不同的是它還需要處理evict瘫想、expire、dynamic load等算法邏輯昌讲,需要一些額外信息來實(shí)現(xiàn)這些操作国夜。對此,根據(jù)面向?qū)ο笏枷刖珧迹枰龇椒ㄅc數(shù)據(jù)的關(guān)聯(lián)封裝支竹。如圖5所示cache的內(nèi)存數(shù)據(jù)模型旋廷,可以看到,使用ReferenceEntry接口來封裝一個(gè)鍵值對礼搁,而用ValueReference來封裝Value值饶碘,之所以用Reference命令,是因?yàn)镃ache要支持WeakReference Key和SoftReference馒吴、WeakReference value扎运。
圖5 Guava Cache數(shù)據(jù)結(jié)構(gòu)圖
ReferenceEntry是對一個(gè)鍵值對節(jié)點(diǎn)的抽象,它包含了key和值的ValueReference抽象類饮戳,Cache由多個(gè)Segment組成豪治,而每個(gè)Segment包含一個(gè)ReferenceEntry數(shù)組,每個(gè)ReferenceEntry數(shù)組項(xiàng)都是一條ReferenceEntry鏈扯罐,且一個(gè)ReferenceEntry包含key负拟、hash、valueReference歹河、next字段掩浙。除了在ReferenceEntry數(shù)組項(xiàng)中組成的鏈,在一個(gè)Segment中秸歧,所有ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue)(后面會介紹鏈的作用)厨姚。ReferenceEntry可以是強(qiáng)引用類型的key,也可以WeakReference類型的key键菱,為了減少內(nèi)存使用量谬墙,還可以根據(jù)是否配置了expireAfterWrite、expireAfterAccess经备、maximumSize來決定是否需要write鏈和access鏈確定要?jiǎng)?chuàng)建的具體Reference:StrongEntry拭抬、StrongWriteEntry、StrongAccessEntry侵蒙、StrongWriteAccessEntry等玖喘。
對于ValueReference,因?yàn)镃ache支持強(qiáng)引用的Value蘑志、SoftReference Value以及WeakReference Value累奈,因而它對應(yīng)三個(gè)實(shí)現(xiàn)類:StrongValueReference、SoftValueReference急但、WeakValueReference澎媒。為了支持動態(tài)加載機(jī)制,它還有一個(gè)LoadingValueReference波桩,在需要?jiǎng)討B(tài)加載一個(gè)key的值時(shí)戒努,先把該值封裝在LoadingValueReference中,以表達(dá)該key對應(yīng)的值已經(jīng)在加載了,如果其他線程也要查詢該key對應(yīng)的值储玫,就能得到該引用侍筛,并且等待改值加載完成,從而保證該值只被加載一次撒穷,在該值加載完成后匣椰,將LoadingValueReference替換成其他ValueReference類型。ValueReference對象中會保留對ReferenceEntry的引用端礼,這是因?yàn)樵赩alue因?yàn)閃eakReference禽笑、SoftReference被回收時(shí),需要使用其key將對應(yīng)的項(xiàng)從Segment的table中移除蛤奥。
WriteQueue和AccessQueue:為了實(shí)現(xiàn)最近最少使用算法佳镜,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個(gè)雙向鏈表凡桥,通過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)的下一個(gè)節(jié)點(diǎn)移除薪捍,并返回笼痹。
了解了cache的整體數(shù)據(jù)結(jié)構(gòu)后,再來看下針對緩存的相關(guān)操作就簡單多了:
Segment中的evict清除策略操作酪穿,是在每一次調(diào)用操作的開始和結(jié)束時(shí)觸發(fā)清理工作凳干,這樣比一般的緩存另起線程監(jiān)控清理相比,可以減少開銷被济,但如果長時(shí)間沒有調(diào)用方法的話救赐,會導(dǎo)致不能及時(shí)的清理釋放內(nèi)存空間的問題。evict主要處理四個(gè)Queue:1. keyReferenceQueue只磷;2. valueReferenceQueue经磅;3. writeQueue泌绣;4. accessQueue。前兩個(gè)queue是因?yàn)閃eakReference预厌、SoftReference被垃圾回收時(shí)加入的阿迈,清理時(shí)只需要遍歷整個(gè)queue,將對應(yīng)的項(xiàng)從LocalCache中移除即可配乓,這里keyReferenceQueue存放ReferenceEntry仿滔,而valueReferenceQueue存放的是ValueReference,要從Cache中移除需要有key犹芹,因而ValueReference需要有對ReferenceEntry的引用崎页,這個(gè)前面也提到過了。而對后面兩個(gè)Queue腰埂,只需要檢查是否配置了相應(yīng)的expire時(shí)間飒焦,然后從頭開始查找已經(jīng)expire的Entry,將它們移除即可屿笼。
Segment中的put操作:put操作相對比較簡單牺荠,首先它需要獲得鎖,然后嘗試做一些清理工作驴一,接下來的邏輯類似ConcurrentHashMap中的rehash休雌,查找位置并注入數(shù)據(jù)。需要說明的是當(dāng)找到一個(gè)已存在的Entry時(shí)肝断,需要先判斷當(dāng)前的ValueRefernece中的值事實(shí)上已經(jīng)被回收了杈曲,因?yàn)樗鼈兛梢允荳eakReference、SoftReference類型胸懈,如果已經(jīng)被回收了担扑,則將新值寫入。并且在每次更新時(shí)注冊當(dāng)前操作引起的移除事件趣钱,指定相應(yīng)的原因:COLLECTED涌献、REPLACED等,這些注冊的事件在退出的時(shí)候統(tǒng)一調(diào)用Cache注冊的RemovalListener首有,由于事件處理可能會有很長時(shí)間燕垃,因而這里將事件處理的邏輯在退出鎖以后才做。最后井联,在更新已存在的Entry結(jié)束后都嘗試著將那些已經(jīng)expire的Entry移除利术。另外put操作中還需要更新writeQueue和accessQueue的語義正確性。
Segment帶CacheLoader的get操作:1. 先查找table中是否已存在沒有被回收低矮、也沒有expire的entry印叁,如果找到,并在CacheBuilder中配置了refreshAfterWrite,并且當(dāng)前時(shí)間間隔已經(jīng)操作這個(gè)事件轮蜕,則重新加載值昨悼,否則,直接返回原有的值跃洛;2. 如果查找到的ValueReference是LoadingValueReference率触,則等待該LoadingValueReference加載結(jié)束,并返回加載的值汇竭;3. 如果沒有找到entry葱蝗,或者找到的entry的值為null,則加鎖后细燎,繼續(xù)在table中查找已存在key對應(yīng)的entry两曼,如果找到并且對應(yīng)的entry.isLoading()為true,則表示有另一個(gè)線程正在加載玻驻,因而等待那個(gè)線程加載完成悼凑,如果找到一個(gè)非null值,返回該值璧瞬,否則創(chuàng)建一個(gè)LoadingValueReference户辫,并調(diào)用loadSync加載相應(yīng)的值,在加載完成后嗤锉,將新加載的值更新到table中渔欢,即大部分情況下替換原來的LoadingValueReference。
Guava Cache提供Builder模式的CacheBuilder生成器來創(chuàng)建緩存的方式瘟忱,十分方便奥额,并且各個(gè)緩存參數(shù)的配置設(shè)置,類似于函數(shù)式編程的寫法酷誓,可自行設(shè)置各類參數(shù)選型。它提供三種方式加載到緩存中态坦。分別是:
在構(gòu)建緩存的時(shí)候盐数,使用build方法內(nèi)部調(diào)用CacheLoader方法加載數(shù)據(jù);
callable 伞梯、callback方式加載數(shù)據(jù)玫氢;
使用粗暴直接的方式,直接Cache.put 加載數(shù)據(jù)谜诫,但自動加載是首選的漾峡,因?yàn)樗梢愿菀椎耐茢嗨芯彺鎯?nèi)容的一致性。
build生成器的兩種方式都實(shí)現(xiàn)了一種邏輯:從緩存中取key的值喻旷,如果該值已經(jīng)緩存過了則返回緩存中的值生逸,如果沒有緩存過可以通過某個(gè)方法來獲取這個(gè)值,不同的地方在于cacheloader的定義比較寬泛,是針對整個(gè)cache定義的槽袄,可以認(rèn)為是統(tǒng)一的根據(jù)key值load value的方法烙无,而callable的方式較為靈活,允許你在get的時(shí)候指定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è)計(jì)借鑒,在高并發(fā)場景支持和線程安全上都有相應(yīng)的改進(jìn)策略乾戏,使用Reference引用命令迂苛,提升高并發(fā)下的數(shù)據(jù)……訪問速度并保持了GC的可回收,有效節(jié)省空間鼓择;同時(shí)三幻,write鏈和access鏈的設(shè)計(jì),能更靈活惯退、高效的實(shí)現(xiàn)多種類型的緩存清理策略赌髓,包括基于容量的清理、基于時(shí)間的清理催跪、基于引用的清理等锁蠕;編程式的build生成器管理,讓使用者有更多的自由度懊蒸,能夠根據(jù)不同場景設(shè)置合適的模式荣倾。
#緩存實(shí)現(xiàn) - 分布式緩存
請參考:分布式系統(tǒng) - 分布式緩存及實(shí)現(xiàn)方案
#緩存實(shí)現(xiàn)方式 - 注解方式
#Spring注解緩存
Spring 3.1之后,引入了注解緩存技術(shù)骑丸,其本質(zhì)上不是一個(gè)具體的緩存實(shí)現(xiàn)方案舌仍,而是一個(gè)對緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation通危,即能夠達(dá)到使用緩存對象和緩存方法的返回對象的效果铸豁。Spring的緩存技術(shù)具備相當(dāng)?shù)撵`活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition菊碟,還提供開箱即用的緩存臨時(shí)存儲方案节芥,也支持和主流的專業(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實(shí)現(xiàn)了在方法調(diào)用前、調(diào)用后獲取方法的入?yún)⒑头祷刂得夷伲M(jìn)而實(shí)現(xiàn)了緩存的邏輯欲鹏。而Spring Cache利用了Spring AOP的動態(tài)代理技術(shù),即當(dāng)客戶端嘗試調(diào)用pojo的foo()方法的時(shí)候臭墨,給它的不是pojo自身的引用赔嚎,而是一個(gè)動態(tài)生成的代理類。
圖12 Spring動態(tài)代理調(diào)用圖
如圖12所示胧弛,實(shí)際客戶端獲取的是一個(gè)代理的引用尤误,在調(diào)用foo()方法的時(shí)候,會首先調(diào)用proxy的foo()方法结缚,這個(gè)時(shí)候proxy可以整體控制實(shí)際的pojo.foo()方法的入?yún)⒑头祷刂邓鹞睿热缇彺娼Y(jié)果,比如直接略過執(zhí)行實(shí)際的foo()方法等红竭,都是可以輕松做到的尤勋。Spring Cache主要使用三個(gè)注釋標(biāo)簽,即@Cacheable茵宪、@CachePut和@CacheEvict最冰,主要針對方法上注解使用,部分場景也可以直接類上注解使用稀火,當(dāng)在類上使用時(shí)暖哨,該類所有方法都將受影響。我們總結(jié)一下其作用和配置方法凰狞,如下表所示篇裁。
標(biāo)簽類型作用主要配置參數(shù)說明
@Cacheable主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存value:緩存的名稱赡若,在 Spring 配置文件中定義达布,必須指定至少一個(gè); key:緩存的 key逾冬,可以為空黍聂,如果指定要按照 SpEL 表達(dá)式編寫,如果不指定粉渠,則默認(rèn)按照方法的所有參數(shù)進(jìn)行組合分冈; condition:緩存的條件圾另,可以為空霸株,使用 SpEL 編寫,返回 true 或者 false集乔,只有為 true 才進(jìn)行緩存
@CachePut主要針對方法配置去件,能夠根據(jù)方法的請求參數(shù)對其結(jié)果進(jìn)行緩存坡椒,和 @Cacheable 不同的是,它每次都會觸發(fā)真實(shí)方法的調(diào)用value:緩存的名稱尤溜,在 spring 配置文件中定義倔叼,必須指定至少一個(gè); key:緩存的 key,可以為空宫莱,如果指定要按照 SpEL 表達(dá)式編寫丈攒,如果不指定,則默認(rèn)按照方法的所有參數(shù)進(jìn)行組合授霸; condition:緩存的條件巡验,可以為空,使用 SpEL 編寫碘耳,返回 true 或者 false显设,只有為 true 才進(jìn)行緩存
@CacheEvict主要針對方法配置,能夠根據(jù)一定的條件對緩存進(jìn)行清空value:緩存的名稱辛辨,在 Spring 配置文件中定義捕捂,必須指定至少一個(gè); 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í)行的時(shí)候就清空緩存,默認(rèn)情況下屡律,如果方法執(zhí)行拋出異常腌逢,則不會清空緩存
可擴(kuò)展支持:Spring注解cache能夠滿足一般應(yīng)用對緩存的需求,但隨著應(yīng)用服務(wù)的復(fù)雜化超埋,大并發(fā)高可用性能要求下搏讶,需要進(jìn)行一定的擴(kuò)展佳鳖,這時(shí)對其自身集成的緩存方案可能不太適用,該怎么辦? Spring預(yù)先有考慮到這點(diǎn)媒惕,那么怎樣利用Spring提供的擴(kuò)展點(diǎn)實(shí)現(xiàn)我們自己的緩存系吩,且在不改變原來已有代碼的情況下進(jìn)行擴(kuò)展? 是否在方法執(zhí)行前就清空,默認(rèn)為false妒蔚,如果指定為true穿挨,則在方法還沒有執(zhí)行的時(shí)候就清空緩存,默認(rèn)情況下肴盏,如果方法執(zhí)行拋出異常絮蒿,則不會清空緩存。
這基本能夠滿足一般應(yīng)用對緩存的需求叁鉴,但現(xiàn)實(shí)總是很復(fù)雜土涝,當(dāng)你的用戶量上去或者性能跟不上,總需要進(jìn)行擴(kuò)展幌墓,這個(gè)時(shí)候你或許對其提供的內(nèi)存緩存不滿意了但壮,因?yàn)槠洳恢С指呖捎眯裕膊痪邆涑志没瘮?shù)據(jù)能力常侣,這個(gè)時(shí)候蜡饵,你就需要自定義你的緩存方案了,還好胳施,Spring也想到了這一點(diǎn)溯祸。
我們先不考慮如何持久化緩存,畢竟這種第三方的實(shí)現(xiàn)方案很多舞肆,我們要考慮的是焦辅,怎么利用Spring提供的擴(kuò)展點(diǎn)實(shí)現(xiàn)我們自己的緩存,且在不改原來已有代碼的情況下進(jìn)行擴(kuò)展椿胯。這需要簡單的三步驟筷登,首先需要提供一個(gè)CacheManager接口的實(shí)現(xiàn)(繼承至AbstractCacheManager),管理自身的cache實(shí)例哩盲;其次前方,實(shí)現(xiàn)自己的cache實(shí)例MyCache(繼承至Cache),在這里面引入我們需要的第三方cache或自定義cache廉油;最后就是對配置項(xiàng)進(jìn)行聲明惠险,將MyCache實(shí)例注入CacheManager進(jìn)行統(tǒng)一管理。
#用戶自定義注解緩存(基于Spring注解)
以下是美團(tuán)酒店商家端使用自定義的緩存注解的方案
注解緩存的使用抒线,可以有效增強(qiáng)應(yīng)用代碼的可讀性班巩,同時(shí)統(tǒng)一管理緩存,提供較好的可擴(kuò)展性十兢,為此趣竣,酒店商家端在Spring注解緩存基礎(chǔ)上,自定義了適合自身業(yè)務(wù)特性的注解緩存旱物。
主要使用兩個(gè)標(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í)時(shí)間設(shè)置
@HotelCacheEvict主要針對方法配置,能夠根據(jù)一定的條件對緩存進(jìn)行清空同上
增加作用域的概念赡译,解決商家信息變更下仲吏,多重重要信息實(shí)時(shí)更新的問題。
圖13 域緩存處理圖
如圖13蝌焚,按舊的方案裹唆,當(dāng)cache0發(fā)送變化時(shí),為了保持信息的實(shí)時(shí)更新只洒,需要手動刪除cache1品腹、cache2、cache3等相關(guān)處的緩存數(shù)據(jù)红碑。增加域緩存概念舞吭,cache0、cache1析珊、cache2羡鸥、cache3是以賬號ID為基礎(chǔ),相互存在影響約束的集合體忠寻,我們作為一個(gè)域集合惧浴,增加域緩存處理,當(dāng)cache0發(fā)送變化時(shí)奕剃,整體的賬號ID domain域已發(fā)生更新衷旅,自動影響cache1捐腿、cache2、cache3等處的緩存數(shù)據(jù)柿顶。將相關(guān)聯(lián)邏輯緩存統(tǒng)一化茄袖,有效提升代碼可讀性,同時(shí)更好服務(wù)業(yè)務(wù)嘁锯,賬號重點(diǎn)信息能夠?qū)崟r(shí)變更刷新宪祥,相關(guān)服務(wù)響應(yīng)速度提升。
另外家乘,增加了cacheCondition緩存刷入前置判斷蝗羊,有效解決商家業(yè)務(wù)多重外部依賴場景下,業(yè)務(wù)降級有損服務(wù)下仁锯,業(yè)務(wù)數(shù)據(jù)一致性保證耀找,不因?yàn)榫彺娴脑黾佑绊憳I(yè)務(wù)的準(zhǔn)確性;自定義CacheManager緩存管理器业崖,可以有效兼容公共基礎(chǔ)組件Medis涯呻、Cellar相關(guān)服務(wù),在對應(yīng)用程序不做改動的情況下腻要,有效切換緩存方式复罐;同時(shí),統(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方法無法使用注解緩存實(shí)現(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í)效性要求很高時(shí)峦失,需要保證緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的保持一致,而且需要保證緩存節(jié)點(diǎn)和副本中的數(shù)據(jù)也保持一致术吗,不能出現(xiàn)差異現(xiàn)象尉辑。這就比較依賴緩存的過期和更新策略。一般會在數(shù)據(jù)發(fā)生更改的時(shí)较屿,主動更新緩存中的數(shù)據(jù)或者移除對應(yīng)的緩存隧魄。
#緩存并發(fā)問題
緩存過期后將嘗試從后端數(shù)據(jù)庫獲取數(shù)據(jù),這是一個(gè)看似合理的流程隘蝎。但是购啄,在高并發(fā)場景下,有可能多個(gè)請求并發(fā)的去從數(shù)據(jù)庫獲取數(shù)據(jù)嘱么,對后端數(shù)據(jù)庫造成極大的沖擊狮含,甚至導(dǎo)致 “雪崩”現(xiàn)象。此外曼振,當(dāng)某個(gè)緩存key在被更新時(shí)几迄,同時(shí)也可能被大量請求在獲取,這也會導(dǎo)致一致性的問題冰评。那如何避免類似問題呢? 我們會想到類似“鎖”的機(jī)制映胁,在緩存更新或者過期的情況下,先嘗試獲取到鎖甲雅,當(dāng)更新或者從數(shù)據(jù)庫獲取完成后再釋放鎖解孙,其他的請求只需要犧牲一定的等待時(shí)間,即可直接從緩存中繼續(xù)獲取數(shù)據(jù)抛人。
#緩存穿透問題
緩存穿透在有些地方也稱為“擊穿”妆距。很多朋友對緩存穿透的理解是:由于緩存故障或者緩存過期導(dǎo)致大量請求穿透到后端數(shù)據(jù)庫服務(wù)器,從而對數(shù)據(jù)庫造成巨大沖擊函匕。
這其實(shí)是一種誤解娱据。真正的緩存穿透應(yīng)該是這樣的:
在高并發(fā)場景下,如果某一個(gè)key被高并發(fā)訪問,沒有被命中中剩,出于對容錯(cuò)性考慮忌穿,會嘗試去從后端數(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)行緩存朴译,如果是集合,可以緩存一個(gè)空的集合(非null)属铁,如果是緩存單個(gè)對象眠寿,可以通過字段標(biāo)識來區(qū)分。這樣避免請求穿透到后端數(shù)據(jù)庫焦蘑。同時(shí)盯拱,也需要保證緩存數(shù)據(jù)的時(shí)效性。這種方式實(shí)現(xiàn)起來成本較低例嘱,比較適合命中不高狡逢,但可能被頻繁更新的數(shù)據(jù)。
單獨(dú)過濾處理
對所有可能對應(yīng)數(shù)據(jù)為空的key進(jìn)行統(tǒng)一的存放拼卵,并在請求前做攔截奢浑,這樣避免請求穿透到后端數(shù)據(jù)庫。這種方式實(shí)現(xiàn)起來相對復(fù)雜腋腮,比較適合命中不高殷费,但是更新不頻繁的數(shù)據(jù)。
#緩存抖動問題
緩存抖動可以看做是一種比“雪崩”更輕微的故障低葫,但是也會在一段時(shí)間內(nèi)對系統(tǒng)造成沖擊和性能影響详羡。一般是由于緩存節(jié)點(diǎn)故障導(dǎo)致。業(yè)內(nèi)推薦的做法是通過一致性Hash算法來解決嘿悬。這里不做過多闡述实柠。
#緩存雪崩問題
緩存雪崩就是指由于緩存的原因,導(dǎo)致大量請求到達(dá)后端數(shù)據(jù)庫善涨,從而導(dǎo)致數(shù)據(jù)庫崩潰窒盐,整個(gè)系統(tǒng)崩潰,發(fā)生災(zāi)難钢拧。導(dǎo)致這種現(xiàn)象的原因有很多種蟹漓,上面提到的“緩存并發(fā)”,“緩存穿透”源内,“緩存顛簸”等問題葡粒,其實(shí)都可能會導(dǎo)致緩存雪崩現(xiàn)象發(fā)生。這些問題也可能會被惡意攻擊者所利用。還有一種情況嗽交,例如某個(gè)時(shí)間點(diǎn)內(nèi)卿嘲,系統(tǒng)預(yù)加載的緩存周期性集中失效了,也可能會導(dǎo)致雪崩夫壁。為了避免這種周期性失效拾枣,可以通過設(shè)置不同的過期時(shí)間,來錯(cuò)開緩存過期盒让,從而避免緩存集中失效梅肤。
從應(yīng)用架構(gòu)角度,我們可以通過限流邑茄、降級姨蝴、熔斷等手段來降低影響,也可以通過多級緩存來避免這種災(zāi)難撩扒。
此外似扔,從整個(gè)研發(fā)體系流程的角度吨些,應(yīng)該加強(qiáng)壓力測試搓谆,盡量模擬真實(shí)場景,盡早的暴露問題從而防范豪墅。
#合理利用緩存
不合理使用緩存非但不能提高系統(tǒng)的性能泉手,還會成為系統(tǒng)的累贅,甚至風(fēng)險(xiǎn)偶器。
#頻繁修改的數(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í)間昆淡,一旦超過失效時(shí)間,就要從數(shù)據(jù)庫中重新加載刽严。因此要容忍一定時(shí)間的數(shù)據(jù)不一致昂灵,如賣家已經(jīng)編輯了商品屬性,但是需要過一段時(shí)間才能被買家看到。還有一種策略是數(shù)據(jù)更新立即更新緩存倔既,不過這也會帶來更多系統(tǒng)開銷和事務(wù)一致性問題恕曲。
#緩存可用性
緩存會承擔(dān)大部分?jǐn)?shù)據(jù)庫訪問壓力,數(shù)據(jù)庫已經(jīng)習(xí)慣了有緩存的日子渤涌,所以當(dāng)緩存服務(wù)崩潰時(shí)佩谣,數(shù)據(jù)庫會因?yàn)橥耆荒艹惺苋绱舜髩毫Χ礄C(jī),導(dǎo)致網(wǎng)站不可用实蓬。這種情況被稱作緩存雪崩茸俭,發(fā)生這種故障,甚至不能簡單地重啟緩存服務(wù)器和數(shù)據(jù)庫服務(wù)器來恢復(fù)安皱。
實(shí)踐中调鬓,有的網(wǎng)站通過緩存熱備份等手段提高緩存可用性:當(dāng)某臺緩存服務(wù)器宕機(jī)時(shí),將緩存訪問切換到熱備服務(wù)器上酌伊。但這種設(shè)計(jì)有違緩存的初衷腾窝,緩存根本就不應(yīng)該當(dāng)做一個(gè)可靠的數(shù)據(jù)源來使用。
通過分布式緩存服務(wù)器集群居砖,將緩存數(shù)據(jù)分布到集群多臺服務(wù)器上可在一定程度上改善緩存的可用性虹脯。當(dāng)一臺緩存服務(wù)器宕機(jī)時(shí),只有部分緩存數(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ù)篩選淘汰出來蔗草,這個(gè)過程需要花費(fèi)較長的時(shí)間咒彤。新系統(tǒng)的緩存系統(tǒng)如果沒有任何數(shù)據(jù),在重建緩存數(shù)據(jù)的過程中咒精,系統(tǒng)的性能和數(shù)據(jù)庫負(fù)載都不太好镶柱,那么最好在緩存系統(tǒng)啟動時(shí)就把熱點(diǎn)數(shù)據(jù)加載好,這個(gè)緩存預(yù)加載手段叫緩存預(yù)熱模叙。對于一些元數(shù)據(jù)如城市地名列表歇拆、類目信息,可以在啟動時(shí)加載數(shù)據(jù)庫中全部數(shù)據(jù)到緩存進(jìn)行預(yù)熱向楼。
#避免緩存穿透
如果因?yàn)椴磺‘?dāng)?shù)臉I(yè)務(wù)查吊、或者惡意攻擊持續(xù)高并發(fā)地請求某個(gè)不存在的數(shù)據(jù),由于緩存沒有保存該數(shù)據(jù)湖蜕,所有的請求都會落到數(shù)據(jù)庫上逻卖,會對數(shù)據(jù)庫造成壓力,甚至崩潰昭抒。一個(gè)簡單的對策是將不存在的數(shù)據(jù)也緩存起來(其value為null)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?更多技術(shù)資源敬請登錄www.ayshuju.com官網(wǎng)