聊聊分散式快取

2023-01-16 12:00:35

快取作為磁碟以外的一種儲存資料的方式,它有著比磁碟更快的存取效率,因此,可以有效提高系統的效能。在單體系統中,一般會用到本地快取。但在分散式系統中,本地快取就顯得不夠用了,這時往往要用到分散式快取。

分散式快取特性

本地快取因為就在應用系統程序的記憶體裡面,不需要網路和物件拷貝的開銷所以效能非常高,不過也正因為資料儲存在應用系統程序的記憶體中也有了一些侷限。例如,多個獨立應用系統之間難於共用快取、快取服務不利於擴充套件等。為了解決這些問題,以Jboss Cache、Memcache、Redis為代表的分散式快取也開始出現,它們以不同的方式實現了分散式快取的特性。

1、快取共用
分散式系統是多個單體系統的集合,分散式快取可以讓這些單體系統共用快取資料。分散式快取實現快取共用一般有兩種方式,分別是同步資料和獨立部署。

  • 同步資料
    這種方式的實現原理是,快取存在於單體系統應用程序的記憶體中(和本地快取一樣),當一個單體系統的快取發生變化時,把變化的內容同步到其他單體系統,從而達到快取共用的效果。這也就是Jboss Cache的思想,這種方式的確解決了共用的問題,但是這種快取同步的操作如果太多那麼本身就會成為一個很大的效能問題。

  • 獨立部署
    這種方式是把快取服務作為一個元件單獨部署,所有的單體系統都可以通過網路請求從快取伺服器存取資料。MemCache和Redis就是這種方式,不過這種方式會有額外的網路開銷,相比於直接在本地就能獲取的資料,自然會有額外的效能開銷。

2、快取擴充套件
當快取資料量規模很大時,單臺伺服器的記憶體是無法容納的,那麼此時就需要支援快取的節點擴充套件。以獨立部署方式實現快取共用的分散式快取服務因為是獨立部署,因此,更有利於做擴充套件。一般會按照一定的演演算法,把快取的資料分配到多個節點。

3、支援高可用
分散式快取一般應用於分散式系統,分散式系統之所以要採用分散式架構,往往是因為系統要應對高並行的場景,而高並行場景又容易造成伺服器負載過重從而導致崩潰。因此,分散式快取作為分散式架構的重要元件,必須要支援高可用。當一個快取服務節點掛掉,可以馬上切換到另外的快取服務節點,以保證系統能正常執行。

分散式快取要注意的問題

問題一:快取一致性問題
快取的資料來源於資料庫,可能會出現資料庫資料跟快取資料不一致的情況,這就是快取一致性的問題。快取一致性問題主要原因是在於修改資料庫的同時還需要把最新的資料更新到快取中去,但是這個過程出現一些問題從而導致快取資料與資料庫的資料不一致。

譬如A、B兩個請求先後修改一個資料a。正常情況下,A修改資料庫-->A修改快取-->B修改資料-->B修改快取。這時候沒有問題,如下圖:

但是,在並行的情況下(A、B兩個請求同時發起)很有可能產生如下情況,A修改資料庫-->B修改資料庫-->B修改快取-->A修改快取。這時候資料庫a=3,而快取a=2,快取資料與資料庫的資料不一致,如下圖:

  • 解決方案一:先修改資料庫、再刪除快取。
    這種策略也稱為Cache Aside(旁路快取),其核心在於:
    修改資料時,先修改資料庫,然後不對快取進行修改而是直接刪除快取。
    讀取資料時,先從快取讀取。如果快取沒有則從資料庫讀取資料,最後把讀取到的資料寫入到快取。

    只要保證資料變更時快取一定會刪除,那麼查詢快取的時候每次都會從資料庫載入資料,所以也就避免了修改快取資料導致的不一致問題,如下圖:

  • 解決方案二:資料標識版本號(樂觀鎖)
    方案一其實只是一定程度上解決了一致性問題。為什麼這麼說呢?如下圖:


    按照上面的步驟,資料庫最終結果是a=3,但快取最終結果是a=2。因此,旁路快取這種策略並沒有完全解決問題。另一個解決方案,可以給資料標識版本號,也就是樂觀鎖。在設計資料表的時候,就應該設定一個記錄資料版本號的欄位,版本號欄位型別可以是整型,版本號越大代表資料越新。快取資料的時候,也把版本號一起快取。這樣,並行修改資料的過程,如下圖:


    當寫入快取的資料版本比當前快取的資料版本要低階,就放棄寫入。這個方案在快取過期情況下同樣適用,如下圖:


    如果上圖中Step6寫入快取失敗,那麼Step7就可以把a=2寫進快取,這樣也會產生資料不一致。這時候,就要引入訊息佇列。把寫入快取失敗的請求放進訊息佇列,然後有一個執行緒在消費訊息,完成寫入快取的工作,當然寫入快取時要先校驗資料版本。

問題二:快取擊穿問題
快取擊穿就是在某一熱點資料快取失效後,查詢資料的請求不得不進入到資料庫查詢資料,這個過程中如果請求並行量非常大,有可能資料庫負載過重而崩潰。

  • 解決方案一:加鎖
    在查詢資料請求未命中快取時,查詢資料庫操作前進行加鎖,加鎖後後面的請求就會阻塞,避免了大量的請求同時進入到資料庫查詢資料。然後,在資料庫查詢資料後,再把資料寫入快取。
  • 解決方案二:設定合理的過期時間
    可以把熱點資料的快取過期時間控制在系統低流量的時間段,避過流量高峰期。
  • 解決方案三:不設定過期時間
    可以不設定過期時間來保證熱點資料快取永遠不會失效,然後通過後臺的執行緒來定時把最新的資料同步到快取裡去。

問題三:快取穿透問題
通常情況,一個請求查詢資料,會先從快取查詢,如果在快取裡找不到,就到資料庫查詢。而快取穿透就是請求系統里根本不存在的資料,導致請求總是進入到資料庫。如果大量的請求同時進入到資料庫,就有可能導致資料庫崩潰。譬如,我們通過文章的id來查詢文章資訊,但是請求的id在資料庫根本不存在。

  • 解決方案一:快取null值
    可以把請求不存在資料快取一個對應key的null值,譬如,某個id在第一次請求的時候發現是系統不存在的資料,就把這個id作為key,值為null快取起來。下次再有請求查詢這個id,就可以在快取找到這個id,並且返回null。但是這種做法仍然存在風險,如果攻擊者使用大量不同的資料(譬如,各種系統不存在的文章id),這樣就會產生大量無意義的null值快取,使得快取伺服器記憶體暴增。因此,一方面null值快取的過期時間要儘可能小,防止無意義內容過多佔用記憶體。另一方面,可以通過業務量估算資料範圍,得到合理的id大致的範圍,請求的資料id一旦超出範圍,就直接返回null。譬如,目前文章的資料量只有10000條資料,按照大概平均每天的業務量,每天最多增長100條資料,一年內也不會超過10萬條資料,那麼請求的id範圍就估計在1到10萬之間,如果某個請求超過這範圍,就可以直接返回null。
  • 解決方案二:布隆過濾器
    布隆過濾器,是巴頓.布隆於1970年提出的,其主旨是採用一個很長的二進位制陣列,通過一系列的Hash函數來確定某個資料是否存在。布隆過濾器本質上是一個N位的二進位制陣列,每一位的初始值都是0,任何一個資料都可以通過一系列的Hash函數來在這個二進位制陣列確定幾個下標,然後這幾個下標的元素的值置為1。相比於方案一有佔用記憶體過多的風險,利用布隆過濾器的優點在於它不需要佔用很多記憶體。

    舉個例子,現在知道了文章的id範圍是1到1000,現在通過布隆過濾器表示這1000個id。一開始需要初始化布隆過濾器,也就是說1到1000都要能在陣列中表示出來。譬如,id1經過3次Hash,得出3個數(1、5、9),那麼陣列下標為1、5、99的元素就置為1。id2經過3次Hash,也得出3個數(1、3、98),那麼陣列下標為1、3、98的元素就置為1。然後到id3...,一直到id1000。

    這樣,當一個請求過來,譬如請求id為1,經過3次Hash計算後,得出1、5、99,然後對應陣列下標這3個元素值都為1,就判斷id1存在於系統。如果有一個請求的id找到的3個陣列元素中只要有一個為0,就判斷這個id不存在於系統。布隆過濾器的具體應用可以查詢相關資料,這裡不詳細展開。

問題四:快取雪崩問題
快取雪崩是指快取資料某一時刻出現大量失效或者快取服務不可用的情況,導致大量請求進入到資料庫,可能導致資料庫崩潰。

  • 解決方案一:快取分片
    快取分片主要目的是分流,把快取的資料拆分到多個快取服務節點,每個節點會儲存一部分的快取資料,減輕了單個節點的存取壓力達到分流效果,而且就算一個節點出現故障也不會造成整個快取服務的不可用。
  • 解決方案二:快取熱備選主
    這種方式是通過冗餘多個備用節點,當主節點發生故障時,通過某種演演算法從備用節點中重新選舉出一個節點作為主節點來對外提供服務。

問題五:熱key問題
如果某個key被大量存取,就會導致某個快取服務節點流量暴增,等存取超出單節點負載,就可能會出現單點故障,單點故障後轉移該key的資料到其他節點,單點問題依舊存在,則可能繼續會讓被轉移到的節點也出現故障,最終影響整個快取服務叢集。

  • 解決方案一:生成多個副本
    對於熱點key,生成多個副本,分別被多個快取服務節點持有,然後按照某種演演算法實現負載均衡。這樣本來單臺節點的流量就會被分到多個節點上,降低單個節點的壓力。
  • 解決方案二:本地快取
    針對熱點key在本地應用伺服器上包一層短存活期的本地快取,用於緩衝熱點伺服器的壓力。

分散式快取Redis/Memcache對比

目前比較常用的分散式快取中介軟體有Redis、MemCache,因此,在這裡對這兩者做了一個對比。對於這兩者,各有各的特點,因此,不一定要二選一,可以混合使用。

對比項 Redis Memcached
高可用 支援主從節點複製設定,從節點可使用RDB和快取的AOF命令進行同步和恢復;支援Sentinel和Cluster(從3.0版本開始)等高可用叢集方案 memcached伺服器互不通訊,分散式部署取決於memcached使用者端,需要二次開發
佇列 支援lpush/brpop、publish/subscribe/psubscribe等佇列和訂閱模式 不支援佇列,可通過第三方MemcachQ來實現
適用場景 複雜的資料結構,有持久化、高可用需求 只需key-value資料結構,數量量非常大,並行量非常大的業務
過期策略 主動過期 +惰性過期 懶淘汰機制,每次往快取放入資料的時候,都會存一個時間,在讀取的時候要和設定的時間做 TTL 比較來判斷是否過期。
虛擬記憶體使用 有自己的VM機制,理論上能夠儲存比實體記憶體更多的資料,當資料超量時,為引發swap,把冷資料刷到磁碟上 所有的資料儲存在實體記憶體裡
網路模型 非阻塞I/O模型 ,提供一些非KV儲存之外的排序,集合功能,在執行這些功能時,複雜的CPU計算會阻塞整個I/O排程 非阻塞I/O模型 ,但是使用了多執行緒,不會出現一個邏輯複雜的請求阻塞對其它請求的響應的場景
資料結構 key-value,雜湊,列表,集合,有序集合 純key-value
持久化 有,RDB和AOF
儲存value容量 最大512M 最大1M
多執行緒 支援單執行緒 支援多執行緒,CPU利用方面優於Redis
單機QPS 約10W 約60W