如何解決緩存數(shù)據(jù)不一致性問題
1. 數(shù)據(jù)不一致的原因
1.1 雙寫導(dǎo)致數(shù)據(jù)不一致
由于分布性系統(tǒng),不能保證每個節(jié)點都可用,所有可能引起 Redis 在極限情況下數(shù)據(jù)沒有寫入成功,那么此時緩存中的數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)不一致。
數(shù)據(jù)的更新為什么會成功:因為事務(wù)保證數(shù)據(jù)不管是成功還是失敗,都不會有臟數(shù)據(jù)。
1.2 高并發(fā)導(dǎo)致數(shù)據(jù)不一致
數(shù)據(jù)在修改的過程中必定會存在網(wǎng)絡(luò)延時,因為分布式系統(tǒng)節(jié)點相互獨立部署,那么在并發(fā)讀的情況之下,還沒來得及修改完,那么對于讀操作,讀到的數(shù)據(jù)都是老的數(shù)據(jù)
如果是某些不嚴(yán)謹(jǐn)?shù)那闆r,無所謂,如果是極致的嚴(yán)謹(jǐn),那么就不能這么做了。比如我們的環(huán)境監(jiān)測,大氣的一些數(shù)據(jù)會每隔1-2分鐘更新,甚至有的是5分鐘更新,所以如果讀到的是一些老的數(shù)據(jù),是沒有關(guān)系的,因為最終幾秒或者幾十秒以后會更新,這些數(shù)據(jù)的來去不會很大,而且我們能容忍一定的誤差,所以也就無所謂了。
2. 查詢數(shù)據(jù)的邏輯
先請求先到 Redis,如果命中則返回結(jié)果。如果 Redis 中沒有數(shù)據(jù),則從數(shù)據(jù)庫查詢,再寫入到緩存中,再返回結(jié)果。
3. 更新數(shù)據(jù)的邏輯
3.1 先刪除緩存,再更新數(shù)據(jù)庫
3.1.1 方案一
在并發(fā)不高的情況下:先刪除 Redis 中的舊數(shù)據(jù)。更新數(shù)據(jù)庫中的數(shù)據(jù)。再將數(shù)據(jù)庫中的數(shù)據(jù)同步到 Redis 中。
3.1.2 方案二
在高并發(fā)的情況下,假設(shè)有請求 A 進行更新操作,另一個請求 B 進行查詢操作,那么有可能會出現(xiàn):
- A 進行更新操作前,先刪除了緩存
- B 查詢發(fā)現(xiàn)緩存不存在
- B 查詢數(shù)據(jù)庫的舊值
- B 將舊值寫入到緩存
- A 執(zhí)行更新,將新值寫入到數(shù)據(jù)庫
- 后續(xù)的請求因為發(fā)現(xiàn)緩存中有數(shù)據(jù),導(dǎo)致 A 更新的數(shù)據(jù)一直無法更新到緩存中,這樣便出現(xiàn)了數(shù)據(jù)庫與緩存不一致的情況。
3.2 先更新數(shù)據(jù)庫,再刪除緩存
3.2.1 方案一
該方案雖然存在并發(fā)問題,但是出現(xiàn)上述情況的概率是極低的,也有一些企業(yè)在使用這種方案。
在超高并發(fā)下,請求 A 執(zhí)行更新操作,請求 B 進行查詢操作:
- B 將新值寫入到數(shù)據(jù)庫
- A 查詢 Redis 得到舊數(shù)據(jù)
- 線程 B 刪除緩存
- 這樣就會導(dǎo)致 A 修改數(shù)據(jù) —> A 刪除 Redis 之間出現(xiàn)臟數(shù)據(jù)。
3.2.2 方案二
在超高并發(fā)下,請求 A 執(zhí)行更新操作,請求 B 進行查詢操作:
- 緩存剛好失效
- B 查詢數(shù)據(jù)庫,得到一個舊值
- A 將新值寫入到數(shù)據(jù)庫
- 線程 A 刪除緩存
- B 將舊值寫入到緩存
- 這樣就會導(dǎo)致后續(xù)的請求之間出現(xiàn)臟數(shù)據(jù)。
3.3 緩存雙刪方案
它的流程為:
- 先刪除緩存
- 再寫數(shù)據(jù)庫
- 休眠一段時間,再刪除緩存
回顧一下方案“先刪除緩存,再更新數(shù)據(jù)庫”可能造成數(shù)據(jù)庫與緩存不一致的情況。
假設(shè)有請求 A 進行更新操作,另一個請求 B 進行查詢操作,如果使用緩存雙刪策略:
- A 進行更新操作前,先刪除了緩存
- B 查詢發(fā)現(xiàn)緩存不存在
- B 查詢數(shù)據(jù)庫的舊值
- B 將舊值寫入到緩存
- A 執(zhí)行更新,將新值寫入到數(shù)據(jù)庫,執(zhí)行休眠 Thread.sleep(t)
- A 蘇醒,再次將緩存中的值刪除
緩存雙刪的優(yōu)點是大大降低了數(shù)據(jù)庫與緩存不一致的概率的發(fā)生,注意這里只是降低,并不是說完全的避免,途中紅框的地方就是緩存臟數(shù)據(jù)的時間,缺點為一定程度上降低了吞吐量,因為系統(tǒng)進行了休眠
這里為什么要采用休眠,對數(shù)據(jù)進行延遲緩存,原因是
- 例如:如果在 A 刪除緩存之后,數(shù)據(jù)庫修改之間 C 再次請求數(shù)據(jù)庫,將老的信息存儲進緩存,那么后續(xù)所有的請求打在緩存中,還是獲取到老的數(shù)據(jù)
- 在分庫分表的情況下,延遲一定的時間,也保證了,修改后的數(shù)據(jù)全部同步到所有的數(shù)據(jù)庫中。
4. 擴展:其它的解決雙寫一直問題
通過監(jiān)聽數(shù)據(jù)庫日志,來修改 Redis 的數(shù)據(jù),使數(shù)據(jù)的修改達到準(zhǔn)實時的級別,例如:canal。但是這種情況下會有一些時間的延遲,也會短暫的產(chǎn)生臟數(shù)據(jù)。這種情況適用于寫多讀少的場景
完全使用緩存作為數(shù)據(jù)庫,后面在定時任務(wù)修改數(shù)據(jù)庫數(shù)據(jù)。這種情況下,沒要求對 Redis 的三高要求非常高,可以采用云廠商的 Redis 服務(wù)。
讀取的時候只提供 Redis,也就是說,當(dāng)更新操作一開始從 Redis 中刪除數(shù)據(jù)了,用戶去讀 Redis,如果沒有是不會從數(shù)據(jù)庫中讀的,因為只提供 Redis 的讀取,寫入的時候只在數(shù)據(jù)新增以及更新后才會放入到 Redis,那么如此一來,并發(fā)讀的時候就不會從數(shù)據(jù)庫讀取老的數(shù)據(jù)并且放入 Redis 中了。沒有讀到也沒關(guān)系,做一些空數(shù)據(jù)的處理,可能會有個幾百毫秒或者 1-2s 的延遲,但是可以忍受。但是要注意做好緩存穿透的校驗處理。
5. 緩存數(shù)據(jù)的思考
我們能放入緩存的數(shù)據(jù)本就不應(yīng)該是實時性、一致性要求超高的,所以緩存數(shù)據(jù)的時候加上過期時間,保證每天拿到當(dāng)前最新數(shù)據(jù)即可。
我們不應(yīng)該過度設(shè)計,增加系統(tǒng)的復(fù)雜性,遇到實時性、一致性要求高的數(shù)據(jù),就應(yīng)該查數(shù)據(jù)庫,即使慢點。
超高并發(fā)場景的一致性,都是最終一致性,也就是弱一致性,所以要考慮每一個環(huán)節(jié)可能失敗的情況,補償 job 也是常有的。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis Template實現(xiàn)分布式鎖的實例代碼
使用Redis的SETNX命令獲取分布式鎖的步驟,接下來通過本文給大家介紹Redis Template實現(xiàn)分布式鎖的實例代碼,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2018-09-09SpringBoot讀寫Redis客戶端并實現(xiàn)Jedis技術(shù)切換功能
這篇文章主要介紹了SpringBoot讀寫Redis客戶端并實現(xiàn)技術(shù)切換功能,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-01-01