緩存穿透、並發和雪崩那些事
緩存穿透、緩存並發和緩存雪崩是常見的由於並發量大而導致的緩存問題,本文講解其產生原因和解決方案。
緩存穿透通常是由惡意×××或者無意造成的;緩存並發是由設計不足造成的;緩存雪崩是由緩存同時失效造成的,三種問題都比較典型,也是難以防範和解決的。本節給出通用的解決方案,以供在緩存設計的過程中參考和使用。
1 緩存穿透
緩存穿透指的是使用不存在的key進行大量的高並發查詢,這導致緩存無法命中,每次請求都要穿透到後端數據庫系統進行查詢,使數據庫壓力過大,甚至使數據庫服務被壓死。
我們通常將空值緩存起來,再次接收到同樣的查詢請求時,若命中緩存並且值為空,就會直接返回,不會透傳到數據庫,避免緩存穿透。當然,有時惡意襲擊者可以猜到我們使用了這種方案,每次都會使用不同的參數來查詢,這就需要我們對輸入的參數進行過濾,例如,如果我們使用ID進行查詢,則可以對ID的格式進行分析,如果不符合產生ID的規則,就直接拒絕,或者在ID上放入時間信息,根據時間信息判斷ID是否合法,或者是否是我們曾經生成的ID,這樣可以攔截一定的無效請求。
當然,每個設計人員都應該對服務的可用性和健壯性負責,應該建設健壯的服務,讓我們的服務像不倒翁一樣,因此,我們需要對服務設計限流和熔斷等功能,請參考《分布式服務架構:原理、設計與實戰》中第1章關於微服務設計模式的內容。
2 緩存並發
緩存並發的問題通常發生在高並發的場景下,當一個緩存key過期時,因為訪問這個緩存key的請求量較大,多個請求同時發現緩存過期,因此多個請求會同時訪問數據庫來查詢最新數據,並且回寫緩存,這樣會造成應用和數據庫的負載增加,性能降低,由於並發較高,甚至會導致數據庫被壓死。
我們通常有3種方式來解決這個問題。
分布式鎖
使用分布式鎖,保證對於每個key同時只有一個線程去查詢後端服務,其他線程沒有獲得分布式鎖的權限,因此只需要等待即可。這種方式將高並發的壓力轉移到了分布式鎖,因此對分布式鎖的考驗很大。
本地鎖
與分布式鎖類似,我們通過本地鎖的方式來限制只有一個線程去數據庫中查詢數據,而其他線程只需等待,等前面的線程查詢到數據後再訪問緩存。但是,這種方法只能限制一個服務節點只有一個線程去數據庫中查詢,如果一個服務有多個節點,則還會有多個數據庫查詢操作,也就是說在節點數量較多的情況下並沒有完全解決緩存並發的問題。
軟過期
軟過期指對緩存中的數據設置失效時間,就是不使用緩存服務提供的過期時間,而是業務層在數據中存儲過期時間信息,由業務程序判斷是否過期並更新,在發現了數據即將過期時,將緩存的時效延長,程序可以派遣一個線程去數據庫中獲取最新的數據,其他線程這時看到延長了的過期時間,就會繼續使用舊數據,等派遣的線程獲取最新數據後再更新緩存。
也可以通過異步更新服務來更新設置軟過期的緩存,這樣應用層就不用關心緩存並發的問題了。
3 緩存雪崩
緩存雪崩指緩存服務器重啟或者大量緩存集中在某一個時間段內失效,給後端數據庫造成瞬時的負載升高的壓力,甚至壓垮數據庫的情況。
通常的解決辦法是對不同的數據使用不同的失效時間,甚至對相同的數據、不同的請求使用不同的失效時間,例如,我們要緩存user數據,會對每個用戶的數據設置不同的緩存過期時間,可以定義一個基礎時間,假設10秒,然後加上一個兩秒以內的隨機數,過期時間為10~12秒,就會避免緩存雪崩。
緩存穿透、並發和雪崩那些事