緩存由於其高並發和高性能的特性,在項目中被廣泛使用。讀緩存流程如下圖:
雙寫一致性有以下三個要求:
- 緩存不能讀到臟數據
- 緩存可能會讀到過期數據,但要在可容忍時間內實現最終一致
- 這個可容忍時間盡可能的小
要想同時滿足上面三條,可以采用讀請求和寫請求串行化,串到一個內存隊列里去,這樣就可以保證一定不會出現不一致的情況。但是,串行化之后,就會導致系統的吞吐量會大幅度的降低,要用比正常情況下多幾倍的機器去支撐線上請求。
所以,在這里,我們討論三種常見方法:
- 先更新數據庫,再更新緩存
- 先刪除緩存,再更新數據庫
- 先更新數據庫,再刪除緩存
1. 先更新數據庫,再更新緩存
這種方法是大家普遍反對的,原因集中在下面兩點:
原因1:線程安全角度。
同時有請求A和請求B進行更新操作,那么會出現:
- 線程A更新了數據庫
- 線程B更新了數據庫
- 線程B更新了緩存
- 線程A更新了緩存
這就出現請求A更新緩存應該比請求B更新緩存早才對,但是因為網絡等原因,B卻比A更早更新了緩存。這就導致了臟數據,因此不考慮。
"先更新緩存,再更新數據庫"這種方案同理,也是造成臟數據,所以不被考慮
原因2:業務場景角度。
有如下兩點:
- 如果你是一個寫數據庫場景比較多,而讀數據場景比較少的業務需求,采用這種方案就會導致數據壓根還沒讀到,緩存就被頻繁的更新,浪費性能。
- 如果你寫入數據庫的值,並不是直接寫入緩存的,而是要經過一系列復雜的計算再寫入緩存。那么,每次寫入數據庫后,都再次計算寫入緩存的值,無疑是浪費性能的。顯然,刪除緩存更為適合。
如果一定要更新緩存,可以考慮給緩存數據增加版本號
2. 先刪除緩存,再更新數據庫
該方案同樣會導致不一致。同時有請求A和請求B進行更新操作,那么會出現:
- 請求A進行寫操作,刪除緩存
- 請求B查詢發現緩存不存在
- 請求B去數據庫查詢得到舊值
- 請求B將舊值寫入緩存
- 請求A將新值寫入數據庫上述情況就會導致不一致的情形出現。而且,如果不采用給緩存設置過期時間策略,該數據永遠都是臟數據。
解決方法:
- 先刪除緩存
- 再寫數據庫(這兩步和原來一樣)
- 休眠一定時間(例如1秒或200ms),再次刪除緩存。這么做,可以將緩存臟數據再次刪除。
然而這種解決方案由於要休眠線程還是很影響吞吐量的
3. 先更新數據庫,再刪除緩存
這種方案是很多工程采用的方案,我們來看下是否一定安全。
假設有兩個請求,一個請求A做查詢操作,一個請求B做更新操作,那么會有如下情形產生
- 緩存剛好失效
- 請求A查詢數據庫,得一個舊值
- 請求B將新值寫入數據庫
- 請求B刪除緩存
- 請求A將查到的舊值寫入緩存
這樣,臟數據就產生了,然而上面的情況是假設在數據庫寫請求比讀請求還要快。實際上,工程中數據庫的讀操作的速度遠快於寫操作的。
要么通過2PC或是Paxos協議保證一致性,要么就是想盡辦法降低並發時臟數據的概率,大概是因為2PC太慢,而Paxos又太復雜,綜合考慮,Facebook選擇了這個第三種方案。
如果刪除緩存失敗了怎么辦?
啟動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。
阿里開源的中間件canal可以完成訂閱binlog日志的功能。
總結
本文是對目前互聯網中已有的一致性方案進行了一個總結,希望大家有所收獲。
最后,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』后台回復關鍵詞領取學習資料、進入前后端技術交流群和程序員副業群。同時也可以加入程序員副業群Q群:735764906 一起交流。