緩存技術
下圖左側為客戶端直接調用存儲層的架構,右側為比較典型的緩存層+存儲層架構。
收益:
①加速讀寫:因為緩存通常都是全內存的,而存儲層通常讀寫性能不夠強悍(例如MySQL),通過緩存的使用可以有效地加速讀寫,優化用戶體驗。
②降低后端負載:幫助后端減少訪問量和復雜計算(例如很復雜的SQL語句),在很大程度降低了后端的負載。
成本:
①數據不一致性:緩存層和存儲層的數據存在着一定時間窗口的不一致性,時間窗口跟更新策略有關。
②代碼維護成本:加入緩存后,需要同時處理緩存層和存儲層的邏輯,增大了開發者維護代碼的成本。
③運維成本:以Redis Cluster為例,加入后無形中增加了運維成本。緩存的使用場景基本包含如下兩種:
①開銷大的復雜計算:以MySQL為例子,一些復雜的操作或者計算(例如大量聯表操作、一些分組計算),如果不加緩存,不但無法滿足高並發量,同時也會給MySQL帶來巨大的負擔。
②加速請求響應:即使查詢單條后端數據足夠快(例如select*from tablewhere id=),那么依然可以使用緩存,以Redis為例子,每秒可以完成數萬次讀寫,並且提供的批量操作可以優化整個IO鏈的響應時間。
緩存更新策略
緩存中的數據會和數據源中的真實數據有一段時間窗口的不一致,需要利用某些策略進行更新,下面會介紹幾種主要的緩存更新策略。
①LRU/LFU/FIFO算法剔除:剔除算法通常用於緩存使用量超過了預設的最大值時候,如何對現有的數據進行剔除。例如Redis使用maxmemory-policy這個配置作為內存最大值后對於數據的剔除策略。
②超時剔除:通過給緩存數據設置過期時間,讓其在過期時間后自動刪除,例如Redis提供的expire命令。如果業務可以容忍一段時間內,緩存層數據和存儲層數據不一致,那么可以為其設置過期時間。在數據過期后,再從真實數據源獲取數據,重新放到緩存並設置過期時間。例如一個視頻的描述信息,可以容忍幾分鍾內數據不一致,但是涉及交易方面的業務,后果可想而知。
③主動更新:應用方對於數據的一致性要求高,需要在真實數據更新后,立即更新緩存數據。例如可以利用消息系統或者其他方式通知緩存更新。(推薦的方法)

有兩個建議:
①低一致性業務建議配置最大內存和淘汰策略的方式使用。
②高一致性業務可以結合使用超時剔除和主動更新,
這樣即使主動更新出了問題,也能保證數據過期時間后刪除臟數據。
緩存的適用場景
-
對於數據實時性要求不高
對於一些經常訪問但是很少改變的數據,讀明顯多於寫,適用緩存就很有必要。比如一些網站配置項。 -
對於性能要求高
比如一些秒殺活動場景。
緩存三種模式
一般來說,緩存有以下三種模式:
- Cache Aside 更新模式
- Read/Write Through 更新模式
- Write Behind Caching 更新模式
Cache Aside 更新模式
這是最常用的緩存模式了,具體的流程是:
- 失效:應用程序先從 cache 取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。
- 命中:應用程序從 cache 中取數據,取到后返回。
- 更新:先把數據存到數據庫中,成功后,再讓緩存失效。
但是為了避免這種極端情況造成臟數據所產生的影響,我們還是要為緩存設置過期時間。
Read/Write Through 更新模式
在上面的 Cache Aside 更新模式中,應用代碼需要維護兩個數據存儲,一個是緩存(Cache),一個是數據庫(Repository)。而在Read/Write Through 更新模式中,應用程序只需要維護緩存,數據庫的維護工作由緩存代理了。
Read Through
Read Through 模式就是在查詢操作中更新緩存,也就是說,當緩存失效的時候,Cache Aside 模式是由調用方負責把數據加載入緩存,而 Read Through 則用緩存服務自己來加載。
Write Through
Write Through 模式和 Read Through 相仿,不過是在更新數據時發生。當有數據更新的時候,如果沒有命中緩存,直接更新數據庫,然后返回。如果命中了緩存,則更新緩存,然后由緩存自己更新數據庫(這是一個同步操作)。
Write Behind Caching 更新模式
Write Behind Caching 更新模式就是在更新數據的時候,只更新緩存,不更新數據庫,而我們的緩存會異步地批量更新數據庫。這個設計的好處就是直接操作內存速度快。因為異步,Write Behind Caching 更新模式還可以合並對同一個數據的多次操作到數據庫,所以性能的提高是相當可觀的。
緩存穿透
緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,通常出於容錯的考慮,如果從存儲層查不到數據則不寫入緩存層。
通常可以在程序中分別統計總調用數、緩存層命中數、存儲層命中數,如果發現大量存儲層空命中,可能就是出現了緩存穿透問題。造成緩存穿透的基本原因有兩個。第一,自身業務代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等造成大量空命中。下面我們來看一下如何解決緩存穿透問題。
1.緩存空對象:
如圖下所示,當第2步存儲層不命中后,仍然將空對象保留到緩存層中,之后再訪問這個數據將會從緩存中獲取,這樣就保護了后端數據源。存儲短期空對象防止緩存穿透 5min?

2.布隆過濾器攔截
如下圖所示,在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截。例如:一個推薦系統有4億個用戶id,每個小時算法工程師會根據每個用戶之前歷史行為計算出推薦數據放到存儲層中,但是最新的用戶由於沒有歷史行為,就會發生緩存穿透的行為,為此可以將所有推薦數據的用戶做成布隆過濾器。如果布隆過濾器認為該用戶id不存在,那么就不會訪問存儲層,在一定程度保護了存儲層。
也就是說定義了有效的id集合,在這個集合內的id才做查詢,這個集合存儲到緩存中去

3.存空對象和布隆過濾器方案對比

雪崩優化
由於緩存層承載着大量請求,有效地保護了存儲層,但是如果緩存層由於某些原因不能提供服務,於是所有的請求都會達到存儲層,存儲層的調用量會暴增,造成存儲層也會級聯宕機的情況。
預防和解決緩存雪崩問題,可以從以下三個方面進行着手:
- 保證緩存層服務高可用性。如果緩存層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,例如前面介紹過的Redis Sentinel和Redis Cluster都實現了高可用。
- 依賴隔離組件為后端限流並降級。在實際項目中,我們需要對重要的資源(例如Redis、MySQL、HBase、外部接口)都進行隔離,讓每種資源都單獨運行在自己的線程池中,即使個別資源出現了問題,對其他服務沒有影響。但是線程池如何管理,比如如何關閉資源池、開啟資源池、資源池閥值管理,這些做起來還是相當復雜的。
- 提前演練。在項目上線前,演練緩存層宕掉后,應用以及后端的負載情況以及可能出現的問題,在此基礎上做一些預案設。
熱點key重建優化
開發人員使用“緩存+過期時間”的策略既可以加速數據讀寫,又保證數據的定期更新,這種模式基本能夠滿足絕大部分需求。但是有兩個問題如果同時出現,可能就會對應用造成致命的危害:
當前key是一個熱點key(例如一個熱門的娛樂新聞),並發量非常大。
重建緩存不能在短時間完成,可能是一個復雜計算,例如復雜的SQL、多次IO、多個依賴等。在緩存失效的瞬間,有大量線程來重建緩存,造成后端負載加大,甚至可能會讓應用崩潰。
要解決這個問題也不是很復雜,但是不能為了解決這個問題給系統帶來更多的麻煩,所以需要制定如下目標:
減少重建緩存的次數
數據盡可能一致
較少的潛在危險
①互斥鎖:此方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據即可,整個過程如圖所示。

②永遠不過期
永遠不過期”包含兩層意思: 從緩存層面來看,確實沒有設置過期時間,所以不會出現熱點key過期后產生的問題,也就是“物理”不過期。 從功能層面來看,為每個value設置一個邏輯過期時間,當發現超過邏輯過期時間后,會使用單獨的線程去構建緩存。
從實戰看,此方法有效杜絕了熱點key產生的問題,但唯一不足的就是重構緩存期間,會出現數據不一致的情況,這取決於應用方是否容忍這種不一致。

兩種熱點key的解決方法

