30-19 之資料庫層的優化 - 資料緩存策略
it 鐵人賽 2019 redis
Lastmod: 2019-12-15

正文開始

上一篇咱們基本上已經理解緩存服務 redis 的基本概念後,接下來咱們要進入正題 :

緩存策略

相信不少人應該會覺得這很簡單,不就是將熱資料丟到緩存,然後用戶先優先去緩存取得,沒有則去資料庫拿去嗎 ?

用腦袋想很簡單沒錯,但是難處就在於 :

你要如何確保資料一致性呢 ?

有沒有覺得這名詞很耳熟呢 ? 你只要記好,只要是多個服務,只要是共用資料的,那就一定會碰到它。

  • 什麼樣類型資料適合緩存呢 ?
  • 緩存讀流程
  • 緩存寫策略與難題
  • 緩存寫策略的難題總結

什麼樣類型資料適合緩存呢 ?

在建立緩存時,我們需要先來決定一件重要的事情。

什麼樣的資料需要存放到緩存中呢 ?

基本上適合緩存資料的特點有以下幾點 :

  • 這個資料是常常被使用到的。
  • 這個資料是不常被更新的。

且中如果符合上述兩個情況的那就可以算在『 適合建立緩存的資料 』選項中。

緩存讀流程

讀的基本流程如下 :

  • (1) 用戶往應用服務發送請求。
  • (2) 應用服務至緩存服務看看是否有緩存。
  • (3A) 有,則回傳。
  • (3B) 無,則前往資料庫服務取得資料。
  • (4) 並將資料回寫入緩存服務。

基本上這種讀的流程比較沒有太大問題與爭論。

圖 1 : 緩存讀取流程

緩存寫策略與難題


緩存策略最大坑在這

比較大的問題在於『 寫 』這裡,因為不同的寫入方式會產生不同的問題,而且這沒有 100% 的完美解,只能有較優但還是有缺點的解。接下來我們來一個一個慢慢看。

先說一下,最大的問題在於 :

在併發情況下,會產生 db 與 緩存 ( redis ) 不一致。

基本上會有以下四種變型。

  1. 先寫 db 後改緩存
  2. 先寫 db 後淘汰援存
  3. 先改緩存 後寫 db
  4. 先淘汰緩存,後寫 db

第一種 : 先寫 DB 後改緩存

首先咱們先來看第一種方式『 先寫 db 後改緩存 』。

流程如下圖 2 所示 :

  • (1) 用戶請求修改資料。
  • (2) 先修改 db 資料。
  • (3) 再修改緩存資料。

圖 2 : 先寫 db 後改緩存模式

這種情境會有什麼問題呢 ?

基本會有兩個問題。

問題 1 : 會有一次讀取到舊的緩存資料

首先第一個問題為會有一次讀取到舊的緩存資料如下圖 3 所示,雖然有些人會覺得這沒什麼,但是你想想如果是搶票情況呢 ? 假設有個用戶已經確定買到票,且已經更新完 db 後,這時另一個用戶,在緩存更新『 前 』讀取,發現還有一張票,但實際上已經沒票了。這時你覺得用戶會如何呢 ?

圖 3 : 先寫 db 後改緩存的問題 1

問題 2 : 併行修改時會有緩存 DB 資料不一致問題

這一種情況是發生同時有兩個用戶要進行修改的情況,如下圖 4 所示,用戶 a 先要求修改數量,接下來用戶 b 要再將數量修改,但是問題就出在緩存這裡,變成用戶 b 先修改緩存,用戶 a 後來才修改緩存。

這是真的有可能會發生的場境,你不能保證所有操作都是照你想的順序進行。

這會造成什麼後果呢 ? 那就是後來進來的請求,全部都會在緩存這讀取到『 錯誤 』的數量,而這個錯誤只能人工發現修改。

圖 4 : 先寫 db 後改緩存的問題 2

第二種 : 先寫 DB 後淘汰緩存 ( Cache Aside Pattern )

接下來咱們先來看第二種方式『 先寫 db 後淘汰緩存 』。這裡和第一種的差別就在於,從『 修改 』轉『 淘汰 』,這裡淘汰的意思就是刪除緩存,然後當讀取 miss 後再去重新建立緩存。

順到說一下,這種類型又被稱為『 Cache Aside Pattern 』,它也是 facebook 所使用的緩存策略。

流程如下圖 5 所示 :

  • (1) 用戶請求修改資料。
  • (2) 先修改 db 資料。
  • (3) 再淘汰緩存 ( 也就是直接將緩存移除 )。

圖 5 : 先寫 db 後淘汰緩存

這種情境會有什麼問題呢 ?

基本會有一個問題。

問題 1 : 緩存操作失敗會導致資料不一致性問題

這種情況會發生當緩存刪除操作失敗時,之後所有的讀取都會讀到錯誤的資訊。

圖 6 : 先寫 db 後淘汰緩存問題 1

這個問題仔細思考一下,發生機率事實上應該不高,首先如果緩存刪除那一步失敗,那再重試,而如果一直不行那事實上也有可能緩存服務整個掛掉,那這樣用戶 b 應用也不會讀到錯誤的緩存。

而這裡還有另一種解法,那就是當發現緩存失敗了,就回滾 db 操作。

~ 小備註 ~ 如果是 db 操作失敗,那也就只是回覆用戶說這個操作失敗,而不是會回錯誤的數量給用戶。

第三種 : 先改緩存後寫 DB

流程如下 :

  • (1) 用戶請求修改資料。
  • (2) 先修改緩存資料。
  • (3) 再修改 db 資料。

圖 7 : 先改緩存後寫 db

這種情境會有什麼問題呢 ?

基本上會有兩個問題 :

問題 1 : 修改 DB 失敗會有資料不一致問題

這種情況會發生在,當修戶緩存成功,但修改 db 失敗時,緩存會是錯誤資料。

圖 8 : 先改緩存後寫 db 問題 1

問題 2 : 並行寫入問題會有資料不一致問題

這種情況會發生在,當用戶 a 請求修改,而用戶 b 在請求修改,但這時如果用戶 b 的都先完成,用戶 a 的後完成,就會發生 db 與緩存會是不一致的。而這情境上面事實上也有發生過,注意只要是『 修改緩存 』的情境,這都會發生。

圖 9 : 先改緩存後寫 db 問題 2

第四種 : 先淘汰緩存後寫 DB

流程如下圖 10 所示 :

  • (1) 用戶請求修改資料。
  • (2) 先淘汰緩存資料。
  • (3) 再修改 db 資料。。

圖 10 : 先淘汰緩存,後寫 db

這種情境會有什麼問題呢 ?

基本會有個問題。

問題 1 : 併行讀時可能會有資料不一致問題

這個情況如下圖 11 所示,如果在刪除緩存到修改 db 這一段時間,有人進來讀取,發現是空的緩存,然後去 db 抓資料來塞緩存,而這個動作又快於用戶 a 修改 db,那就會發生緩存與 db 資料不一致的問題。

圖 11 : 先淘汰緩存後寫 db 問題 1

緩存寫策略的難題總結

那要選那個方案好呢 ?

第一種: 先寫 DB 後改緩存

  • 問題 1 : 會有一次讀取到舊的緩存資料。
  • 問題 2 : 並行修改時會有緩存 db 資料不一致問題。

第二種: 先寫 DB 後淘汰援存 ( Cache Aside Pattern )

  • 問題 1 : 緩存操作失敗會導致資料不一致性問題。

第三種: 先改緩存 後寫 DB

  • 問題 1 : 修改 db 失敗會有資料不一致問題。
  • 問題 2 : 並行寫入問題會有資料不一致問題。

第四種: 先淘汰緩存,後寫 DB

  • 問題 1 : 併行讀時可能會有資料不一致問題

小總結

事實上咱們可以注意到一件事情,每一種都會有問題,這個基本上是無法避免的事情,所以咱們只能儘可能的選擇比較不差的。

首先第一種與第三種這兩個類型,可以先刪除了,這兩個問題比較多些而。

改緩存的選項可移除

接下來第二種與第四種來選。基本上如果是我來選的話,應該會選擇『 第二種 』方法當緩存策略。

建議走第二種 : 先寫 DB 後淘汰援存 ( Cache Aside Pattern )

主要的原因在於這種感覺比較想的到解法,當你發現後淘汰緩存失敗後,你可以手動的讓 db rollback,雖然在 rollback 這一段時間,可能緩存還是錯誤的,但至少會回復。

而第四種雖然這種發生的機率可能性比較小,但是這種很難像上面情況我們可以處理,除非我們將它完全變成序列化執行,也就是所謂的一個一個處理,但是相對的,性能完全是大打折,這做法幾乎就是 mysql 開 serializable 級別一樣。

不過這個答案不是絕對,只是個人看法。

~ 小備註 ~ 架構師之路這一系列文章中,有專門在討探緩存這一塊,其中作者是支持『 第四種: 先淘汰緩存,後寫 db』,有興趣的友人可以去看看裡面留言的一堆論戰。不過好像連結看不到留言,要用微信……

但於它贊成的原因在於,就是因為第二種會碰到上述問題,不過感覺它裡面沒有說的很清楚,第四種他的解法是如何處理……,不過這不影響這個作者寫的『 架構師之路 』這一系列文章的價值,他真的寫的很好。

缓存,究竟是淘汰,还是修改? Cache Aside Pattern

結論與心得

本篇文章中咱們大概理了一下,基本上緩存策略,其中讀的流程比較不會有問題,問題出在『 寫 』的過程,而最後咱們大概離出只有這兩種選擇 :

  • 第二種: 先寫 db 後淘汰援存 ( Cache Aside Pattern )
  • 第四種: 先淘汰緩存,後寫 db

其中這裡是比較建議用第二種,不過也是有人支持第四種,詳細可以參考上面說的那幾篇文章。

建議走第二種 : 先寫 DB 後淘汰援存 ( Cache Aside Pattern )

最後說一下,從上面可以知道,事實上這四種方法都還是會有一些不一致情況產生,而且都是在『 寫 』這一塊會發生問題,所以這裡也可以總結一下幾個 cache 使用時機 :

緩存適合用在大量讀取,且更新頻率較少的資料

如果在常更新的資料上進行快取,那和找死沒差多少。

最後這裡提一下。

現階段我們都是假設資料庫是單機的情況,如果多機的情況,答案或需有可能不同。之後會談到。

你想想資料庫讀寫分離方案,緩存策略這裡還是一樣嗎 ?

參考資料

comments powered by Disqus