讀寫操作一致性分析
引言
首先,先說一下。老外提出了一個緩存一致性設計套路,名為《Cache-Aside pattern》。其中就指出
跟新:應用程序先從cache取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。
命中:應用程序從cache中取數據,取到后返回。
失效:先把數據存到數據庫中,成功后,再讓緩存失效。
另外,知名社交網站facebook也在論文《Scaling Memcache at Facebook》中提出,他們用的是先更新數據庫,再刪緩存的策略
讀操作業務流程,大家應該沒啥疑問,操作流程如下:
寫操作流程分歧比較嚴重,如下分析三種更新緩存策略
- 先更新數據庫,再更新緩存
- 先刪除緩存,再更新數據庫
- 先更新數據庫,再刪除緩存
第一種:先更新數據庫,再更新緩存分析
這種業界比較統一,從性能,業務,技術角度都不建議
-
線程安全角度
同時有請求A和請求B進行更新操作,那么會出現
(1)線程A更新了數據庫
(2)線程B更新了數據庫
(3)線程B更新了緩存
(4)線程A更新了緩存
這就出現請求A更新緩存應該比請求B更新緩存早才對,但是因為網絡等原因,B卻比A更早更新了緩存。這就導致了臟數據,因此不考慮。 -
業務場景角度
有如下兩點:
(1)如果你是一個寫數據庫場景比較多,而讀數據場景比較少的業務需求,采用這種方案就會導致,數據壓根還沒讀到,緩存就被頻繁的更新,浪費性能。
(2)如果你寫入數據庫的值,並不是直接寫入緩存的,而是要經過一系列復雜的計算再寫入緩存。那么,每次寫入數據庫后,都再次計算寫入緩存的值,無疑是浪費性能的。顯然,刪除緩存更為適合。
接下來討論的就是爭議最大的,先刪緩存,再更新數據庫。還是先更新數據庫,再刪緩存的問題。
第二種:先刪緩存,再更新數據庫
該方案會導致不一致的原因是。同時有一個請求A進行更新操作,另一個請求B進行查詢操作。那么會出現如下情形:
(1)請求A進行寫操作,刪除緩存
(2)請求B查詢發現緩存不存在
(3)請求B去數據庫查詢得到舊值
(4)請求B將舊值寫入緩存
(5)請求A將新值寫入數據庫
- 上述情況就會導致不一致的情形出現。而且,如果不采用給緩存設置過期時間策略,該數據永遠都是臟數據。
如何解決呢?采用延時雙刪策略,偽代碼如下:
redis.deleteKey(key);
userService.update(id);
Thread.sleep(1000);
redis.deleteKey(key);
- 雙刪策略,休眠時間是考慮的重點,是休眠1s還是多久? 需要根據業務情況您的寫請求耗時多長,然后再此基礎上加上幾百ms即可
- 假如刪除緩存失敗,如何處理?兩種方案處理,主要思想通過重試的機制刪除,直到成功為止
-
第一種方案:將刪除失敗的key放入消息隊列,再業務系統訂閱再重試機制刪除
-
第二種方案:將刪除失敗的key放入消息隊列,處理機制是將刪除失敗的key不再由業務系統處理,單獨啟獨立的線程及不影響業務系統的操作來做重試刪除機制
第三種:先更新數據庫,再刪除緩存
這種情況極端情況會存在並發問題么,假設這會有兩個請求,一個請求A做查詢操作,一個請求B做更新操作,那么會有如下情形產生
(1)緩存剛好失效
(2)請求A查詢數據庫,得一個舊值
(3)請求B將新值寫入數據庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
分析發生這種情況的概率又有多少呢?發生上述情況有一個先天性條件,就是步驟(3)的寫數據庫操作比步驟(2)的讀數據庫操作耗時更短,才有可能使得步驟(4)先於步驟(5)可是,大家想想,數據庫的讀操作的速度遠快於寫操作的(不然做讀寫分離干嘛,做讀寫分離的意義就是因為讀操作比較快,耗資源少),因此步驟(3)耗時比步驟(2)更短;這種情況發生概覽極其低的, 正如引言所言fackbook采用的是這種方案;
小結: 沒有一種方案策略是完美的,一致性問題是分布式存儲解決方案一直以來的痛點, 問題都需要根據具體的業務場景再具體的分析,如上方案僅供參考;