架構設計很重要的一部分內容便是(shì)如何(hé)滿足業務的(de)性能訴求,在性能優化上,利用(yòng)緩存的案例非常多,其本質都是為了彌補內存高讀寫與(yǔ)磁盤慢讀寫之(zhī)間的(de)鴻溝。
一(yī)個(gè)係統的長(zhǎng)期建設,所采用的架(jià)構(gòu)肯定不是一成不變的,會隨著業務不斷變化(huà)而進行對應調整(zhěng),以下(xià)便是在係統建設的不同階段逐步引入不同級別緩存的過程。
1.0時代,業務量(liàng)小,應(yīng)用直接通過數據庫進(jìn)行數據讀寫。

2.0時代,業務量(liàng)有(yǒu)了一定的增長,數據庫出現性能瓶頸(jǐng),使用分布式緩(huǎn)存進行熱點數據的訪問加速。

3.0時代,業務量開始暴增,更高頻的熱點數據訪問(wèn),因為網絡io、序列化操作等帶來了性能壓力,采用本地緩存再次進行加速,減少網絡(luò)請求同時(shí)還省去了序列化的開銷。

目前分布式緩(huǎn)存、本地緩存相關技術棧(zhàn)都非常多,分(fèn)布式緩存成熟的(de)有redis、memcached等,目(mù)前使用最廣泛的還是redis,本地緩存常用的有(yǒu)ConcurrentHashMap、Guava、caffeine、ehcache、spring cache等,其中spring cache因(yīn)為整合簡單,支持緩存組件廣,使用方(fāng)便,使用越來越廣(guǎng)泛(fàn)。
從具體緩存實現(xiàn)來看,絕大部分情況都是采用旁路緩存,通過應用程序更新緩存,緩存組件不直接操作數(shù)據源。
分(fèn)布式架構情況下,多(duō)個微服務節點(diǎn),必須通過會話共享,才能保證一次登錄,在(zài)分發請求(qiú)後每個服務節點的登錄狀態一致,所以引入分布式緩存進行會話共享。

業務應用上經常要控(kòng)製在(zài)一定時間內對某個數據對象的(de)操作次數,在分布式應(yīng)用情況下,對同一數據對象計數很容易(yì)產生重複計算,數量失控的情況,而使用分布式計數器功能特性可以很好規避。
典型應用場景:防(fáng)止刷單、限製登錄次數、活(huó)動限額等。
本地鎖隻能(néng)鎖住當(dāng)前進程,已經無法滿足當前的係統設計需求(qiú)。分布式鎖支撐同時去一個(gè)地方“鎖占”,如果占到,就執行邏(luó)輯。否則就必須(xū)等待,直到釋放鎖,等待可以自旋的方式。
典型應用場景:在購物車或者提交訂單情況下,係統(tǒng)如(rú)何防止重複提(tí)交。
緩存的一致性就是指緩存中的數據是否(fǒu)和目標存儲中的數據是一樣的,也(yě)就是說緩存中已經修改的(de)數據是否已經保存到了物理(lǐ)存儲中,物理存儲中已經被修的內容,是否與緩存的內容是一樣的。在多級緩存情況下,物理存儲與多級緩存之間的內容也需保持(chí)一樣。
項目上經常聽見(jiàn)這類聲音,緩存數據與數據庫數據不一致,緩存刷不成功,部分節點數據不一致等等,案例非(fēi)常多,比如:
某(mǒu)集團項目經常出現(xiàn)銷售(shòu)品屬性、產品屬性緩(huǎn)存(cún)與數據(jù)庫不一致的問題,最終確定是因為刷新緩存(cún)讀取的數據源是外部接口,外(wài)部接口偶爾失(shī)敗,導(dǎo)致(zhì)取到結果為空,將空對(duì)象寫入了(le)緩存(cún)中。
某省份項目經常出(chū)現通過清空緩存刷新時,一部分節(jiē)點沒(méi)有執行(háng)成功,後麵定位到是本地緩存刷新線程一定概率發(fā)生異常終止導致,程序沒有捕(bǔ)獲異常,導致清空失敗,沒有加載到最新數據。
某項目使用zk廣播的方式(shì)刷新本地緩存,由於應用FGC很頻繁,刷新緩存時部分節點一定概率出現FGC,導致zk通知失敗,沒有(yǒu)進行結果處理並重試,造成節點間本地緩存不一致。
旁路緩(huǎn)存模式(Cache Aside Pattern)問題分析問(wèn)題前(qián),我們先了解下該模式。
寫(Write)
讀(Read)

以上模式(shì)基本可(kě)以解決絕大部分場景的使用情況,但是在更新緩存時,因為數據庫操作(zuò)效率肯(kěn)定比緩存操作效率慢,比如更新數(shù)據的查詢語句性能較差,或者並發(fā)情況下出現A線程獲取數據後寫入緩存,B線程同(tóng)時在更新數據並刪除緩存(cún),若B線程完成時間早於(yú)A線程,那麽最終緩存將會是A線程讀取的舊數據(jù)。這種情(qíng)況下,為了保證強一致(zhì)性,可以采用延(yán)遲雙刪,刪除緩(huǎn)存線程執行完以後,再增加一個刪除命令,等待一定時間進行二次刪除。這(zhè)樣(yàng)會增加複雜度,具體要看業務容(róng)忍度。
本地緩存(cún)與分布式緩存不一致(zhì)問題
分布式緩存一般(bān)隻有一個數據源(yuán),所以一致性容易保證,但是(shì)本地緩存分散到各個應用節(jiē)點中,在更新分布式緩存(cún)同時,如何保證所有(yǒu)應用節點(diǎn)本地(dì)緩存都(dōu)能更新(xīn),一般(bān)有如下方案:
① zk廣(guǎng)播通知,事務執行節點在刷新分布式緩存以後,發起一(yī)條通(tōng)知通過zk廣(guǎng)播通知的方(fāng)式,通知給所有消費節(jiē)點,消費節點收到消息(xī)以後(hòu),執行對應邏輯刷新本(běn)地緩存。該方式存在一定缺陷,如果期間服務異常,或者服務進程在做Crash,可能收(shōu)到通知(zhī)後處理失敗,失敗後沒有重試機製。
② 消(xiāo)息發布/訂閱,redis具備消息發布/訂閱能(néng)力,事務執行節點寫入一條消息,各應用節點進行訂閱消費,接收消息後進行刷新邏輯(jí)執行,redis消(xiāo)息滿足了(le)絕大部分場(chǎng)景,但是如果為了(le)提高穩定性可(kě)以采用消息中間件代替。
緩存穿透指查詢一個一(yī)定不存在的數據,由於緩存沒命中,將去查數據庫,但是數據庫也無此記錄,這將(jiāng)導致每次請(qǐng)求都打到(dào)了數據(jù)庫,失去了緩存的意義(yì)。緩存穿透可能會使數據庫負載加大,由於數據庫在高並發下性能較差,甚至可能造成數據庫宕機,該場景在項目上(shàng)經常碰見。

某省份項目由於(yú)訂單處理時,從緩存中沒有讀取到規(guī)則配置數據,執行了從數(shù)據庫加(jiā)載全量配置,數據庫中也沒有對應配置數據,導致每次請求都會全量加載一遍規(guī)則配置數據,嚴重影響了訂單處理性能(néng),對數據庫性能也產生(shēng)了(le)較大影(yǐng)響。
通常可以在程序中(zhōng)統計總調用數、緩存層命中(zhōng)數、如果同一個Key的緩存命中率很低,可(kě)能就是出現了緩存穿透問題。
一般(bān)可以通過如下方案解決:
設置NullObject,訪問數據庫miss時,設置(zhì)一個空對象到緩存中,防止下次繼續(xù)請求數據庫。該方法(fǎ)實現簡單,但也存在一定(dìng)的缺(quē)陷,需要業務側自行評估。一是,如果miss數據很多,大量key寫入緩存,會占用內存空間。二是,數據一致性問題,如果NullObject設置以後,數據庫新增(zēng)了數據,無法自動(dòng)更新到緩存,需要業務側額外(wài)實現邏輯進行更新。
布隆過濾器,其實就是(shì)在訪問緩存層(céng)和數據庫之前,將存(cún)在的key用布(bù)隆(lóng)過濾器提前保存起來,做第一層攔截(當收到一個對key請求時先用布隆過濾器驗證是key否存(cún)在,如果存在再進入緩存層、存(cún)儲層)。布隆過濾器優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是(shì)有一定的誤識別(bié)率和刪除困難。
緩(huǎn)存充當了數據庫訪問的保護層,防止數據(jù)庫訪問壓力過大而宕機,但是如果緩存(cún)出現宕機,或(huò)大批量同時失效(xiào),大(dà)量請求打到數據庫,導致數(shù)據庫負荷突然拉高,壓力過大(dà)而導致雪崩。(該問題(tí)在項目上(shàng)出現(xiàn)的概率也不低,一(yī)般是緩存時效時間設置不合理導致(zhì)。)
某省份項目由於樓層(céng)數據緩存采用固定有效期,一(yī)次版本(běn)升級以(yǐ)後,緩存數據全量做了一次更新,在失效期到了以後,所有緩存失效,頁麵請求同一時間點全部打到數據庫加載緩存數據,導致數據庫壓力瞬間過大(dà),影響整個係統性能響應。
針(zhēn)對緩存雪崩,通過如(rú)下(xià)方案可規避:
緩存服務搭建(jiàn)采用高可(kě)用模(mó)式(shì),防止單節點宕機導致整(zhěng)個服務受影(yǐng)響。
采用多級緩(huǎn)存,本地進程作(zuò)為一級緩存,redis作為二級緩存,不同級別的緩存設置的超時時間不同,即使某級緩存過期了,也有其他級別緩存兜(dōu)底。
緩存的過期時間使用固定值+隨(suí)機值,盡量讓不同的key的過期(qī)時間不同。
序列化主要的用處就(jiù)是在傳遞和保存對象的時候(hòu),保證對象的完(wán)整(zhěng)性(xìng)和可傳(chuán)遞性。序列化是把對象轉換(huàn)成有序字節(jiē)流,以便在網絡上傳輸。反序列化便是根據網絡傳輸(shū)字節流中所保存(cún)的對象狀態及描述信息,通過反序列化重建對象(xiàng)。
在保存對象和重建對象過程中,不同序列化工具對描(miáo)述信息差異容忍度(dù)不一致、性能不一致,容易出現問題(tí)。
某產品緩存序列化工具(jù)采用kryo,某次版本修改了(le)對象屬(shǔ)性,版本發布時忘記清理已有緩存,導致存量數據在(zài)反序列化時全部報(bào)錯,造(zào)成生產故(gù)障。
某門戶(hù)產品,由於(yú)會話用戶信息使(shǐ)用(yòng)kryo存儲,多係統(tǒng)共用會話緩存,某次門戶升級增加了用戶屬性,未(wèi)及時通知下遊係統升級,導致升級(jí)後下遊係統出(chū)現用戶(hù)信息反序列失敗的故障。
分布式緩存訪問的,涉及實體對象,必須通過序列、反序列化來進行存取,實體對象一般(bān)映射數據庫模型,在業務需求變更時(shí),模(mó)型字段發生了變化,對於已經寫入的緩存,部分序列化工具會存在(zài)反序列失敗的問(wèn)題。同(tóng)時不同序列化工具在性能上麵也存在一定差異,業務側根據各自情況進行選擇使用。
所謂熱key問題就是,突然有幾十萬的請求去訪問redis上的某個特定key。那麽,這樣會造成流(liú)量過於集中,達到物理(lǐ)網(wǎng)卡上(shàng)限,從而導致這(zhè)台redis的服務器宕機。
某項(xiàng)目一個靜態配置數據放(fàng)在了redis緩存,業(yè)務規(guī)則處理(lǐ)中存在大循(xún)環調用,一次業務處(chù)理重複獲取了幾百次(cì)靜態數據(jù),業務量大時導致該key的訪問非(fēi)常頻繁。
將每(měi)次業務訪問的key進(jìn)行拆分,避免總是訪(fǎng)問同一個key。
對需要頻繁訪問的key進(jìn)行本地緩存,本地緩存數據(jù)可以通過定時策略進行更(gèng)新。
優化業務處理邏輯,減少無效交(jiāo)互訪(fǎng)問:例如一個服務裏麵有N次訪問某個配置的邏輯,那麽在服務(wù)邏輯開始時從緩存裏麵取一次(cì)配置就好,避(bì)免單一(yī)服務大量重複緩存(cún)交(jiāo)互(hù)。
在分布式高並發的條件下,如果有個線程獲(huò)得鎖的同時,還沒有來得及(jí)去釋放鎖,就因為係統故(gù)障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無(wú)法獲得鎖,造成死鎖。
某采(cǎi)購(gòu)項目,為了避(bì)免訂單、購物車重複提交(jiāo),在服務入口處,使用SETNX獲取分布(bù)式鎖,在(zài)服務出口進行分布式鎖解鎖操作,由於沒有考慮執行異常的情況,異常(cháng)後沒有執(zhí)行(háng)解鎖,導致鎖一直無法釋放。
項目上使用分布式鎖經常出現死鎖,或者鎖失效的情(qíng)況,基本都是在(zài)使用原理及業務場景匹配上存在不清晰所導致,一把穩定的分布式鎖一般具有如下特(tè)征:
互斥性(xìng), 任意時刻,隻有一個客戶端能持有鎖。
鎖超時釋放,持有(yǒu)鎖超時,可以釋放,防止不必要的資源浪費,也可以防止死鎖。
可重入性,一個線程如果獲(huò)取了(le)鎖之後(hòu),可以再次對其請求加鎖。
高性能和高可用(yòng),加(jiā)鎖和解鎖需要(yào)開銷盡可能低,同時也要保證高可用,避(bì)免分布式鎖失效(xiào)。
安全性,鎖隻能被持(chí)有的客戶端刪除,不能被其他客戶端刪除(chú)。
從實現上,一般有如下幾(jǐ)種方案:
SETNX + EXPIRE,SETNX獲(huò)取鎖以後(hòu),再使用EXPIRE進行過期時間(jiān)設置,防止客戶端崩潰後,鎖無法釋放,但是這樣存在問題,SETNX + EXPIRE並非原子操作,如果發送EXPIRE時正(zhèng)好應用Crash,一樣(yàng)會導致(zhì)死鎖。
使用Lua腳本(包含SETNX + EXPIRE兩條指令),通過Lua腳(jiǎo)本執行,保證了兩(liǎng)條(tiáo)指令的原子(zǐ)性(xìng)。但還是存在一定風險,比如A線程鎖到期釋放了,但是業務邏(luó)輯還沒執行完,導致B線程又重新(xīn)獲取了鎖,最後(hòu)B線程把A線程的(de)鎖給刪除。
使用Lua腳本(SET EX PX NX + 校驗(yàn)唯(wéi)一隨機值(zhí),再釋放(fàng)鎖),采用set命令,結合擴(kuò)展參數,同時通過唯一隨機值校驗,解決了鎖被誤刪的情況,但是這樣還是解決(jué)不了鎖過期釋放而業務沒有(yǒu)執行(háng)完的問(wèn)題。
開源框架Redisson,通過開啟守護線程,每隔一段時間檢查鎖是否(fǒu)還存在(zài),存在則對鎖的過期(qī)時間延長(zhǎng),防(fáng)止鎖(suǒ)過期提前釋放。但是該方案在集群模式下,會存在(zài)同步(bù)延時的問(wèn)題。
實現的分布式鎖Redlock,由Redis作(zuò)者antirez提出一種(zhǒng)高級的分(fèn)布式鎖算法,按順序向多個master節點獲取鎖,按一定(dìng)比例成功率進行計(jì)算(suàn)。
緩存使用場景及使(shǐ)用中可能遇到的問題,遠不止上麵列(liè)出來的內容,需要注(zhù)意的點非常多,所以我們在引入時,或者用(yòng)到其中的特性要從整體去看,了解相關原理、適用場景、注意事項,多方麵規避(bì)風險,總結下來,在緩存使用(yòng)過程中,應該要從以下(xià)方(fāng)麵進行考(kǎo)慮:
設計(jì)上確(què)保穩定、安全、高性能
根據業務(wù)特性、體量等,合理選擇緩存產品
根據緩(huǎn)存產品特性,結合業務(wù)對穩定性、性能等(děng)方麵的(de)要求(qiú),對部署架構進行規劃評估
結合安全管控要求,在賬號管理、網絡、災備(bèi)方麵進行安(ān)全設計
結合業務容忍度,在一致性(xìng)、健壯性上進行分析考慮,同時要規避(bì)一些風(fēng)險命令的使用
做好(hǎo)緩存數據生命(mìng)周期的規劃,不同業務數據設計合適的生命周期
做好key、value設計,易維護,合理選擇數據類型
研發上注意關鍵配置,熟悉產品(pǐn)特性
運維上確保快速響應、勤(qín)總結
根據不同的緩存(cún)產品,選擇(zé)合適的監控工具(jù),熟悉其監控工具
熟悉(xī)緩存產品(pǐn)相關監控指標(biāo)
勤於總結相關運維問題(tí),沉澱知識庫,不斷提升運維質(zhì)量及效率