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