到P1結束,redis都已經是一個不錯的服務了,具體體現在快取應用程式需要的資料,甚至在記憶體爆滿的條件下還可以提供服務,似乎目的已經達成。但是實際上可能會遇到一些極端的情況,比如當機。如果redis當機了怎麼辦?目前所有的資料都儲存在記憶體當中,當機意為著失去所有快取的資料。前面說過我理解的真正需要用到redis的應用程式,它們一定有大量的查詢請求,可能造成後面躲著的資料庫服務扛不住。
其實寫到P1結束的時候,我突然意識到一個問題:在平時寫的系統中,redis服務掛了,那些請求redis的API會丟擲異常,這將導致明明還有資料庫可以承擔業務,但是使用者的請求會全部異常,為什麼沒人捕獲這個異常,繼續向資料庫請求呢?
@Service
class XxxService {
/* dependencies */
// Service Code
public Xxx xxxMethod () {
Xxx xxx = null;
if (/*redis opertions*/) {
/* xxx = ...*/
}
if (xxx == null) {
/* database opertions */
}
return xxx;
}
}
基於這個問題,去檢索了一下,結果在知乎上還真的有討論帖,裡面的回答都很有道理:分散式系統中Redis等快取系統當機應不應該影響正常應用系統執行? - 知乎 (zhihu.com)
已知如果連線失敗,則如果全域性捕獲了異常,則不會使系統當機。
這個標題就讓我更迷惑了:為什麼是該不該影響?難道還有第二個選項:redis當機了資料庫能用也不用?這似乎不合常理,但仔細一想,也不是沒有道理的,下面就來探討一下這個問題[1]。不過前提,實際業務場景仍是要具體問題具體分析,這是永恆不變的,這裡討論的也只是某些情況。
回到為什麼要加快取這個問題,效能瓶頸是一方面,我認為保護系統(高可用)是另外一個重要的原因:假如請求的數量資料庫可以支撐,那麼就用資料庫。但是如果大量的請求會擊垮資料庫呢?這就不止一個兩個介面會無法使用,可能整個系統都無法使用!絕不可以一拍腦袋就將壓力降下去,這是危險的。這個時候可以採取的方案有:
- 可以訪問資料庫,但開啟熔斷,使請求的數量維持在資料庫可以接收的範圍內;
- 放棄提供服務,抓緊搶修redis服務,使redis服務重新上線;
- 使用redis叢集,在redis當機時,讓備用節點頂替,自動恢復,不過這個是需要更多的成本(得加錢)
如果redis服務在系統當中很重要(架構師在技術選型時已經確認很重要),那麼對它的使用就要做好備用方案。
基於上面的問題,redis和資料庫之間有三個常見的概念,涉及到請求的資料redis沒有快取:
快取穿透
資料庫裡面沒有這個資料(假如說是10),自然快取也不存在,那麼每次訪問10的情況就都是:訪問10->快取沒有,去資料庫拿->資料庫沒有(沒有也沒辦法寫到快取)。每次訪問10都會造成資源浪費。
利用這,攻擊者就可以製造大量的請求去攻擊應用程式背後的db,比如攻擊者可以獲取user_id的格式,構造大量的根本不存在在資料庫的請求(這一步甚至可以構造合法的請求),那麼保護在資料庫前的快取的作用就消失了,拖慢系統的效能甚至讓資料庫當機。這就是快取穿透攻擊。
解決方案
-
對請求的引數進行校驗,不合法的引數直接忽略。不過上面也提到,這個是可以被繞過的,不過只要資料的格式隱藏的夠深讓人不好猜到,也能起到奇效。比如引數是123456789,但是如果12和89是加的字首和字尾,那麼只要攻擊者不知道這個規則,就可以防禦。
-
快取空值,如果請求在資料庫不存在,可以在快取中存入一個特殊值代表沒有,比如說“null”。這樣也有壞處,如果攻擊者構建了大量的不存在的鍵,則會給快取帶來很大壓力,可能會導致新的快取不能存入(noeviction記憶體淘汰策略),或頻繁的進行記憶體淘汰、刪除過期鍵時間長等等。
-
密碼學防禦,讀者可以參考快取穿透和密碼學防禦思路 - FreeBuf網路安全行業門戶。
-
布隆過濾器(著重介紹)
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位制向量和一系列隨機對映函式。布隆過濾器可以用於檢索一個元素是否在一個集合中。布隆過濾器可以告訴我們 “某樣東西一定不存在或者可能存在”,也就是說布隆過濾器說這個數不存在則一定不存,布隆過濾器說這個數存在可能不存在(即誤判)。
利用這個判斷是否存在的特點可以做很多有趣的事情。
- 解決Redis快取穿透問題(面試重點)
- 郵件過濾,使用布隆過濾器來做郵件黑名單過濾
- 對爬蟲網址進行過濾,爬過的不再爬
- 解決新聞推薦過的不再推薦(類似抖音刷過的往下滑動不再刷到)
- HBase\RocksDB\LevelDB等資料庫內建布隆過濾器,用於判斷資料是否存在,可以減少資料庫的IO請求
這張圖可以解釋布隆過濾器的原理,二進位制向量和雜湊函式。圖中是一個兩個位元組大小的向量,有兩個元素,三個雜湊函式,根據雜湊函式計算出來的值將對應的位置1。將來想要判斷值是否存在的時候,需要判斷這三個雜湊函式計算的值的在向量中的位是否都是1,如果都是,則證明這個值可能存在,否則一定不存在。
為什麼是可能存在,其實也好理解,如圖:
現在很湊巧,有一個請求查詢的鍵為k3,根據雜湊函式計算結果如圖所示,但是它在資料庫中實際是不存在的!(即符合快取穿透,快取和資料庫中都沒有這個鍵)理想情況下,我們希望布隆過濾器可以識別這是個不存在的鍵,可惜這一次攔截失敗了,布隆過濾器誤判了,認為這個鍵存在,也即出現了一次“快取穿透”。另外,當布隆過濾器中存的值越來越多,即置1的位越來越多,也會帶來誤判機率的增加。這從直覺上就可以解釋。
但是即便如此,布隆過濾器的效能依舊是很優秀的,可以用於億級資料的過濾,其效能可見一斑。此外,我們還可以透過數學計算及合理的配置來降低誤判的機率。如何做到?Java對布隆過濾器的依賴包裡有一個fpp(誤判率)的引數,透過設定可以讓誤判率儘可能接近這個值。實際上,底層也是透過增加位陣列的大小和使用更多的雜湊函式來降低誤判率的,不過這也會帶來時空複雜度的消耗。增加位陣列的長度自然可以讓設定的位更加分散,增加雜湊函式可以讓鍵的匹配更精準,但是過多的雜湊函式也會讓一個鍵置1的位變多,可能也會增加誤判率。實際開發中可以選擇使用提供的布隆過濾器的API來控制誤判率,如果想要自己實現,就要合理地平衡雜湊函式的個數、陣列長度和資料量之間的選值。至於個數應該怎麼選,我沒有很好的頭緒,可能數學推導和統計實踐可以計算出一個好的引數組合,如同JDK中HashMap的平衡因子那樣,從實踐中得到一個效能較好的數值。
原文連結:https://blog.csdn.net/qq_41125219/article/details/119982158
快取擊穿
快取擊穿,從名字上就感覺比快取穿透要嚴重得多。這個名詞針對的是熱點key的場景。眾所周知,redis的key可以設定過期時間,但是在大多數專案中,都會存在“熱點資料”,即併發訪問量很大的資料。快取擊穿中,我們指的都是單個熱點key突然失效,導致大量的請求一瞬間全部擊打在資料庫上,導致資料庫瞬間訪問量激增,瞬時壓力暴增甚至崩潰。
解決方案
根據業務的場景可以選擇合適的解決方案,如熱點資料永不過期、控制訪問量,如熔斷、互斥訪問資料庫(不好,低效能)。
快取雪崩
快取雪崩的名字又比快取擊穿恐怖一些,這次指的是大量的熱點key幾乎同一時間過期,依然是資料庫瞬時壓力激增崩潰,甚至重啟以後還可能會被打到崩潰的嚴重現象,這叫快取雪崩。還蠻好理解記憶的。
解決方案
解決快取雪崩,主要的矛盾是不能讓大量的熱點資料在同一時間過期。那麼,可以給每一個熱點資料的過期時間上加上一些隨機的值,讓過期時間分散開。同時,熔斷策略依然是有效的。還有為了防止因為redis服務當機導致瞬時壓力增加,還需要為redis提高容災能力,如構建redis叢集。當然,為資料庫提高容災能力是治標不治本的策略。
資料庫中的快取
詳解MySQL中的Buffer Pool,深入底層帶你搞懂它!-騰訊雲開發者社群-騰訊雲 (tencent.com)
這涉及到一個大家在準備後端的面試前一般會涉及到的概念——快取擊穿。這個概念一般與另外兩個概念——快取穿透、快取雪崩有關聯,為了不影響行文的連續性,我將會在P2的後面再來著重介紹這三個概念,現在扯回來 ↩︎