0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

到底是更新緩存還是刪緩存

數(shù)據(jù)分析與開(kāi)發(fā) ? 來(lái)源:水滴與銀彈 ? 作者:Magic Kaito ? 2021-10-22 17:05 ? 次閱讀

如何保證緩存和數(shù)據(jù)庫(kù)一致性,這是一個(gè)老生常談的話題了。

但很多人對(duì)這個(gè)問(wèn)題,依舊有很多疑惑:

到底是更新緩存還是刪緩存?

到底選擇先更新數(shù)據(jù)庫(kù),再刪除緩存,還是先刪除緩存,再更新數(shù)據(jù)庫(kù)?

為什么要引入消息隊(duì)列保證一致性?

延遲雙刪會(huì)有什么問(wèn)題?到底要不要用?

這篇文章,我們就來(lái)把這些問(wèn)題講清楚。

這篇文章干貨很多,希望你可以耐心讀完。

引入緩存提高性能

我們從最簡(jiǎn)單的場(chǎng)景開(kāi)始講起。

如果你的業(yè)務(wù)處于起步階段,流量非常小,那無(wú)論是讀請(qǐng)求還是寫請(qǐng)求,直接操作數(shù)據(jù)庫(kù)即可,這時(shí)你的架構(gòu)模型是這樣的:

但隨著業(yè)務(wù)量的增長(zhǎng),你的項(xiàng)目請(qǐng)求量越來(lái)越大,這時(shí)如果每次都從數(shù)據(jù)庫(kù)中讀數(shù)據(jù),那肯定會(huì)有性能問(wèn)題。

這個(gè)階段通常的做法是,引入「緩存」來(lái)提高讀性能,架構(gòu)模型就變成了這樣:

c8576fe8-3230-11ec-82a8-dac502259ad0.jpg

當(dāng)下優(yōu)秀的緩存中間件,當(dāng)屬 Redis 莫屬,它不僅性能非常高,還提供了很多友好的數(shù)據(jù)類型,可以很好地滿足我們的業(yè)務(wù)需求。

但引入緩存之后,你就會(huì)面臨一個(gè)問(wèn)題:之前數(shù)據(jù)只存在數(shù)據(jù)庫(kù)中,現(xiàn)在要放到緩存中讀取,具體要怎么存呢?

最簡(jiǎn)單直接的方案是「全量數(shù)據(jù)刷到緩存中」:

數(shù)據(jù)庫(kù)的數(shù)據(jù),全量刷入緩存(不設(shè)置失效時(shí)間)

寫請(qǐng)求只更新數(shù)據(jù)庫(kù),不更新緩存

啟動(dòng)一個(gè)定時(shí)任務(wù),定時(shí)把數(shù)據(jù)庫(kù)的數(shù)據(jù),更新到緩存中

這個(gè)方案的優(yōu)點(diǎn)是,所有讀請(qǐng)求都可以直接「命中」緩存,不需要再查數(shù)據(jù)庫(kù),性能非常高。

但缺點(diǎn)也很明顯,有 2 個(gè)問(wèn)題:

緩存利用率低:不經(jīng)常訪問(wèn)的數(shù)據(jù),還一直留在緩存中

數(shù)據(jù)不一致:因?yàn)槭恰付〞r(shí)」刷新緩存,緩存和數(shù)據(jù)庫(kù)存在不一致(取決于定時(shí)任務(wù)的執(zhí)行頻率)

所以,這種方案一般更適合業(yè)務(wù)「體量小」,且對(duì)數(shù)據(jù)一致性要求不高的業(yè)務(wù)場(chǎng)景。

那如果我們的業(yè)務(wù)體量很大,怎么解決這 2 個(gè)問(wèn)題呢?

緩存利用率和一致性問(wèn)題

先來(lái)看第一個(gè)問(wèn)題,如何提高緩存利用率?

想要緩存利用率「最大化」,我們很容易想到的方案是,緩存中只保留最近訪問(wèn)的「熱數(shù)據(jù)」。但具體要怎么做呢?

我們可以這樣優(yōu)化:

寫請(qǐng)求依舊只寫數(shù)據(jù)庫(kù)

讀請(qǐng)求先讀緩存,如果緩存不存在,則從數(shù)據(jù)庫(kù)讀取,并重建緩存

同時(shí),寫入緩存中的數(shù)據(jù),都設(shè)置失效時(shí)間

這樣一來(lái),緩存中不經(jīng)常訪問(wèn)的數(shù)據(jù),隨著時(shí)間的推移,都會(huì)逐漸「過(guò)期」淘汰掉,最終緩存中保留的,都是經(jīng)常被訪問(wèn)的「熱數(shù)據(jù)」,緩存利用率得以最大化。

再來(lái)看數(shù)據(jù)一致性問(wèn)題。

要想保證緩存和數(shù)據(jù)庫(kù)「實(shí)時(shí)」一致,那就不能再用定時(shí)任務(wù)刷新緩存了。

所以,當(dāng)數(shù)據(jù)發(fā)生更新時(shí),我們不僅要操作數(shù)據(jù)庫(kù),還要一并操作緩存。具體操作就是,修改一條數(shù)據(jù)時(shí),不僅要更新數(shù)據(jù)庫(kù),也要連帶緩存一起更新。

但數(shù)據(jù)庫(kù)和緩存都更新,又存在先后問(wèn)題,那對(duì)應(yīng)的方案就有 2 個(gè):

先更新緩存,后更新數(shù)據(jù)庫(kù)

先更新數(shù)據(jù)庫(kù),后更新緩存

哪個(gè)方案更好呢?

先不考慮并發(fā)問(wèn)題,正常情況下,無(wú)論誰(shuí)先誰(shuí)后,都可以讓兩者保持一致,但現(xiàn)在我們需要重點(diǎn)考慮「異?!骨闆r。

因?yàn)椴僮鞣譃閮刹?,那么就很有可能存在「第一步成功、第二步失敗」的情況發(fā)生。

這 2 種方案我們一個(gè)個(gè)來(lái)分析。

1) 先更新緩存,后更新數(shù)據(jù)庫(kù)

如果緩存更新成功了,但數(shù)據(jù)庫(kù)更新失敗,那么此時(shí)緩存中是最新值,但數(shù)據(jù)庫(kù)中是「舊值」。

雖然此時(shí)讀請(qǐng)求可以命中緩存,拿到正確的值,但是,一旦緩存「失效」,就會(huì)從數(shù)據(jù)庫(kù)中讀取到「舊值」,重建緩存也是這個(gè)舊值。

這時(shí)用戶會(huì)發(fā)現(xiàn)自己之前修改的數(shù)據(jù)又「變回去」了,對(duì)業(yè)務(wù)造成影響。

2) 先更新數(shù)據(jù)庫(kù),后更新緩存

如果數(shù)據(jù)庫(kù)更新成功了,但緩存更新失敗,那么此時(shí)數(shù)據(jù)庫(kù)中是最新值,緩存中是「舊值」。

之后的讀請(qǐng)求讀到的都是舊數(shù)據(jù),只有當(dāng)緩存「失效」后,才能從數(shù)據(jù)庫(kù)中得到正確的值。

這時(shí)用戶會(huì)發(fā)現(xiàn),自己剛剛修改了數(shù)據(jù),但卻看不到變更,一段時(shí)間過(guò)后,數(shù)據(jù)才變更過(guò)來(lái),對(duì)業(yè)務(wù)也會(huì)有影響。

可見(jiàn),無(wú)論誰(shuí)先誰(shuí)后,但凡后者發(fā)生異常,就會(huì)對(duì)業(yè)務(wù)造成影響。那怎么解決這個(gè)問(wèn)題呢?

別急,后面我會(huì)詳細(xì)給出對(duì)應(yīng)的解決方案。

我們繼續(xù)分析,除了操作失敗問(wèn)題,還有什么場(chǎng)景會(huì)影響數(shù)據(jù)一致性?

這里我們還需要重點(diǎn)關(guān)注:并發(fā)問(wèn)題。

并發(fā)引發(fā)的一致性問(wèn)題

假設(shè)我們采用「先更新數(shù)據(jù)庫(kù),再更新緩存」的方案,并且兩步都可以「成功執(zhí)行」的前提下,如果存在并發(fā),情況會(huì)是怎樣的呢?

有線程 A 和線程 B 兩個(gè)線程,需要更新「同一條」數(shù)據(jù),會(huì)發(fā)生這樣的場(chǎng)景:

線程 A 更新數(shù)據(jù)庫(kù)(X = 1)

線程 B 更新數(shù)據(jù)庫(kù)(X = 2)

線程 B 更新緩存(X = 2)

線程 A 更新緩存(X = 1)

最終 X 的值在緩存中是 1,在數(shù)據(jù)庫(kù)中是 2,發(fā)生不一致。

也就是說(shuō),A 雖然先于 B 發(fā)生,但 B 操作數(shù)據(jù)庫(kù)和緩存的時(shí)間,卻要比 A 的時(shí)間短,執(zhí)行時(shí)序發(fā)生「錯(cuò)亂」,最終這條數(shù)據(jù)結(jié)果是不符合預(yù)期的。

同樣地,采用「先更新緩存,再更新數(shù)據(jù)庫(kù)」的方案,也會(huì)有類似問(wèn)題,這里不再詳述。

除此之外,我們從「緩存利用率」的角度來(lái)評(píng)估這個(gè)方案,也是不太推薦的。

這是因?yàn)槊看螖?shù)據(jù)發(fā)生變更,都「無(wú)腦」更新緩存,但是緩存中的數(shù)據(jù)不一定會(huì)被「馬上讀取」,這就會(huì)導(dǎo)致緩存中可能存放了很多不常訪問(wèn)的數(shù)據(jù),浪費(fèi)緩存資源。

而且很多情況下,寫到緩存中的值,并不是與數(shù)據(jù)庫(kù)中的值一一對(duì)應(yīng)的,很有可能是先查詢數(shù)據(jù)庫(kù),再經(jīng)過(guò)一系列「計(jì)算」得出一個(gè)值,才把這個(gè)值才寫到緩存中。

由此可見(jiàn),這種「更新數(shù)據(jù)庫(kù) + 更新緩存」的方案,不僅緩存利用率不高,還會(huì)造成機(jī)器性能的浪費(fèi)。

所以此時(shí)我們需要考慮另外一種方案:刪除緩存。

刪除緩存可以保證一致性嗎?

刪除緩存對(duì)應(yīng)的方案也有 2 種:

先刪除緩存,后更新數(shù)據(jù)庫(kù)

先更新數(shù)據(jù)庫(kù),后刪除緩存

經(jīng)過(guò)前面的分析我們已經(jīng)得知,但凡「第二步」操作失敗,都會(huì)導(dǎo)致數(shù)據(jù)不一致。

這里我不再詳述具體場(chǎng)景,你可以按照前面的思路推演一下,就可以看到依舊存在數(shù)據(jù)不一致的情況。

這里我們重點(diǎn)來(lái)看「并發(fā)」問(wèn)題。

1) 先刪除緩存,后更新數(shù)據(jù)庫(kù)

如果有 2 個(gè)線程要并發(fā)「讀寫」數(shù)據(jù),可能會(huì)發(fā)生以下場(chǎng)景:

線程 A 要更新 X = 2(原值 X = 1)

線程 A 先刪除緩存

線程 B 讀緩存,發(fā)現(xiàn)不存在,從數(shù)據(jù)庫(kù)中讀取到舊值(X = 1)

線程 A 將新值寫入數(shù)據(jù)庫(kù)(X = 2)

線程 B 將舊值寫入緩存(X = 1)

最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫(kù)中是 2(新值),發(fā)生不一致。

可見(jiàn),先刪除緩存,后更新數(shù)據(jù)庫(kù),當(dāng)發(fā)生「讀+寫」并發(fā)時(shí),還是存在數(shù)據(jù)不一致的情況。

2) 先更新數(shù)據(jù)庫(kù),后刪除緩存

依舊是 2 個(gè)線程并發(fā)「讀寫」數(shù)據(jù):

緩存中 X 不存在(數(shù)據(jù)庫(kù) X = 1)

線程 A 讀取數(shù)據(jù)庫(kù),得到舊值(X = 1)

線程 B 更新數(shù)據(jù)庫(kù)(X = 2)

線程 B 刪除緩存

線程 A 將舊值寫入緩存(X = 1)

最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫(kù)中是 2(新值),也發(fā)生不一致。

這種情況「理論」來(lái)說(shuō)是可能發(fā)生的,但實(shí)際真的有可能發(fā)生嗎?

其實(shí)概率「很低」,這是因?yàn)樗仨殱M足 3 個(gè)條件:

緩存剛好已失效

讀請(qǐng)求 + 寫請(qǐng)求并發(fā)

更新數(shù)據(jù)庫(kù) + 刪除緩存的時(shí)間(步驟 3-4),要比讀數(shù)據(jù)庫(kù) + 寫緩存時(shí)間短(步驟 2 和 5)

仔細(xì)想一下,條件 3 發(fā)生的概率其實(shí)是非常低的。

因?yàn)閷憯?shù)據(jù)庫(kù)一般會(huì)先「加鎖」,所以寫數(shù)據(jù)庫(kù),通常是要比讀數(shù)據(jù)庫(kù)的時(shí)間更長(zhǎng)的。

這么來(lái)看,「先更新數(shù)據(jù)庫(kù) + 再刪除緩存」的方案,是可以保證數(shù)據(jù)一致性的。

所以,我們應(yīng)該采用這種方案,來(lái)操作數(shù)據(jù)庫(kù)和緩存。

好,解決了并發(fā)問(wèn)題,我們繼續(xù)來(lái)看前面遺留的,第二步執(zhí)行「失敗」導(dǎo)致數(shù)據(jù)不一致的問(wèn)題。

如何保證兩步都執(zhí)行成功?

前面我們分析到,無(wú)論是更新緩存還是刪除緩存,只要第二步發(fā)生失敗,那么就會(huì)導(dǎo)致數(shù)據(jù)庫(kù)和緩存不一致。

保證第二步成功執(zhí)行,就是解決問(wèn)題的關(guān)鍵。

想一下,程序在執(zhí)行過(guò)程中發(fā)生異常,最簡(jiǎn)單的解決辦法是什么?

答案是:重試。

是的,其實(shí)這里我們也可以這樣做。

無(wú)論是先操作緩存,還是先操作數(shù)據(jù)庫(kù),但凡后者執(zhí)行失敗了,我們就可以發(fā)起重試,盡可能地去做「補(bǔ)償」。

那這是不是意味著,只要執(zhí)行失敗,我們「無(wú)腦重試」就可以了呢?

答案是否定的?,F(xiàn)實(shí)情況往往沒(méi)有想的這么簡(jiǎn)單,失敗后立即重試的問(wèn)題在于:

立即重試很大概率「還會(huì)失敗」

「重試次數(shù)」設(shè)置多少才合理?

重試會(huì)一直「占用」這個(gè)線程資源,無(wú)法服務(wù)其它客戶端請(qǐng)求

看到了么,雖然我們想通過(guò)重試的方式解決問(wèn)題,但這種「同步」重試的方案依舊不嚴(yán)謹(jǐn)。

那更好的方案應(yīng)該怎么做?

答案是:異步重試。什么是異步重試?

其實(shí)就是把重試請(qǐng)求寫到「消息隊(duì)列」中,然后由專門的消費(fèi)者來(lái)重試,直到成功。

或者更直接的做法,為了避免第二步執(zhí)行失敗,我們可以把操作緩存這一步,直接放到消息隊(duì)列中,由消費(fèi)者來(lái)操作緩存。

到這里你可能會(huì)問(wèn),寫消息隊(duì)列也有可能會(huì)失敗?。慷?,引入消息隊(duì)列,這又增加了更多的維護(hù)成本,這樣做值得嗎?

這個(gè)問(wèn)題很好,但我們思考這樣一個(gè)問(wèn)題:如果在執(zhí)行失敗的線程中一直重試,還沒(méi)等執(zhí)行成功,此時(shí)如果項(xiàng)目「重啟」了,那這次重試請(qǐng)求也就「丟失」了,那這條數(shù)據(jù)就一直不一致了。

所以,這里我們必須把重試或第二步操作放到另一個(gè)「服務(wù)」中,這個(gè)服務(wù)用「消息隊(duì)列」最為合適。這是因?yàn)橄㈥?duì)列的特性,正好符合我們的需求:

消息隊(duì)列保證可靠性:寫到隊(duì)列中的消息,成功消費(fèi)之前不會(huì)丟失(重啟項(xiàng)目也不擔(dān)心)

消息隊(duì)列保證消息成功投遞:下游從隊(duì)列拉取消息,成功消費(fèi)后才會(huì)刪除消息,否則還會(huì)繼續(xù)投遞消息給消費(fèi)者(符合我們重試的場(chǎng)景)

至于寫隊(duì)列失敗和消息隊(duì)列的維護(hù)成本問(wèn)題:

寫隊(duì)列失?。翰僮骶彺婧蛯懴㈥?duì)列,「同時(shí)失敗」的概率其實(shí)是很小的

維護(hù)成本:我們項(xiàng)目中一般都會(huì)用到消息隊(duì)列,維護(hù)成本并沒(méi)有新增很多

所以,引入消息隊(duì)列來(lái)解決這個(gè)問(wèn)題,是比較合適的。這時(shí)架構(gòu)模型就變成了這樣:

那如果你確實(shí)不想在應(yīng)用中去寫消息隊(duì)列,是否有更簡(jiǎn)單的方案,同時(shí)又可以保證一致性呢?

方案還是有的,這就是近幾年比較流行的解決方案:訂閱數(shù)據(jù)庫(kù)變更日志,再操作緩存。

具體來(lái)講就是,我們的業(yè)務(wù)應(yīng)用在修改數(shù)據(jù)時(shí),「只需」修改數(shù)據(jù)庫(kù),無(wú)需操作緩存。

那什么時(shí)候操作緩存呢?這就和數(shù)據(jù)庫(kù)的「變更日志」有關(guān)了。

拿 MySQL 舉例,當(dāng)一條數(shù)據(jù)發(fā)生修改時(shí),MySQL 就會(huì)產(chǎn)生一條變更日志(Binlog),我們可以訂閱這個(gè)日志,拿到具體操作的數(shù)據(jù),然后再根據(jù)這條數(shù)據(jù),去刪除對(duì)應(yīng)的緩存。

訂閱變更日志,目前也有了比較成熟的開(kāi)源中間件,例如阿里的 canal,使用這種方案的優(yōu)點(diǎn)在于:

無(wú)需考慮寫消息隊(duì)列失敗情況:只要寫 MySQL 成功,Binlog 肯定會(huì)有

自動(dòng)投遞到下游隊(duì)列:canal 自動(dòng)把數(shù)據(jù)庫(kù)變更日志「投遞」給下游的消息隊(duì)列

當(dāng)然,與此同時(shí),我們需要投入精力去維護(hù) canal 的高可用和穩(wěn)定性。

如果你有留意觀察很多數(shù)據(jù)庫(kù)的特性,就會(huì)發(fā)現(xiàn)其實(shí)很多數(shù)據(jù)庫(kù)都逐漸開(kāi)始提供「訂閱變更日志」的功能了,相信不遠(yuǎn)的將來(lái),我們就不用通過(guò)中間件來(lái)拉取日志,自己寫程序就可以訂閱變更日志了,這樣可以進(jìn)一步簡(jiǎn)化流程。

至此,我們可以得出結(jié)論,想要保證數(shù)據(jù)庫(kù)和緩存一致性,推薦采用「先更新數(shù)據(jù)庫(kù),再刪除緩存」方案,并配合「消息隊(duì)列」或「訂閱變更日志」的方式來(lái)做。

主從庫(kù)延遲和延遲雙刪問(wèn)題

到這里,還有 2 個(gè)問(wèn)題,是我們沒(méi)有重點(diǎn)分析過(guò)的。

第一個(gè)問(wèn)題,還記得前面講到的「先刪除緩存,再更新數(shù)據(jù)庫(kù)」方案,導(dǎo)致不一致的場(chǎng)景么?

這里我再把例子拿過(guò)來(lái)讓你復(fù)習(xí)一下:

2 個(gè)線程要并發(fā)「讀寫」數(shù)據(jù),可能會(huì)發(fā)生以下場(chǎng)景:

線程 A 要更新 X = 2(原值 X = 1)

線程 A 先刪除緩存

線程 B 讀緩存,發(fā)現(xiàn)不存在,從數(shù)據(jù)庫(kù)中讀取到舊值(X = 1)

線程 A 將新值寫入數(shù)據(jù)庫(kù)(X = 2)

線程 B 將舊值寫入緩存(X = 1)

最終 X 的值在緩存中是 1(舊值),在數(shù)據(jù)庫(kù)中是 2(新值),發(fā)生不一致。

第二個(gè)問(wèn)題:是關(guān)于「讀寫分離 + 主從復(fù)制延遲」情況下,緩存和數(shù)據(jù)庫(kù)一致性的問(wèn)題。

在「先更新數(shù)據(jù)庫(kù),再刪除緩存」方案下,「讀寫分離 + 主從庫(kù)延遲」其實(shí)也會(huì)導(dǎo)致不一致:

線程 A 更新主庫(kù) X = 2(原值 X = 1)

線程 A 刪除緩存

線程 B 查詢緩存,沒(méi)有命中,查詢「從庫(kù)」得到舊值(從庫(kù) X = 1)

從庫(kù)「同步」完成(主從庫(kù) X = 2)

線程 B 將「舊值」寫入緩存(X = 1)

最終 X 的值在緩存中是 1(舊值),在主從庫(kù)中是 2(新值),也發(fā)生不一致。

看到了么?這 2 個(gè)問(wèn)題的核心在于:緩存都被回種了「舊值」。

那怎么解決這類問(wèn)題呢?

最有效的辦法就是,把緩存刪掉。

但是,不能立即刪,而是需要「延遲刪」,這就是業(yè)界給出的方案:緩存延遲雙刪策略。

按照延時(shí)雙刪策略,這 2 個(gè)問(wèn)題的解決方案是這樣的:

解決第一個(gè)問(wèn)題:在線程 A 刪除緩存、更新完數(shù)據(jù)庫(kù)之后,先「休眠一會(huì)」,再「刪除」一次緩存。

解決第二個(gè)問(wèn)題:線程 A 可以生成一條「延時(shí)消息」,寫到消息隊(duì)列中,消費(fèi)者延時(shí)「刪除」緩存。

這兩個(gè)方案的目的,都是為了把緩存清掉,這樣一來(lái),下次就可以從數(shù)據(jù)庫(kù)讀取到最新值,寫入緩存。

但問(wèn)題來(lái)了,這個(gè)「延遲刪除」緩存,延遲時(shí)間到底設(shè)置要多久呢?

問(wèn)題1:延遲時(shí)間要大于「主從復(fù)制」的延遲時(shí)間

問(wèn)題2:延遲時(shí)間要大于線程 B 讀取數(shù)據(jù)庫(kù) + 寫入緩存的時(shí)間

但是,這個(gè)時(shí)間在分布式和高并發(fā)場(chǎng)景下,其實(shí)是很難評(píng)估的。

很多時(shí)候,我們都是憑借經(jīng)驗(yàn)大致估算這個(gè)延遲時(shí)間,例如延遲 1-5s,只能盡可能地降低不一致的概率。

所以你看,采用這種方案,也只是盡可能保證一致性而已,極端情況下,還是有可能發(fā)生不一致。

所以實(shí)際使用中,我還是建議你采用「先更新數(shù)據(jù)庫(kù),再刪除緩存」的方案,同時(shí),要盡可能地保證「主從復(fù)制」不要有太大延遲,降低出問(wèn)題的概率。

可以做到強(qiáng)一致嗎?

看到這里你可能會(huì)想,這些方案還是不夠完美,我就想讓緩存和數(shù)據(jù)庫(kù)「強(qiáng)一致」,到底能不能做到呢?

其實(shí)很難。

要想做到強(qiáng)一致,最常見(jiàn)的方案是 2PC、3PC、Paxos、Raft 這類一致性協(xié)議,但它們的性能往往比較差,而且這些方案也比較復(fù)雜,還要考慮各種容錯(cuò)問(wèn)題。

相反,這時(shí)我們換個(gè)角度思考一下,我們引入緩存的目的是什么?

沒(méi)錯(cuò),性能。

一旦我們決定使用緩存,那必然要面臨一致性問(wèn)題。性能和一致性就像天平的兩端,無(wú)法做到都滿足要求。

而且,就拿我們前面講到的方案來(lái)說(shuō),當(dāng)操作數(shù)據(jù)庫(kù)和緩存完成之前,只要有其它請(qǐng)求可以進(jìn)來(lái),都有可能查到「中間狀態(tài)」的數(shù)據(jù)。

所以如果非要追求強(qiáng)一致,那必須要求所有更新操作完成之前期間,不能有「任何請(qǐng)求」進(jìn)來(lái)。

雖然我們可以通過(guò)加「分布鎖」的方式來(lái)實(shí)現(xiàn),但我們要付出的代價(jià),很可能會(huì)超過(guò)引入緩存帶來(lái)的性能提升。

所以,既然決定使用緩存,就必須容忍「一致性」問(wèn)題,我們只能盡可能地去降低問(wèn)題出現(xiàn)的概率。

同時(shí)我們也要知道,緩存都是有「失效時(shí)間」的,就算在這期間存在短期不一致,我們依舊有失效時(shí)間來(lái)兜底,這樣也能達(dá)到最終一致。

總結(jié)

好了,總結(jié)一下這篇文章的重點(diǎn)。

1、想要提高應(yīng)用的性能,可以引入「緩存」來(lái)解決

2、引入緩存后,需要考慮緩存和數(shù)據(jù)庫(kù)一致性問(wèn)題,可選的方案有:「更新數(shù)據(jù)庫(kù) + 更新緩存」、「更新數(shù)據(jù)庫(kù) + 刪除緩存」

3、更新數(shù)據(jù)庫(kù) + 更新緩存方案,在「并發(fā)」場(chǎng)景下無(wú)法保證緩存和數(shù)據(jù)一致性,且存在「緩存資源浪費(fèi)」和「機(jī)器性能浪費(fèi)」的情況發(fā)生

4、在更新數(shù)據(jù)庫(kù) + 刪除緩存的方案中,「先刪除緩存,再更新數(shù)據(jù)庫(kù)」在「并發(fā)」場(chǎng)景下依舊有數(shù)據(jù)不一致問(wèn)題,解決方案是「延遲雙刪」,但這個(gè)延遲時(shí)間很難評(píng)估,所以推薦用「先更新數(shù)據(jù)庫(kù),再刪除緩存」的方案

5、在「先更新數(shù)據(jù)庫(kù),再刪除緩存」方案下,為了保證兩步都成功執(zhí)行,需配合「消息隊(duì)列」或「訂閱變更日志」的方案來(lái)做,本質(zhì)是通過(guò)「重試」的方式保證數(shù)據(jù)一致性

6、在「先更新數(shù)據(jù)庫(kù),再刪除緩存」方案下,「讀寫分離 + 主從庫(kù)延遲」也會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)不一致,緩解此問(wèn)題的方案是「延遲雙刪」,憑借經(jīng)驗(yàn)發(fā)送「延遲消息」到隊(duì)列中,延遲刪除緩存,同時(shí)也要控制主從庫(kù)延遲,盡可能降低不一致發(fā)生的概率

后記

本以為這個(gè)老生常談的話題,寫起來(lái)很好寫,沒(méi)想到在寫的過(guò)程中,還是挖到了很多之前沒(méi)有深度思考過(guò)的細(xì)節(jié)。

在這里我也分享 4 點(diǎn)心得給你:

1、性能和一致性不能同時(shí)滿足,為了性能考慮,通常會(huì)采用「最終一致性」的方案

2、掌握緩存和數(shù)據(jù)庫(kù)一致性問(wèn)題,核心問(wèn)題有 3 點(diǎn):緩存利用率、并發(fā)、緩存 + 數(shù)據(jù)庫(kù)一起成功問(wèn)題

3、失敗場(chǎng)景下要保證一致性,常見(jiàn)手段就是「重試」,同步重試會(huì)影響吞吐量,所以通常會(huì)采用異步重試的方案

4、訂閱變更日志的思想,本質(zhì)是把權(quán)威數(shù)據(jù)源(例如 MySQL)當(dāng)做 leader 副本,讓其它異質(zhì)系統(tǒng)(例如 Redis / Elasticsearch)成為它的 follower 副本,通過(guò)同步變更日志的方式,保證 leader 和 follower 之間保持一致

很多一致性問(wèn)題,都會(huì)采用這些方案來(lái)解決,希望我的這些心得對(duì)你有所啟發(fā)。

責(zé)任編輯:haq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 緩存
    +關(guān)注

    關(guān)注

    1

    文章

    229

    瀏覽量

    26635
  • 數(shù)據(jù)庫(kù)
    +關(guān)注

    關(guān)注

    7

    文章

    3752

    瀏覽量

    64229
  • 模型
    +關(guān)注

    關(guān)注

    1

    文章

    3112

    瀏覽量

    48658

原文標(biāo)題:緩存和數(shù)據(jù)庫(kù)一致性問(wèn)題,看這篇就夠了

文章出處:【微信號(hào):DBDevs,微信公眾號(hào):數(shù)據(jù)分析與開(kāi)發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    DSP指令緩存性能OMAP5912

    電子發(fā)燒友網(wǎng)站提供《DSP指令緩存性能OMAP5912.pdf》資料免費(fèi)下載
    發(fā)表于 10-16 10:16 ?0次下載
    DSP指令<b class='flag-5'>緩存</b>性能OMAP5912

    請(qǐng)問(wèn)LMV772到底是雙電源還是單電源???

    請(qǐng)問(wèn)LMV772到底是雙電源還是單電源???手冊(cè)前面寫的太模糊了。求指教
    發(fā)表于 09-09 07:10

    什么是CPU緩存?它有哪些作用?

    CPU緩存(Cache Memory)是計(jì)算機(jī)系統(tǒng)中一個(gè)至關(guān)重要的組成部分,它位于CPU與內(nèi)存之間,作為兩者之間的臨時(shí)存儲(chǔ)器。CPU緩存的主要作用是減少CPU訪問(wèn)內(nèi)存所需的時(shí)間,從而提高系統(tǒng)的整體性能。以下將詳細(xì)闡述CPU緩存
    的頭像 發(fā)表于 08-22 14:54 ?2086次閱讀

    ESP8266緩存AP后,是否會(huì)自動(dòng)連接到任何緩存的AP?

    ,如果 ESP 進(jìn)入范圍,ESP 是否會(huì)自動(dòng)連接到任何緩存的 AP?還是只有第一個(gè)(或第零個(gè))?我的任務(wù)是在它們之間切換嗎? SDK 說(shuō)道
    發(fā)表于 07-11 07:58

    LOTO示波器軟件PC緩存(波形錄制與回放)功能

    ,保持設(shè)定幀數(shù)量的PC緩存。這些數(shù)據(jù)幀如圖中的1所示,會(huì)按編號(hào)顯示在波形區(qū)的下方,并且有滑動(dòng)條可以快速滑動(dòng)瀏覽。 在示波器采集過(guò)程中,由于PC緩存時(shí)不斷更新存儲(chǔ)的,所以無(wú)法點(diǎn)擊選中任何一幀存儲(chǔ)
    發(fā)表于 05-16 11:23

    交換機(jī)分布緩存_述說(shuō)數(shù)據(jù)中心交換機(jī)的重要性能指標(biāo)——緩存

    交換機(jī)是數(shù)據(jù)中心不可缺少的網(wǎng)絡(luò)設(shè)備,在數(shù)據(jù)中心里發(fā)揮著重要作用。在平時(shí)使用和采購(gòu)時(shí),大多數(shù)都關(guān)注交換機(jī)的背板帶寬、端口密度、單端口速度、協(xié)議特性等方面的性能指標(biāo),很少有人去關(guān)注緩存指標(biāo),這是一個(gè)常常
    的頭像 發(fā)表于 03-15 17:39 ?682次閱讀

    如何選擇合適的本地緩存

    小編最近在使用系統(tǒng)的時(shí)候,發(fā)現(xiàn)盡管應(yīng)用已經(jīng)使用了 redis 緩存提高查詢效率,但是仍然有進(jìn)一步優(yōu)化的空間,于是想到了比分布式緩存性能更好的本地緩存,因此對(duì)領(lǐng)域內(nèi)常用的本地緩存進(jìn)行了一
    的頭像 發(fā)表于 01-18 11:19 ?796次閱讀
    如何選擇合適的本地<b class='flag-5'>緩存</b>?

    請(qǐng)問(wèn)M487KMCAN的SRAM到底是128KB還是160K?

    M487KMCAN的SRAM到底是128 KB 還是160K
    發(fā)表于 01-16 07:18

    labview怎么清除串口緩存的數(shù)據(jù)

    LabVIEW 是一款功能強(qiáng)大的圖形化編程軟件,常用于控制、監(jiān)測(cè)和數(shù)據(jù)采集等應(yīng)用。當(dāng)我們使用串口進(jìn)行數(shù)據(jù)通信時(shí),有時(shí)會(huì)遇到串口緩存的數(shù)據(jù)無(wú)法及時(shí)清除或清除不徹底的情況。解決這個(gè)問(wèn)題的方法有多種,下面
    的頭像 發(fā)表于 01-08 11:30 ?3391次閱讀

    Redis緩存預(yù)熱+緩存雪崩+緩存擊穿+緩存穿透要點(diǎn)簡(jiǎn)析

    緩存預(yù)熱就是系統(tǒng)上線后,提前將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。
    的頭像 發(fā)表于 12-25 09:41 ?825次閱讀
    Redis<b class='flag-5'>緩存</b>預(yù)熱+<b class='flag-5'>緩存</b>雪崩+<b class='flag-5'>緩存</b>擊穿+<b class='flag-5'>緩存</b>穿透要點(diǎn)簡(jiǎn)析

    去耦濾波電容怎么布局?jǐn)[放,到底是先大后小還是先小后大?

    去耦濾波電容怎么布局?jǐn)[放,到底是先大后小還是先小后大?
    的頭像 發(fā)表于 12-04 15:43 ?2251次閱讀
    去耦濾波電容怎么布局?jǐn)[放,<b class='flag-5'>到底是</b>先大后小<b class='flag-5'>還是</b>先小后大?

    mybatis一級(jí)緩存和二級(jí)緩存的原理

    MyBatis是一種輕量級(jí)的持久化框架,它提供了一級(jí)緩存和二級(jí)緩存的機(jī)制來(lái)優(yōu)化數(shù)據(jù)庫(kù)操作性能。一級(jí)緩存是默認(rèn)開(kāi)啟的,而二級(jí)緩存需要手動(dòng)配置啟用。 一、一級(jí)
    的頭像 發(fā)表于 12-03 11:55 ?1056次閱讀

    Redis緩存與Mysql如何保證一致性?

    基本流程就是客戶端A請(qǐng)求,先去刪除緩存,然后將數(shù)據(jù)寫入數(shù)據(jù)庫(kù),此時(shí)客戶端B查詢先去查詢緩存緩存沒(méi)有返回,去查數(shù)據(jù)庫(kù),此時(shí)還沒(méi)有完成主從同步,拿到是從庫(kù)的舊數(shù)據(jù),然后將舊數(shù)據(jù)進(jìn)行緩存,
    的頭像 發(fā)表于 12-02 14:23 ?873次閱讀
    Redis<b class='flag-5'>緩存</b>與Mysql如何保證一致性?

    Spring Cache緩存常規(guī)配置

    作者最近在開(kāi)發(fā)公司項(xiàng)目時(shí)使用到 Redis 緩存,并在翻看前人代碼時(shí),看到了一種關(guān)于 @Cacheable 注解的自定義緩存有效期的解決方案,感覺(jué)比較實(shí)用,因此作者自己拓展完善了一番后分享給各位。
    的頭像 發(fā)表于 11-28 10:44 ?562次閱讀
    Spring Cache<b class='flag-5'>緩存</b>常規(guī)配置

    盤點(diǎn)那些強(qiáng)大又低調(diào)的Java緩存

    HashMap 是很多程序員接觸的第一種緩存 , 因?yàn)楝F(xiàn)實(shí)業(yè)務(wù)場(chǎng)景里,我們可能需要給緩存添加緩存統(tǒng)計(jì)、過(guò)期失效、淘汰策略等功能,HashMap 的功能就顯得孱弱 ,所以 HashMap 在業(yè)務(wù)系統(tǒng)中使用得并不算多。
    的頭像 發(fā)表于 11-14 18:02 ?424次閱讀
    盤點(diǎn)那些強(qiáng)大又低調(diào)的Java<b class='flag-5'>緩存</b>