上一篇咱們基本上已經理解緩存服務 redis 的基本概念後,接下來咱們要進入正題 :
緩存策略
相信不少人應該會覺得這很簡單,不就是將熱資料丟到緩存,然後用戶先優先去緩存取得,沒有則去資料庫拿去嗎 ?
用腦袋想很簡單沒錯,但是難處就在於 :
你要如何確保資料一致性呢 ?
有沒有覺得這名詞很耳熟呢 ? 你只要記好,只要是多個服務,只要是共用資料的,那就一定會碰到它。
在建立緩存時,我們需要先來決定一件重要的事情。
什麼樣的資料需要存放到緩存中呢 ?
基本上適合緩存資料的特點有以下幾點 :
且中如果符合上述兩個情況的那就可以算在『 適合建立緩存的資料 』選項中。
讀的基本流程如下 :
基本上這種讀的流程比較沒有太大問題與爭論。
圖 1 : 緩存讀取流程
緩存策略最大坑在這
比較大的問題在於『 寫 』這裡,因為不同的寫入方式會產生不同的問題,而且這沒有 100% 的完美解,只能有較優但還是有缺點的解。接下來我們來一個一個慢慢看。
先說一下,最大的問題在於 :
在併發情況下,會產生 db 與 緩存 ( redis ) 不一致。
基本上會有以下四種變型。
首先咱們先來看第一種方式『 先寫 db 後改緩存 』。
流程如下圖 2 所示 :
圖 2 : 先寫 db 後改緩存模式
這種情境會有什麼問題呢 ?
基本會有兩個問題。
首先第一個問題為會有一次讀取到舊的緩存資料如下圖 3 所示,雖然有些人會覺得這沒什麼,但是你想想如果是搶票情況呢 ? 假設有個用戶已經確定買到票,且已經更新完 db 後,這時另一個用戶,在緩存更新『 前 』讀取,發現還有一張票,但實際上已經沒票了。這時你覺得用戶會如何呢 ?
圖 3 : 先寫 db 後改緩存的問題 1
這一種情況是發生同時有兩個用戶要進行修改的情況,如下圖 4 所示,用戶 a 先要求修改數量,接下來用戶 b 要再將數量修改,但是問題就出在緩存這裡,變成用戶 b 先修改緩存,用戶 a 後來才修改緩存。
這是真的有可能會發生的場境,你不能保證所有操作都是照你想的順序進行。
這會造成什麼後果呢 ? 那就是後來進來的請求,全部都會在緩存這讀取到『 錯誤 』的數量,而這個錯誤只能人工發現修改。
圖 4 : 先寫 db 後改緩存的問題 2
接下來咱們先來看第二種方式『 先寫 db 後淘汰緩存 』。這裡和第一種的差別就在於,從『 修改 』轉『 淘汰 』,這裡淘汰的意思就是刪除緩存,然後當讀取 miss 後再去重新建立緩存。
順到說一下,這種類型又被稱為『 Cache Aside Pattern 』,它也是 facebook 所使用的緩存策略。
流程如下圖 5 所示 :
圖 5 : 先寫 db 後淘汰緩存
這種情境會有什麼問題呢 ?
基本會有一個問題。
這種情況會發生當緩存刪除操作失敗時,之後所有的讀取都會讀到錯誤的資訊。
圖 6 : 先寫 db 後淘汰緩存問題 1
這個問題仔細思考一下,發生機率事實上應該不高,首先如果緩存刪除那一步失敗,那再重試,而如果一直不行那事實上也有可能緩存服務整個掛掉,那這樣用戶 b 應用也不會讀到錯誤的緩存。
而這裡還有另一種解法,那就是當發現緩存失敗了,就回滾 db 操作。
~ 小備註 ~
如果是 db 操作失敗,那也就只是回覆用戶說這個操作失敗,而不是會回錯誤的數量給用戶。
流程如下 :
圖 7 : 先改緩存後寫 db
這種情境會有什麼問題呢 ?
基本上會有兩個問題 :
這種情況會發生在,當修戶緩存成功,但修改 db 失敗時,緩存會是錯誤資料。
圖 8 : 先改緩存後寫 db 問題 1
這種情況會發生在,當用戶 a 請求修改,而用戶 b 在請求修改,但這時如果用戶 b 的都先完成,用戶 a 的後完成,就會發生 db 與緩存會是不一致的。而這情境上面事實上也有發生過,注意只要是『 修改緩存 』的情境,這都會發生。
圖 9 : 先改緩存後寫 db 問題 2
流程如下圖 10 所示 :
圖 10 : 先淘汰緩存,後寫 db
這種情境會有什麼問題呢 ?
基本會有個問題。
這個情況如下圖 11 所示,如果在刪除緩存到修改 db 這一段時間,有人進來讀取,發現是空的緩存,然後去 db 抓資料來塞緩存,而這個動作又快於用戶 a 修改 db,那就會發生緩存與 db 資料不一致的問題。
圖 11 : 先淘汰緩存後寫 db 問題 1
那要選那個方案好呢 ?
事實上咱們可以注意到一件事情,每一種都會有問題,這個基本上是無法避免的事情,所以咱們只能儘可能的選擇比較不差的。
首先第一種與第三種這兩個類型,可以先刪除了,這兩個問題比較多些而。
改緩存的選項可移除
接下來第二種與第四種來選。基本上如果是我來選的話,應該會選擇『 第二種 』方法當緩存策略。
建議走第二種 : 先寫 DB 後淘汰援存 ( Cache Aside Pattern )
主要的原因在於這種感覺比較想的到解法,當你發現後淘汰緩存失敗後,你可以手動的讓 db rollback,雖然在 rollback 這一段時間,可能緩存還是錯誤的,但至少會回復。
而第四種雖然這種發生的機率可能性比較小,但是這種很難像上面情況我們可以處理,除非我們將它完全變成序列化執行,也就是所謂的一個一個處理,但是相對的,性能完全是大打折,這做法幾乎就是 mysql 開 serializable 級別一樣。
不過這個答案不是絕對,只是個人看法。
~ 小備註 ~
架構師之路這一系列文章中,有專門在討探緩存這一塊,其中作者是支持『 第四種: 先淘汰緩存,後寫 db』,有興趣的友人可以去看看裡面留言的一堆論戰。不過好像連結看不到留言,要用微信……
但於它贊成的原因在於,就是因為第二種會碰到上述問題,不過感覺它裡面沒有說的很清楚,第四種他的解法是如何處理……,不過這不影響這個作者寫的『 架構師之路 』這一系列文章的價值,他真的寫的很好。
缓存,究竟是淘汰,还是修改?
Cache Aside Pattern
本篇文章中咱們大概理了一下,基本上緩存策略,其中讀的流程比較不會有問題,問題出在『 寫 』的過程,而最後咱們大概離出只有這兩種選擇 :
其中這裡是比較建議用第二種,不過也是有人支持第四種,詳細可以參考上面說的那幾篇文章。
建議走第二種 : 先寫 DB 後淘汰援存 ( Cache Aside Pattern )
最後說一下,從上面可以知道,事實上這四種方法都還是會有一些不一致情況產生,而且都是在『 寫 』這一塊會發生問題,所以這裡也可以總結一下幾個 cache 使用時機 :
緩存適合用在大量讀取,且更新頻率較少的資料
如果在常更新的資料上進行快取,那和找死沒差多少。
最後這裡提一下。
現階段我們都是假設資料庫是單機的情況,如果多機的情況,答案或需有可能不同。之後會談到。
你想想資料庫讀寫分離方案,緩存策略這裡還是一樣嗎 ?
『首先第一種與第二種這兩個類型,可以先刪除了,這兩個問題比較多些而。』這句話,似乎應該改成『第一種與第三種』。
『接下來第二種與第三種來選』這句話,似乎應該改成『第二種與第四種』。
跪謝…… 不好意思沒檢查到 ~ 感謝你 ~
請問第三種的問題ㄧ的不一致指的是緩存和db不一致嗎?
因為圖8假設資料10為正確的情況下,使用者B讀取到10看起來是沒什麼問題的
謝謝
嗯對 ~ 不一致的確是指『 緩存 』與『 db 』不一致。
圖 8 的確是要將資料修改成 10 沒錯,而使用者會讀到 10 也沒錯。但是緩存只是個暫存區,我們一切資料都是以資料庫為基準。
以圖 8 的例子來看,我們事實上未來會發生這種事情 ~
了解~感謝說明
看起來這樣的情境發生時會造成使用者不愉悅的狀況。
很怕面對不一致問題時,會漏掉某些狀況
想請教馬克大大在思考不一致問題時是把以下變因依照時間軸作排列組合,然後一個一個去思考有甚麼問題嗎?
還是有其他建議的方向
謝謝
嗯目前一致性問題我也只想的到這樣想,不過應該是不太會有什麼漏掉,大概。
BTW 順到提一下,在思考的技術中有一種叫 mece 原則的東東,簡單的說就是在思考情境分類時可以達到『不重不漏 (白話文就是不漏掉)』。
不過這只是原則,要實際做到這個某些面需要很多的訓練與閱讀,建議可以去買一下說明思考的書來理解理解。這篇文章可以參考一下兒 ~
好喔,感謝!
第二個方法先寫db,後刪除緩存
也會有讀到一次舊緩存的議題
似乎沒提到
呵…沒錯… 真不知道我腦袋當初在想什麼……
感謝你喔 ~