引入快取後給業務帶來的問題,你看懂了嗎?

2024.09.11

一、引入快取後給業務帶來的問題

業務系統引入快取之後,架構由原來的兩層架構變成了三層架構:

由此,帶來了三個問題需要解決,分別是快取讀取、快取更新和快取淘汰。

1.快取讀取

快取讀取比較簡單,查詢資料時先查詢緩存,如果快取命中,則從快取讀取資料。如果快取不命中,則查詢資料庫,並且更新快取。

2.快取更新

快取更新時,在更新儲存和更新快取的先後關係上,有以下幾種策略:

1)先更新資料庫再更新快取

首先,這種方案會有線程安全的問題:

例如,同時有執行緒A和執行緒B對資料進行更新操作,可能會出現下面的執行順序。

①線程A更新了資料庫

②線程B更新了資料庫

③線程B更新了快取

④線程A更新了快取

此時就會出現資料庫中的資料與快取的資料不一致的情況,這是因為線程A先更新了資料庫,可能因為網路等異常狀況,線程B更新完資料庫進而更新了緩存,當線程B更新完緩存後,線程A才更新緩存,這導致了資料庫資料與快取資料的不一致。

其次,這種方案也有其不適用的業務場景:

首先一個業務場景就是資料庫寫多讀少的場景,這種場景下採用先更新資料庫再更新快取的策略,就會導致快取並未被讀取就會被頻繁的更新,極大的浪費了伺服器的性能。

再一個業務場景就是資料庫中的資料不是直接寫入快取的,而是需要大量的複雜運算,將運算結果寫入快取。如果這種場景下使用先更新資料庫再更新快取的策略,也會造成伺服器資源的浪費。

2)先刪除緩存,再更新資料庫

先刪除快取再更新資料庫的方案也存在線程安全的問題,例如,線程A更新緩存,同時,線程B讀取快取的資料。可能會出現下面的執行順序:

①線程A刪除快取

②線程B查詢緩存,發現快取中沒有想要的數據

③線程B查詢資料庫中的舊數據

④線程B將查詢到的舊資​​料寫入快取

⑤線程A將新資料寫入資料庫

此時,就出現了資料庫中的資料和快取中的資料不一致的情況。如果刪除快取失敗,也會出現資料庫資料和快取資料不一致的現象。

3)先更新資料庫,再刪除快取

首先,這種方式也有極小的機率發生資料庫資料和快取資料不一致的情況,例如,執行緒A做查詢操作,執行緒B執行更新操作,其執行的順序如下所示:

①快取剛好失效

②請求A查詢資料庫,取得到資料庫中的舊值

③請求B將新值寫入資料庫

④請求B刪除快取

⑤請求A將查到的舊值寫入快取

如果上述順序一旦發生,就會造成資料庫中的資料和快取中的資料不一致的情況發生。但是,先更新資料庫再刪除快取的策略發生資料庫和快取資料不一致的機率很低,原因就是:③的寫入資料庫操作比步驟②的讀取資料庫操作耗時更短,才有可能使得步驟④先於步驟⑤執行。

但是,往往資料庫的讀取操作的速度遠快於寫入操作,因此步驟③耗時比步驟②更短這一場景很難出現。因此,先更新資料庫,再刪除快取是值得推薦的做法。

4)異常狀況

上面的討論與對比都是在刪除快取和更新資料庫這兩步驟操作都成功的情況下敘述的。當然系統正常運作時的操作基本上都是成功的,那麼如果兩步驟操作有其中一步操作失敗了呢?以先更新資料庫再刪除快取舉例:

  • 更新資料庫失敗:這種情況很簡單,不會影響第二步操作,也不會影響資料一致性,直接拋異常出去就好了;
  • 更新快取失敗:這種情況需要繼續嘗試刪除緩存,直到快取刪除成功,可以用一個訊息佇列完成,如下圖所示:

①更新資料庫資料;

②刪除快取資料失敗;

③將需要刪除的key傳送至訊息佇列;

④自己消費訊息,獲得需要刪除的key;

⑤繼續重試刪除操作,直到成功。

3.緩存淘汰

主要有兩種策略,分別是主動淘汰和被動淘汰:

  • 主動淘汰:給鍵值對設定TTL時間,到期自動淘汰(建議),這種方式可以達到快取熱資料的目的;
  • 被動淘汰:記憶體達到最大限制時,透過LRU、LFU演算法淘汰(不建議),這種方式影響快取效能,快取品質不可控。

二、緩存的三座大山

1.一致性

一致性主要解決以下幾個問題:

1)並行更新如何解決隔離性問題

  • 序列更新:單一Key更新序列需序列更新,保證時序
  • 並行更新:不同的key可以放到不同的Slot中,在Slot維度可以進行平行更新,提升效能

2)原子性更新時如何解決部分更新的問題

  • 系統連動:快取&儲存即時同步更新狀態,透過revision同步狀態
  • 部分成功:快取更新成功,儲存更新失敗,觸發HA,保障寫入成功(日誌冪等)

3)如何解決讀一致的問題

  • revision:每個Key都附有一個revision,透過revision辨識資料新舊
  • 淘汰控制:Redis不淘汰儲存未更新的資料(Redis不淘汰revision < 4的資料),確保Redis不快取舊版數據

2.緩存擊穿

快取擊穿指的是存取資料時直接繞過緩存,存取資料庫。可能會有兩種原因造成這種現象:

一種是大量的空查詢,例如駭客攻擊;另一種是緩存污染,例如大量的網路爬蟲造成的。

1)為了解決空查詢帶來的快取擊穿,主要有兩種方案:

第一種是在快取層前面再加一層布隆過濾器,布隆過濾器是一種資料結構,比較巧妙的機率型資料結構(probabilistic data structure),特點是有效率地插入和查詢,可以用來告訴你「某樣東西一定不存在或可能存在」。布隆過濾器是一個bit 向量或說bit 數組,長這樣:

如果我們要映射一個值到布隆過濾器中,我們需要使用多個不同的雜湊函數產生多個雜湊值,並對每個產生的雜湊值指向的bit 位置1,例如針對值「baidu 」 和三個不同的雜湊函數分別產生了雜湊值1、4、7,則上圖轉變為:

我們現在再存一個值“tencent”,如果雜湊函數回傳3、4、8 的話,圖繼續變成:

圖片圖片

值得注意的是,4 這個bit 位元由於兩個值的雜湊函數都回傳了這個bit 位,因此它被覆寫了。現在我們如果想查詢「dianping」 這個值是否存在,哈希函數回傳了1、5、8三個值,結果我們發現5 這個bit 位元上的值為0,說明沒有任何一個值對應到這個bit 位上,因此我們可以很確定地說“dianping” 這個值不存在。

而當我們需要查詢“baidu” 這個值是否存在的話,那麼雜湊函數必然會回傳1、4、7,然後我們檢查發現這三個bit 位元上的值均為1,那麼我們可以說“baidu”存在了麼?答案是不可以,只能是「baidu」 這個值可能存在。

這是為什麼呢?答案很簡單,因為隨著增加的值越來越多,被置為1 的bit 位元也會越來越多,這樣某個值「taobao」 即使沒有被儲存過,但是萬一雜湊函數會傳回的三個bit 位元都被其他值置位了1 ,那麼程式還是會判斷「taobao」 這個值存在。

很顯然,過小的布隆過濾器很快所有的bit 位均為1,那麼查詢任何值都會返回“可能存在”,起不到過濾的目的了。布隆過濾器的長度會直接影響誤報率,布隆過濾器越長其誤報率越小。另外,雜湊函數的數量也需要權衡,個數越多則布隆過濾器bit 位置位1 的速度越快,且布隆過濾器的效率越低;但是如果太少的話,那我們的誤報率會變高。

第二種是把所有的key和熱資料value加入緩存,在快取層攔截空資料查詢。

2)為了解決爬蟲帶來的快取擊穿問題,可以設定快取策略:

  • 針對更新的操作,需要立即緩存
  • 針對讀取的操作,可以在設定是否立即緩衝還是延遲緩存,以及在規定的時間窗內命中的次數是否達到一定的次數才進行緩存

3.緩存雪崩

快取雪崩指的是熱資料集中淘汰,大量請求瞬間透傳到儲存層,導致儲存層過載。

造成緩存雪崩的原因主要是TTL機制過於簡單造成的,解決方案主要有以下內容:

  • 設定TTL時給過期時間加上一個隨機的時間值
  • 每一次的訪問都會重新更新TTL,此外業務可以更精準地指定熱數據快取時間