詳解MySQL和Redis如何保證數(shù)據(jù)一致性
什么是一致性
“數(shù)據(jù)一致”一般指的是:緩存中有數(shù)據(jù),緩存的數(shù)據(jù)值=數(shù)據(jù)庫中的值。但根據(jù)緩存中是有數(shù)據(jù)為依據(jù),則“一致”可以包含兩種情況:
1)緩存中有數(shù)據(jù),緩存的數(shù)據(jù)值=數(shù)據(jù)庫中的值。
2)緩存中本沒有數(shù)據(jù),數(shù)據(jù)庫中的值=最新值(有請求查詢數(shù)據(jù)庫時(shí),會將數(shù)據(jù)寫入緩存,則變?yōu)樯厦娴?ldquo;一致”狀態(tài))。
“數(shù)據(jù)不一致”:緩存的數(shù)據(jù)值≠數(shù)據(jù)庫中的值;緩存或者數(shù)據(jù)庫中存在舊值,導(dǎo)致其他線程讀到舊數(shù)據(jù)。
一致性就是數(shù)據(jù)保持一致,在分布式系統(tǒng)中,可以理解為多個(gè)節(jié)點(diǎn)中數(shù)據(jù)的值是一致的。
- 強(qiáng)一致性:這種一致性級別是最符合用戶直覺的,它要求系統(tǒng)寫入什么,讀出來的也會是什么,用戶體驗(yàn)好,但實(shí)現(xiàn)起來往往對系統(tǒng)的性能影響大
- 弱一致性:這種一致性級別約束了系統(tǒng)在寫入成功后,不承諾立即可以讀到寫入的值,也不承諾多久之后數(shù)據(jù)能夠達(dá)到一致,但會盡可能地保證到某個(gè)時(shí)間級別(比如秒級別)后,數(shù)據(jù)能夠達(dá)到一致狀態(tài)
- 最終一致性:最終一致性是弱一致性的一個(gè)特例,系統(tǒng)會保證在一定時(shí)間內(nèi),能夠達(dá)到一個(gè)數(shù)據(jù)一致的狀態(tài)。這里之所以將最終一致性單獨(dú)提出來,是因?yàn)樗侨跻恢滦灾蟹浅M瞥绲囊环N一致性模型,也是業(yè)界在大型分布式系統(tǒng)的數(shù)據(jù)一致性上比較推崇的模型
導(dǎo)致數(shù)據(jù)不一致的原因?
1) 在高并發(fā)的業(yè)務(wù)場景下,數(shù)據(jù)庫大多數(shù)情況都是用戶并發(fā)訪問最薄弱的環(huán)節(jié)。所以,就需要使用redis做一個(gè)緩沖操作,讓請求先訪問到redis,而不是直接訪問MySQL等數(shù)據(jù)庫;
2)讀取緩存步驟一般沒有什么問題,但是一旦涉及到數(shù)據(jù)更新,數(shù)據(jù)庫和緩存更新,就容易出現(xiàn)緩存(Redis)和數(shù)據(jù)庫(MySQL)間的數(shù)據(jù)一致性問題;
3)這個(gè)業(yè)務(wù)場景,主要是解決讀數(shù)據(jù)從Redis緩存,一般都是按照下圖的流程來進(jìn)行業(yè)務(wù)操作。
應(yīng)對策略
針對緩存更新問題,提出了一個(gè)旁路緩存的緩存更新套路,這個(gè)策略分為以下三種場景:
1)失效:應(yīng)用程序先從緩存取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。
2)命中:應(yīng)用程序從緩存中取數(shù)據(jù),取到后返回。
3)更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。
不管是先刪緩存再更新數(shù)據(jù)庫還是先更新數(shù)據(jù)庫再刪緩存,都會導(dǎo)致緩存跟數(shù)據(jù)不一致問題!
先寫MySQL,再寫Redis
先寫Redis,再寫MySQL
先刪除Redis,再寫MySQL
先寫 MySQL,再刪除 Redis
不管是先寫數(shù)據(jù)庫,再刪除緩存;還是先刪除緩存,再寫庫,都有可能出現(xiàn)數(shù)據(jù)不一致的情況。
現(xiàn)有的大部分業(yè)務(wù)場景下大多采用讀寫分離的操作來提升數(shù)據(jù)庫吞吐量,但是并發(fā)讀寫訪問的時(shí)候,對緩存和數(shù)據(jù)庫相互交叉執(zhí)行操作,則會出現(xiàn)數(shù)據(jù)不一致問題。
在進(jìn)行數(shù)據(jù)更新時(shí),就涉及到先更新緩存還是先更新數(shù)據(jù)庫了,其實(shí)兩種方式都有數(shù)據(jù)一致性問題:
舉個(gè)例子:假如業(yè)務(wù)A為寫請求,業(yè)務(wù)B為讀請求
1.先更新數(shù)據(jù)庫再更新緩存
步驟1:業(yè)務(wù)A先更新數(shù)據(jù)庫,此時(shí)該業(yè)務(wù)線由于宕機(jī)或者其他原因延遲沒有繼續(xù)進(jìn)行。
步驟2:業(yè)務(wù)B讀取數(shù)據(jù),讀取的是緩存中的舊數(shù)據(jù)。
步驟3:業(yè)務(wù)A恢復(fù)過來,更新緩存
可以看到,由于寫請求延遲,可能會讀到舊的緩存數(shù)據(jù)。
2.先更新緩存再更新數(shù)據(jù)庫
步驟1:業(yè)務(wù)A先刪除緩存
步驟2:業(yè)務(wù)B進(jìn)入,業(yè)務(wù)B發(fā)現(xiàn)緩存中沒有數(shù)據(jù),直接從數(shù)據(jù)庫中進(jìn)行讀取,讀到了數(shù)據(jù)庫中的舊數(shù)據(jù)
步驟3:業(yè)務(wù)A更新數(shù)據(jù)庫并返回。
可以看到,由于寫請求延遲,可能讀到舊的數(shù)據(jù)庫數(shù)據(jù)。
因?yàn)閷懞妥x是并發(fā)的,沒法保證順序,就會出現(xiàn)緩存和數(shù)據(jù)庫的數(shù)據(jù)不一致的問題。
解決方案
(1)讀寫請求串行化
最為簡單的一種方法,寫請求在更新之前需要先獲得分布式鎖,獲取到鎖才能去更新數(shù)據(jù)庫,獲取不到則進(jìn)行等待,超時(shí)直接返回更新失敗。更新完數(shù)據(jù)庫后更新緩存,如果更新失敗,放到內(nèi)存隊(duì)列中進(jìn)行重新嘗試。讀請求則同樣需要獲得鎖,然判斷緩存中是否有數(shù)據(jù),有則直接讀緩存,沒有則直接讀數(shù)據(jù)庫,并更新緩存。
這種方案可以保證數(shù)據(jù)的一致性。但是會降低系統(tǒng)吞吐量(等待時(shí)間長),這在需要數(shù)據(jù)強(qiáng)一致的情況下適用。(銀行轉(zhuǎn)賬)
(2)刪除緩存
- 1.先刪除緩存,后更新數(shù)據(jù)庫
- 2.先更新數(shù)據(jù)庫,后刪除緩存
先刪除緩存,后更新數(shù)據(jù)庫,第二步操作失敗,數(shù)據(jù)庫沒有更新成功,那下次讀緩存發(fā)現(xiàn)不存在則從數(shù)據(jù)庫中讀取,并重建緩存,此時(shí)數(shù)據(jù)庫和緩存依日保持一致。
但如果是先更新數(shù)據(jù)庫,后刪除緩存,第二步操作失敗,數(shù)據(jù)庫是最新值,緩存中是舊值,發(fā)生不致。所以,這個(gè)方案依舊存在問題。
總之,和前面提到的問題類似,第二步失敗依舊有不一致的風(fēng)險(xiǎn)
我們再來看[并發(fā)]問題,這個(gè)問題是我們需要關(guān)注的[重點(diǎn)]
先更新數(shù)據(jù)庫,后刪除緩存
依舊是 2 個(gè)線程并發(fā)[讀寫]數(shù)據(jù)
1.緩存中 X 不存在 (數(shù)據(jù)庫 X = 1)
2.線程 A 讀取數(shù)據(jù)庫,得到目值 (X = 1)
3.線程 B 更新數(shù)據(jù)庫 (X = 2)
4.線程 B 刪除緩存
5.線程 A 將日值寫入緩存 (X = 1)
最終 X的值在緩存中是 1 (日值) ,在數(shù)據(jù)庫中是 2(新值),也發(fā)生不一致
這種情況[理論]來說是可能發(fā)生的,但實(shí)際真的有可能發(fā)生嗎?
其實(shí)概率[很低],這是因?yàn)樗仨殱M足 3 個(gè)條件
1.緩存剛好已失效
2.讀請求 + 寫請求并發(fā)
3.更新數(shù)據(jù)庫 + 除緩存的時(shí)間 (步 3-4) ,要比讀數(shù)據(jù)庫 + 寫緩存時(shí)間短(步 2 和5)
仔細(xì)想一下,條件 3 發(fā)生的概率其實(shí)是非常低的因?yàn)閷憯?shù)據(jù)庫一般會先[加鎖],所以寫數(shù)據(jù)庫,通常是要比讀數(shù)據(jù)庫的時(shí)間更長的這么來看,[先更新數(shù)據(jù)庫 + 再刪除緩存]的方案,是可以保證數(shù)據(jù)一致性的。
所以,我們應(yīng)該采用這種方案,來操作數(shù)據(jù)庫和緩存
如何保證兩步都執(zhí)行成功?
無論是更新緩存還是刪除緩存,只要第二步發(fā)生失敗,那么就會導(dǎo)致數(shù)據(jù)庫和緩存不一致。保證第二步成功執(zhí)行,就是解決問題的關(guān)鍵.
程序在執(zhí)行過程中發(fā)生異常,最簡單的解決辦法是什么?
答案是:異步重試
- 如果是同步重試,立即重試很大概率還會失敗,[重試次數(shù)]設(shè)置多少才合理?
- 重試會一直[占用]這個(gè)線程資源,無法服務(wù)其它客戶端請求
- 異步其實(shí)就是把重試請求寫到消息隊(duì)列中,然后由專門的消費(fèi)者來重試,直到成功。
為了避免第二步執(zhí)行失敗,我們可以把操作緩存這一步,直接放到消息隊(duì)列中,由消費(fèi)者來操作緩存
到這里你可能會問,寫消息隊(duì)列也有可能會失敗啊? 而且,引入消息隊(duì)列,這又增加了更多的維擴(kuò)成本,這樣做值得嗎?
這個(gè)問題很好,但我們思考這樣一個(gè)問題:如果在執(zhí)行失敗的線程中一直重試,還沒等執(zhí)行成功,此時(shí)如果項(xiàng)目[重啟]了,那這次重試請求也就[丟失]了,那這條數(shù)據(jù)就一直不一致了
所以,這里我們必須把重試消息或第二步操作放到另一個(gè)[服務(wù)]中,這個(gè)服務(wù)用[消息隊(duì)列]最為合適。
- 消息隊(duì)列保證可靠性: 寫到隊(duì)列中的消息,成功消費(fèi)之前不會丟失(重啟項(xiàng)目也不擔(dān)心)
- 消息隊(duì)列保證消息成功投遞: 下游從隊(duì)列拉取消息,成功消費(fèi)后才會刪除消息,否則還會繼續(xù)投遞消息給消費(fèi)者 (符合我們重試的需求)
至于寫隊(duì)列失敗和消息隊(duì)列的維護(hù)成本問題
- 寫隊(duì)列失敗: 操作緩存和寫消息隊(duì)列,[同時(shí)失敗]的概率其實(shí)是很小的維護(hù)成本:
- 我們項(xiàng)目中一般都會用到消息隊(duì)列,維護(hù)成本并沒有新增很多
以上就是詳解MySQL和Redis如何保證數(shù)據(jù)一致性的詳細(xì)內(nèi)容,更多關(guān)于MySQL和Redis數(shù)據(jù)一致性的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MySQL進(jìn)行表之間關(guān)聯(lián)更新的實(shí)現(xiàn)方法
在實(shí)際編程工作或運(yùn)維實(shí)踐中,對MySQL數(shù)據(jù)庫表進(jìn)行關(guān)聯(lián)更新是一種比較常見的應(yīng)用場景,針對這樣的業(yè)務(wù)場景,我們來看看有什么方法可以實(shí)現(xiàn)關(guān)聯(lián)更新,需要的朋友可以參考下2023-10-10mysql中decimal數(shù)據(jù)類型小數(shù)位填充問題詳解
這篇文章主要介紹了mysql中decimal數(shù)據(jù)類型小數(shù)位填充問題詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02MySQL可直接使用的查詢表的列信息(實(shí)現(xiàn)方案)
本文介紹了如何使用SQL快速將下劃線命名的表字段轉(zhuǎn)換為駝峰命名格式,包括確定下劃線位置、找到第一個(gè)字符、截取并拼接字符串等步驟,通過使用LOCATE、CONCAT、UCASE和LOWER等函數(shù),可以實(shí)現(xiàn)高效的字段命名轉(zhuǎn)換,感興趣的朋友跟隨小編一起看看吧2025-01-01mysql之跨庫關(guān)聯(lián)查詢(dblink)問題
這篇文章主要介紹了mysql之跨庫關(guān)聯(lián)查詢(dblink)問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03