快取穿透、快取雪崩、快取擊穿

MaChao發表於2020-10-28

什麼是快取?

快取,就是資料交換的緩衝區,針對服務物件的不同(本質就是不同的硬體)都可以構建快取。

目的是,把讀寫速度慢的介質的資料儲存在讀寫速度快的介質中,從而提高讀寫速度,減少時間消耗。 例如:

  • CPU 快取記憶體 :快取記憶體的讀寫速度遠高於記憶體。
    • CPU 讀資料時,如果在快取記憶體中找到所需資料,就不需要讀記憶體
    • CPU 寫資料時,先寫到快取記憶體,再回寫到記憶體。
  • 磁碟快取:磁碟快取其實就把常用的磁碟資料儲存在記憶體中,記憶體讀寫速度也是遠高於磁碟的。
    • 讀資料時,從記憶體讀取。
    • 寫資料時,可先寫到記憶體,定時或定量回寫到磁碟,或者是同步回寫。

為什麼要用快取?

使用快取的目的,就是提升讀寫效能。而實際業務場景下,更多的是為了提升讀效能,帶來更好的效能,更高的併發量。

日常業務中,我們使用比較多的資料庫是 MySQL,快取是 Redis 。Redis 比 MySQL 的讀寫效能好很多。那麼,我們將 MySQL 的熱點資料,快取到 Redis 中,提升讀取效能,也減小 MySQL 的讀取壓力。例如說:

  • 論壇帖子的訪問頻率比較高,且要實時更新閱讀量,使用 Redis 記錄帖子的閱讀量,可以提升效能和併發。
  • 商品資訊,資料更新的頻率不高,但是讀取的頻率很高,特別是熱門商品。

分散式快取系統面臨的問題

image

快取與資料庫雙寫不一致

一般來說,如果允許快取可以稍微的跟資料庫偶爾有不一致的情況,也就是說如果你的系統不是嚴格要求 “快取+資料庫” 必須保持一致性的話,最好不要做這個方案,即:讀請求和寫請求序列化,串到一個記憶體佇列裡去。

序列化可以保證一定不會出現不一致的情況,但是它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。

最經典的就是快取+資料庫讀寫的模式(Cache Aside Pattern)。

  • 讀的時候,先讀快取,快取沒有的話,再讀資料庫,然後取出資料後放入快取,同時返回響應。
  • 更新的時候,先更新資料庫,然後再刪除快取。

快取穿透和快取雪崩

快取穿透

概念:

訪問一個不存在的 key,快取不起作用,請求會穿透到 DB,流量大時 DB 會掛掉。

舉個栗子。系統A,每秒 5000 個請求,結果其中 4000 個請求是黑客發出的惡意攻擊。資料庫 id 是從 1 開始的,而黑客發過來的請求 id 全部都是負數。這樣的話,快取中不會有,請求每次都“視快取於無物”,直接查詢資料庫。這種惡意攻擊場景的快取穿透就會直接把資料庫給打死。

image

解決方案:

(1)對查詢結果為空的情況也進行快取,快取時間設定短一點,或者該 key 對應的資料 insert 之後再清理快取。

(2)對一定不存在的 key 進行過濾。可以把所有的可能存在的key放到一個大的Bitmap中,查詢時通過該Bitmap過濾。

快取雪崩

概念:

對於系統 A,假設每天高峰期每秒 5000 個請求,本來快取在高峰期可以扛住每秒 4000 個請求,但是快取機器意外發生了全盤當機。快取掛了,此時 1 秒 5000 個請求全部落資料庫,資料庫必然扛不住,它會報一下警,然後就掛了。此時,如果沒有采用什麼特別的方案來處理這個故障,DBA 很著急,重啟資料庫,但是資料庫立馬又被新的流量給打死了。

image

解決方案

(1)在快取失效後,通過加鎖或者佇列來控制讀資料庫寫快取的執行緒數量。比如對某個 key 只允許一個執行緒查詢資料和寫快取,其他執行緒等待。

(2)不同的 key,設定不同的過期時間,讓快取失效的時間點儘量均勻。

(3)做二級快取,A1 為原始快取,A2為拷貝快取,A1 失效時,可以訪問 A2,A1 快取失效時間設定為短期,A2 設定為長期(此點為補充)

快取擊穿

概念:

某個 key 非常熱點,訪問非常頻繁,處於集中式高併發訪問的情況,當這個 key 在失效的瞬間,大量的請求就擊穿了快取,直接請求資料庫,就像是在一道屏障上鑿開了一個洞。

解決方案:

(1)使用互斥鎖 (mutex key):感知到快取失效,去查詢 DB 時,使用分散式鎖,使得只有一個執行緒去資料庫載入資料,加鎖失敗的執行緒,等待即可。

  • 獲取分散式鎖,直到成功或超時。如果超時,則丟擲異常,返回。如果成功,繼續向下執行。
  • 再去快取中。如果存在值,則直接返回;如果不存在,則繼續往下執行。因為,獲得到鎖,可能已經被“那個”執行緒去查詢過 DB ,並更新到快取中了。
  • 查詢 DB ,並更新到快取中,返回值。

(2)手動過期:redis 上從不設定過期時間,功能上將過期時間存在 key 對應的 value 裡,如果發現要過期,通過一個後臺的非同步執行緒進行快取的構建,也就是“手動”過期。

快取併發競爭

某個時刻,多個系統例項都去更新某個 key。可以基於 zookeeper 實現分散式鎖。每個系統通過 zookeeper 獲取分散式鎖,確保同一時間,只能有一個系統例項在操作某個 key,別人都不允許讀和寫。

image

要寫入快取的資料都是從 mysql 裡查出來的,都得寫入 mysql 中,寫入 mysql 中的時候必須儲存一個時間戳,從 mysql 查出來的時候,時間戳也要查出來。

每次要寫之前,先判斷一下當前這個 value 的時間戳是否比快取裡的 value 的時間戳要新。如果是的話,那麼可以寫,否則,就不能用舊的資料覆蓋新的資料。

參考文章:

相關文章