架構設計很重要的一部分內容便是如何滿足業務的性能訴求,在性能優化上,利用緩存的案例非常多,其本質都是為了彌補內存高讀寫與磁盤慢讀寫之間的鴻溝。
一個系統的長期建設,所采用的架構肯定不是一成不變的,會隨著業務不斷變化而進行對應調整,以下便是在系統建設的不同階段逐步引入不同級別緩存的過程。
1.0時代,業務量小,應用直接通過數據庫進行數據讀寫。

2.0時代,業務量有了一定的增長,數據庫出現性能瓶頸,使用分布式緩存進行熱點數據的訪問加速。

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

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

業務應用上經常要控制在一定時間內對某個數據對象的操作次數,在分布式應用情況下,對同一數據對象計數很容易產生重復計算,數量失控的情況,而使用分布式計數器功能特性可以很好規避。
典型應用場景:防止刷單、限制登錄次數、活動限額等。
本地鎖只能鎖住當前進程,已經無法滿足當前的系統設計需求。分布式鎖支撐同時去一個地方“鎖占”,如果占到,就執行邏輯。否則就必須等待,直到釋放鎖,等待可以自旋的方式。
典型應用場景:在購物車或者提交訂單情況下,系統如何防止重復提交。
緩存的一致性就是指緩存中的數據是否和目標存儲中的數據是一樣的,也就是說緩存中已經修改的數據是否已經保存到了物理存儲中,物理存儲中已經被修的內容,是否與緩存的內容是一樣的。在多級緩存情況下,物理存儲與多級緩存之間的內容也需保持一樣。
項目上經常聽見這類聲音,緩存數據與數據庫數據不一致,緩存刷不成功,部分節點數據不一致等等,案例非常多,比如:
某集團項目經常出現銷售品屬性、產品屬性緩存與數據庫不一致的問題,最終確定是因為刷新緩存讀取的數據源是外部接口,外部接口偶爾失敗,導致取到結果為空,將空對象寫入了緩存中。
某省份項目經常出現通過清空緩存刷新時,一部分節點沒有執行成功,后面定位到是本地緩存刷新線程一定概率發生異常終止導致,程序沒有捕獲異常,導致清空失敗,沒有加載到最新數據。
某項目使用zk廣播的方式刷新本地緩存,由于應用FGC很頻繁,刷新緩存時部分節點一定概率出現FGC,導致zk通知失敗,沒有進行結果處理并重試,造成節點間本地緩存不一致。
旁路緩存模式(Cache Aside Pattern)問題分析問題前,我們先了解下該模式。
寫(Write)
讀(Read)

以上模式基本可以解決絕大部分場景的使用情況,但是在更新緩存時,因為數據庫操作效率肯定比緩存操作效率慢,比如更新數據的查詢語句性能較差,或者并發情況下出現A線程獲取數據后寫入緩存,B線程同時在更新數據并刪除緩存,若B線程完成時間早于A線程,那么最終緩存將會是A線程讀取的舊數據。這種情況下,為了保證強一致性,可以采用延遲雙刪,刪除緩存線程執行完以后,再增加一個刪除命令,等待一定時間進行二次刪除。這樣會增加復雜度,具體要看業務容忍度。
本地緩存與分布式緩存不一致問題
分布式緩存一般只有一個數據源,所以一致性容易保證,但是本地緩存分散到各個應用節點中,在更新分布式緩存同時,如何保證所有應用節點本地緩存都能更新,一般有如下方案:
① zk廣播通知,事務執行節點在刷新分布式緩存以后,發起一條通知通過zk廣播通知的方式,通知給所有消費節點,消費節點收到消息以后,執行對應邏輯刷新本地緩存。該方式存在一定缺陷,如果期間服務異常,或者服務進程在做Crash,可能收到通知后處理失敗,失敗后沒有重試機制。
② 消息發布/訂閱,redis具備消息發布/訂閱能力,事務執行節點寫入一條消息,各應用節點進行訂閱消費,接收消息后進行刷新邏輯執行,redis消息滿足了絕大部分場景,但是如果為了提高穩定性可以采用消息中間件代替。
緩存穿透指查詢一個一定不存在的數據,由于緩存沒命中,將去查數據庫,但是數據庫也無此記錄,這將導致每次請求都打到了數據庫,失去了緩存的意義。緩存穿透可能會使數據庫負載加大,由于數據庫在高并發下性能較差,甚至可能造成數據庫宕機,該場景在項目上經常碰見。

某省份項目由于訂單處理時,從緩存中沒有讀取到規則配置數據,執行了從數據庫加載全量配置,數據庫中也沒有對應配置數據,導致每次請求都會全量加載一遍規則配置數據,嚴重影響了訂單處理性能,對數據庫性能也產生了較大影響。
通??梢栽诔绦蛑薪y計總調用數、緩存層命中數、如果同一個Key的緩存命中率很低,可能就是出現了緩存穿透問題。
一般可以通過如下方案解決:
設置NullObject,訪問數據庫miss時,設置一個空對象到緩存中,防止下次繼續請求數據庫。該方法實現簡單,但也存在一定的缺陷,需要業務側自行評估。一是,如果miss數據很多,大量key寫入緩存,會占用內存空間。二是,數據一致性問題,如果NullObject設置以后,數據庫新增了數據,無法自動更新到緩存,需要業務側額外實現邏輯進行更新。
布隆過濾器,其實就是在訪問緩存層和數據庫之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截(當收到一個對key請求時先用布隆過濾器驗證是key否存在,如果存在再進入緩存層、存儲層)。布隆過濾器優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。
緩存充當了數據庫訪問的保護層,防止數據庫訪問壓力過大而宕機,但是如果緩存出現宕機,或大批量同時失效,大量請求打到數據庫,導致數據庫負荷突然拉高,壓力過大而導致雪崩。(該問題在項目上出現的概率也不低,一般是緩存時效時間設置不合理導致。)
某省份項目由于樓層數據緩存采用固定有效期,一次版本升級以后,緩存數據全量做了一次更新,在失效期到了以后,所有緩存失效,頁面請求同一時間點全部打到數據庫加載緩存數據,導致數據庫壓力瞬間過大,影響整個系統性能響應。
針對緩存雪崩,通過如下方案可規避:
緩存服務搭建采用高可用模式,防止單節點宕機導致整個服務受影響。
采用多級緩存,本地進程作為一級緩存,redis作為二級緩存,不同級別的緩存設置的超時時間不同,即使某級緩存過期了,也有其他級別緩存兜底。
緩存的過期時間使用固定值+隨機值,盡量讓不同的key的過期時間不同。
序列化主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序字節流,以便在網絡上傳輸。反序列化便是根據網絡傳輸字節流中所保存的對象狀態及描述信息,通過反序列化重建對象。
在保存對象和重建對象過程中,不同序列化工具對描述信息差異容忍度不一致、性能不一致,容易出現問題。
分布式緩存訪問的,涉及實體對象,必須通過序列、反序列化來進行存取,實體對象一般映射數據庫模型,在業務需求變更時,模型字段發生了變化,對于已經寫入的緩存,部分序列化工具會存在反序列失敗的問題。同時不同序列化工具在性能上面也存在一定差異,業務側根據各自情況進行選擇使用。
所謂熱key問題就是,突然有幾十萬的請求去訪問redis上的某個特定key。那么,這樣會造成流量過于集中,達到物理網卡上限,從而導致這臺redis的服務器宕機。
某項目一個靜態配置數據放在了redis緩存,業務規則處理中存在大循環調用,一次業務處理重復獲取了幾百次靜態數據,業務量大時導致該key的訪問非常頻繁。
將每次業務訪問的key進行拆分,避免總是訪問同一個key。
對需要頻繁訪問的key進行本地緩存,本地緩存數據可以通過定時策略進行更新。
優化業務處理邏輯,減少無效交互訪問:例如一個服務里面有N次訪問某個配置的邏輯,那么在服務邏輯開始時從緩存里面取一次配置就好,避免單一服務大量重復緩存交互。
在分布式高并發的條件下,如果有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。
某采購項目,為了避免訂單、購物車重復提交,在服務入口處,使用SETNX獲取分布式鎖,在服務出口進行分布式鎖解鎖操作,由于沒有考慮執行異常的情況,異常后沒有執行解鎖,導致鎖一直無法釋放。
項目上使用分布式鎖經常出現死鎖,或者鎖失效的情況,基本都是在使用原理及業務場景匹配上存在不清晰所導致,一把穩定的分布式鎖一般具有如下特征:
鎖超時釋放,持有鎖超時,可以釋放,防止不必要的資源浪費,也可以防止死鎖。
可重入性,一個線程如果獲取了鎖之后,可以再次對其請求加鎖。
高性能和高可用,加鎖和解鎖需要開銷盡可能低,同時也要保證高可用,避免分布式鎖失效。
安全性,鎖只能被持有的客戶端刪除,不能被其他客戶端刪除。
從實現上,一般有如下幾種方案:
SETNX + EXPIRE,SETNX獲取鎖以后,再使用EXPIRE進行過期時間設置,防止客戶端崩潰后,鎖無法釋放,但是這樣存在問題,SETNX + EXPIRE并非原子操作,如果發送EXPIRE時正好應用Crash,一樣會導致死鎖。
使用Lua腳本(包含SETNX + EXPIRE兩條指令),通過Lua腳本執行,保證了兩條指令的原子性。但還是存在一定風險,比如A線程鎖到期釋放了,但是業務邏輯還沒執行完,導致B線程又重新獲取了鎖,最后B線程把A線程的鎖給刪除。
使用Lua腳本(SET EX PX NX + 校驗唯一隨機值,再釋放鎖),采用set命令,結合擴展參數,同時通過唯一隨機值校驗,解決了鎖被誤刪的情況,但是這樣還是解決不了鎖過期釋放而業務沒有執行完的問題。
開源框架Redisson,通過開啟守護線程,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。但是該方案在集群模式下,會存在同步延時的問題。
實現的分布式鎖Redlock,由Redis作者antirez提出一種高級的分布式鎖算法,按順序向多個master節點獲取鎖,按一定比例成功率進行計算。
緩存使用場景及使用中可能遇到的問題,遠不止上面列出來的內容,需要注意的點非常多,所以我們在引入時,或者用到其中的特性要從整體去看,了解相關原理、適用場景、注意事項,多方面規避風險,總結下來,在緩存使用過程中,應該要從以下方面進行考慮:
設計上確保穩定、安全、高性能
根據業務特性、體量等,合理選擇緩存產品
根據緩存產品特性,結合業務對穩定性、性能等方面的要求,對部署架構進行規劃評估
結合安全管控要求,在賬號管理、網絡、災備方面進行安全設計
結合業務容忍度,在一致性、健壯性上進行分析考慮,同時要規避一些風險命令的使用
做好緩存數據生命周期的規劃,不同業務數據設計合適的生命周期
做好key、value設計,易維護,合理選擇數據類型
研發上注意關鍵配置,熟悉產品特性
運維上確??焖夙憫?、勤總結