最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

美團(tuán)2面:如何保障 MySQL 和 Redis 數(shù)據(jù)一致性?這樣答,虐爆面試官

2023-03-10 16:23 作者:兩年半的java練習(xí)生  | 我要投稿

最近,有個(gè)小伙伴美團(tuán),2面又遇到了這個(gè)問(wèn)題。

這里,給大家梳理一個(gè)科書(shū)式的答案。

首先,什么是Cache-Aside Pattern(旁路緩存模式)?

Cache-Aside Pattern(旁路緩存)模式,又叫旁路路由策略,在這種模式中,讀取緩存、讀取數(shù)據(jù)庫(kù)和更新緩存的操作都是在應(yīng)用程序中完成。此模式是業(yè)務(wù)系統(tǒng)最常用的緩存策略。

旁路緩存又模式分為讀緩存和寫(xiě)緩存。

旁路緩存模式在讀的時(shí)候,先讀緩存,緩存命中的話(huà),直接返回?cái)?shù)據(jù);如果緩存沒(méi)有命中的話(huà),就去讀數(shù)據(jù)庫(kù),從數(shù)據(jù)庫(kù)取出數(shù)據(jù),放入緩存后,同時(shí)返回響應(yīng)。

Cache-Aside Pattern(旁路緩存)模式讀操作流程,具體如下:

  • step 1:應(yīng)用程序接收用戶(hù)的數(shù)據(jù)查詢(xún)的請(qǐng)求;

  • step 2:應(yīng)用程序優(yōu)先從緩存查找數(shù)據(jù);

  • step 3:如果存在(cache hit),從緩存上查詢(xún)出來(lái),返回查詢(xún)到數(shù)據(jù);

  • Step 4:如果不存在(cache miss),從數(shù)據(jù)庫(kù)中查詢(xún)數(shù)據(jù)并存入緩存中,返回查詢(xún)到數(shù)據(jù)。

Cache-Aside Pattern(旁路緩存)模式讀操作流程,具體如下圖所示:


圖:Cache-Aside Pattern(旁路緩存)模式讀操作流程

Cache-Aside Pattern(旁路緩存)模式寫(xiě)操作流程,具體如下:

  • step 1:接收用戶(hù)的數(shù)據(jù)寫(xiě)入的請(qǐng)求;

  • step 2:先寫(xiě)入數(shù)據(jù)庫(kù);

  • step 3:再寫(xiě)入緩存。

Cache-Aside Pattern(旁路緩存)模式寫(xiě)操作流程,具體如下圖所示:


圖:Cache-Aside Pattern(旁路緩存)模式寫(xiě)操作流程

數(shù)據(jù)什么時(shí)候從數(shù)據(jù)庫(kù)(如Mysql集群)加載到緩存(如Redis集群)呢?有以下兩種加載模式可被選擇:懶漢模式、餓漢模式。懶漢模式、餓漢模式可以理解為及時(shí)加載模式、延遲加載模式。

所謂懶漢模式,就會(huì)在使用時(shí)臨時(shí)加載緩存。具體來(lái)說(shuō),就是當(dāng)需要使用數(shù)據(jù)時(shí),就從數(shù)據(jù)庫(kù)中把它查詢(xún)出來(lái),然后寫(xiě)入緩存。第一次查詢(xún)之后,后續(xù)的請(qǐng)求都能從緩存中查詢(xún)到數(shù)據(jù)。

所謂餓漢模式,就是提前預(yù)加載緩存。具體來(lái)說(shuō),在項(xiàng)目啟動(dòng)的時(shí)候,預(yù)加載數(shù)據(jù)到緩存。當(dāng)需要使用數(shù)據(jù)時(shí),能直接從緩存獲取數(shù)據(jù),而不需要從數(shù)據(jù)獲取。

餓漢模式,提前預(yù)加載數(shù)據(jù)到緩存的時(shí)機(jī),能極大地提升請(qǐng)求處理的性能力,極大地提升系統(tǒng)的吞吐量。此模式,適合于緩存那些不是經(jīng)常變更的數(shù)據(jù)(例如商品類(lèi)目數(shù)據(jù)),或者那些訪(fǎng)問(wèn)非常頻繁的極熱數(shù)據(jù)(例如秒殺商品數(shù)據(jù))。

Cache-Aside如何保證雙寫(xiě)的數(shù)據(jù)一致性?

Cache-Aside是日常開(kāi)發(fā)中使用最多的緩存層高并發(fā)訪(fǎng)問(wèn)模式。所以,面試官也喜歡圍繞這種模式進(jìn)行發(fā)問(wèn)。一個(gè)非常高頻的問(wèn)題是:Cache-Aside在寫(xiě)入的時(shí)候,為什么是刪除緩存而不是更新緩存呢。而且,很多大廠也喜歡問(wèn)這個(gè)領(lǐng)域的問(wèn)題,下面就是一道來(lái)自于社群的美團(tuán)真題。

美團(tuán)面試題

Cache-Aside如何保證DB和Cache雙寫(xiě)的數(shù)據(jù)一致性?

要完美的回答這個(gè)問(wèn)題,咱們把Cache-Aside模式(旁路緩存模式)下的DB和Cache雙寫(xiě)的策略,做一個(gè)系統(tǒng)化的梳理,大概分為如下五大策略。

  • 策略一:先更數(shù)據(jù)庫(kù),再更緩存

  • 策略二:先刪緩存,再更新數(shù)據(jù)庫(kù)

  • 策略三:先更數(shù)據(jù)庫(kù),再刪緩存

  • 策略四:延遲雙刪策略

  • 策略五:邏輯刪除策略

  • 策略六:先更數(shù)據(jù)庫(kù),再基于隊(duì)列刪緩存

如果能在面試的時(shí)候,把其中每一種策略的角色功能、適用場(chǎng)景、執(zhí)行流程、優(yōu)勢(shì)弱點(diǎn)、改進(jìn)策略進(jìn)行系統(tǒng)化、體系化的陳述,無(wú)論是那個(gè)廠,無(wú)論是什么頂級(jí)的大廠,一定會(huì)對(duì)候選人的能力有十分的認(rèn)可。

這里的內(nèi)容,來(lái)自于《Java高并發(fā)核心編程 卷3加強(qiáng)版》

有關(guān)6中策略的代碼實(shí)操介紹,請(qǐng)參見(jiàn) 100Wqps三級(jí)緩存組件實(shí)操

策略一:先更數(shù)據(jù)庫(kù),再更緩存

在實(shí)際的業(yè)務(wù)場(chǎng)景中,一種常見(jiàn)的并發(fā)場(chǎng)景是:微服務(wù)Provider實(shí)例A、B同時(shí)進(jìn)行同一個(gè)數(shù)據(jù)的更新操作。按照先更數(shù)據(jù)庫(kù),再更緩存的策略,則微服務(wù)Provider實(shí)例A、B可能會(huì)出現(xiàn)下面的執(zhí)行次序:

  • step 1:微服務(wù)A去執(zhí)行update DB

  • step 2:微服務(wù)B去執(zhí)行update DB

  • step 3:微服務(wù)B去執(zhí)行update Cache

  • step 4:微服務(wù)A去執(zhí)行update Cache

上面的執(zhí)行流程,具體如下圖所示:


圖:先更數(shù)據(jù)庫(kù),再更緩存的并發(fā)執(zhí)行案例

上面的執(zhí)行流程,是典型的并發(fā)寫(xiě)入場(chǎng)景。

在圖中的并發(fā)寫(xiě)入的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B也進(jìn)行數(shù)據(jù)的寫(xiě)入。

最終的結(jié)果是:DB中的數(shù)據(jù)是Provider B的數(shù)據(jù),Cache中的數(shù)據(jù)是Provider A的數(shù)據(jù),出現(xiàn)DB和Cache數(shù)據(jù)不一致問(wèn)題。

具體的原因是:Provider B的更新在Cache中的數(shù)據(jù),被Provider A的更新在Cache中的數(shù)據(jù)覆蓋了。DB的更新次序先A后B,理論上Cache中的數(shù)據(jù)更新也應(yīng)該是先A后B。理論上,最終Cache中的數(shù)據(jù)應(yīng)該是Provider B的數(shù)據(jù),而不是Provider A的數(shù)據(jù)。所以,在流程執(zhí)行完畢后,緩存中的Provider A的數(shù)據(jù)為臟數(shù)據(jù)。

而之出現(xiàn)這個(gè)問(wèn)題,是因?yàn)橐陨狭鞒讨衧tep 3與step 4的執(zhí)行均為操作緩存,都是高并發(fā)的操作,很難保證先后次序,所以緩存出現(xiàn)臟數(shù)據(jù)的概率很大。

為何不更新緩存而是刪除緩存?

核心面試題

一個(gè)非常高頻的問(wèn)題是:Cache-Aside在寫(xiě)入的時(shí)候,為什么是刪除緩存而不是更新緩存呢?

回到上一節(jié)的例子,在圖中的并發(fā)寫(xiě)入的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B也進(jìn)行數(shù)據(jù)的寫(xiě)入。

在這個(gè)例子中,寫(xiě)入DB的次序如下:

  • Provider A先發(fā)起一個(gè)寫(xiě)操作,第一步先更新數(shù)據(jù)庫(kù)

  • Provider B再發(fā)起一個(gè)寫(xiě)操作,第二步更新了數(shù)據(jù)庫(kù)

現(xiàn)在,由于分布式系統(tǒng),無(wú)法保證并發(fā)操作的有序性,寫(xiě)入Cache的次序可能如下:

  • Provider B先發(fā)起一個(gè)Cache寫(xiě)操作,第一步先更新Cache

  • Provider A再發(fā)起一個(gè)Cache寫(xiě)操作,第二步更新了Cache

這時(shí)候,Cache保存的是Provider A的數(shù)據(jù)(老數(shù)據(jù)),DB保存的是B的數(shù)據(jù)(新數(shù)據(jù)),于是發(fā)生了DB和Cache數(shù)據(jù)不一致,Cache中出現(xiàn)臟數(shù)據(jù)。

如果使用刪除操作取代更新操作,則Cache不會(huì)出現(xiàn)上面的臟數(shù)據(jù)問(wèn)題。具體如下圖所示:


圖:為何不更新緩存而是刪除緩存

除了能夠減少臟數(shù)據(jù)之外,更新緩存相對(duì)于刪除緩存,還有兩點(diǎn)劣勢(shì):

(1)如果寫(xiě)入Cache的值,是經(jīng)過(guò)復(fù)雜計(jì)算才得到的話(huà)。更新緩存頻率高的話(huà),就會(huì)大大降低性能。

(2)及時(shí)更新緩存屬于餓漢模式,適用于數(shù)據(jù)讀取高頻的場(chǎng)景。在寫(xiě)多讀少的情況下,數(shù)據(jù)很多時(shí)候還沒(méi)被讀取到,又被更新了,這也浪費(fèi)了Cache的空間,也降低了性能。

策略二:先刪緩存,再更新數(shù)據(jù)庫(kù)

在實(shí)際的業(yè)務(wù)場(chǎng)景中,一種常見(jiàn)的并發(fā)場(chǎng)景是:微服務(wù)Provider實(shí)例A進(jìn)行數(shù)據(jù)的寫(xiě)入,而服務(wù)Provider實(shí)例 B同時(shí)進(jìn)行同一個(gè)數(shù)據(jù)的讀取操作。按照先刪緩存,再更新數(shù)據(jù)庫(kù)的策略,則微服務(wù)Provider實(shí)例A、B可能會(huì)出現(xiàn)下面的執(zhí)行次序:

  • step 1:微服務(wù)A去執(zhí)行delete Cache

  • step 2:微服務(wù)B去執(zhí)行l(wèi)oad from DB

  • step 3:微服務(wù)B去執(zhí)行update Cache

  • step 4:微服務(wù)A去執(zhí)行update DB

上面的執(zhí)行流程,具體如下圖所示:


圖:先刪緩存,再更新數(shù)據(jù)庫(kù)的并發(fā)執(zhí)行案例

上面的執(zhí)行流程,是典型的并發(fā)讀寫(xiě)場(chǎng)景。

在圖中的并發(fā)讀寫(xiě)的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B進(jìn)行數(shù)據(jù)的查詢(xún)。

最終,DB中的數(shù)據(jù)是Provider A的更新數(shù)據(jù),Cache中的數(shù)據(jù)是Provider B從DB加載的數(shù)據(jù),而這個(gè)數(shù)據(jù)已經(jīng)過(guò)時(shí),出現(xiàn)DB和Cache數(shù)據(jù)不一致問(wèn)題。

具體的原因是:Provider B查詢(xún)Cache的時(shí)候,Cache中的數(shù)據(jù)被刪除,Provider B只能去DB查找,然后將數(shù)據(jù)更新在Cache。而Provider A在Provider B查完之后,竟然更新了DB,導(dǎo)致了DB和Cache的不一致。

出現(xiàn)這個(gè)DB和Cache的不一致問(wèn)題的根本原因,大致如下:

寫(xiě)操作是先刪Cache(操作1)再寫(xiě)DB(操作2),如果在此期間發(fā)生并發(fā)讀,讀取的動(dòng)作很容易發(fā)生操作1、操作2的中間,從而讀取到過(guò)時(shí)的數(shù)據(jù),最終導(dǎo)致Cache和DB不一致。更為嚴(yán)重的時(shí)候,讀操作把過(guò)期數(shù)據(jù)刷入Cache后,會(huì)導(dǎo)致后面比較長(zhǎng)時(shí)間的不一致。這個(gè)時(shí)間,一直持續(xù)到緩存過(guò)期,如說(shuō)4個(gè)小時(shí)(以項(xiàng)目中的配置時(shí)間為準(zhǔn))。

上面的Cache和DB不一致,將導(dǎo)致一個(gè)嚴(yán)重的后面:后續(xù)的讀取操作,都會(huì)使用Cache中的數(shù)據(jù),所以,后面的讀取操作都會(huì)使用過(guò)時(shí)數(shù)據(jù)。

這里的內(nèi)容,來(lái)自于《Java高并發(fā)核心編程 卷3加強(qiáng)版》 (注意,是加強(qiáng)版)的 策略2

策略2的代碼實(shí)操介紹,請(qǐng)參見(jiàn) 尼恩的 100Wqps三級(jí)緩存組件實(shí)操


策略三:先更數(shù)據(jù)庫(kù),再刪緩存

先更數(shù)據(jù)庫(kù),再刪緩存,基本上可以解決并發(fā)讀寫(xiě)場(chǎng)景中,Cache和DB數(shù)據(jù)不一致的問(wèn)題。

但是,在一些特殊的場(chǎng)景中,還是會(huì)存在數(shù)據(jù)不一致的問(wèn)題。

一種非常特殊的并發(fā)場(chǎng)景是:

微服務(wù)Provider實(shí)例A進(jìn)行數(shù)據(jù)的寫(xiě)入操作,先寫(xiě)DB(操作1),再刪Cache(操作2),如果由于某種原因出現(xiàn)了卡頓,沒(méi)有及時(shí)把數(shù)據(jù)放入Cache,或者說(shuō)放入Cache的操作,簡(jiǎn)單的說(shuō),操作2發(fā)生了滯后。

此時(shí),服務(wù)Provider實(shí)例 B進(jìn)行一個(gè)數(shù)據(jù)的讀取操作,讀取的次序仍然是先讀Cache,再讀DB,很容易發(fā)生DB和Cache的不一致性。

按照先更數(shù)據(jù)庫(kù),再刪緩存的策略,則微服務(wù)Provider實(shí)例A、B可能會(huì)出現(xiàn)下面的執(zhí)行次序:

  • step 1:微服務(wù)A去執(zhí)行update DB

  • step 2:微服務(wù)B去執(zhí)行l(wèi)oad from Cache

  • step 3:微服務(wù)A去執(zhí)行delete Cache,但是發(fā)生了延遲

上面的執(zhí)行流程,具體如下圖所示:


圖:先更數(shù)據(jù)庫(kù),再刪緩存的并發(fā)執(zhí)行案例

在圖中的并發(fā)讀寫(xiě)的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B進(jìn)行數(shù)據(jù)的查詢(xún)。

微服務(wù)Provider實(shí)例A先寫(xiě)DB(操作1),再刪Cache(操作2),如果Provider實(shí)例A發(fā)生卡頓、或者網(wǎng)絡(luò)延遲等異常的問(wèn)題,導(dǎo)致操作2嚴(yán)重滯后。在操作2執(zhí)行完成之前,DB和Cache的數(shù)據(jù)是不一致的。

在此期間,其他的數(shù)據(jù)讀取操作,都會(huì)讀取Cache中的過(guò)期數(shù)據(jù),出現(xiàn)DB和Cache數(shù)據(jù)不一致問(wèn)題。

出現(xiàn)這個(gè)DB和Cache的不一致問(wèn)題的根本原因,大致如下:

寫(xiě)操作是先寫(xiě)DB(操作1)再刪Cache(操作2),如果在此期間發(fā)生并發(fā)讀,讀操作很容易發(fā)生操作1、操作2的中間,從而,并發(fā)讀操作從Cache讀取到過(guò)時(shí)的數(shù)據(jù),最終導(dǎo)致Cache和DB不一致。

但是等到寫(xiě)操作刪除Cache(操作2)的動(dòng)作執(zhí)行完成之后,Cache和DB的數(shù)據(jù),會(huì)恢復(fù)一致性。

無(wú)論如何,策略三(先寫(xiě)DB再刪Cache),比策略二(先刪Cache再寫(xiě)DB)發(fā)生數(shù)據(jù)不一致的時(shí)間短。相比較而言,推薦大家使用策略三,而不是策略二。

那么,策略三的問(wèn)題是啥呢?

(1)寫(xiě)DB(操作1)和刪Cache(操作2)之間,存在短時(shí)間的數(shù)據(jù)不一致;

(2)如果刪Cache失敗,存在較長(zhǎng)時(shí)間的數(shù)據(jù)不一致,這個(gè)時(shí)間會(huì)一直持續(xù)到Cache過(guò)期;

如何解決策略三中Cache刪除失敗所導(dǎo)致的DB和Cache較長(zhǎng)時(shí)間的數(shù)據(jù)不一致呢?可以使用策略四:延遲雙刪。

策略四:延遲雙刪策略

什么是延遲雙刪呢?延遲雙刪是基于策略二進(jìn)行改進(jìn),就是先刪Cache,后寫(xiě)DB,最后延遲一定時(shí)間,再次刪Cache。

在實(shí)際的業(yè)務(wù)場(chǎng)景中,一種常見(jiàn)的并發(fā)場(chǎng)景是:微服務(wù)Provider實(shí)例A進(jìn)行數(shù)據(jù)的寫(xiě)入,而服務(wù)Provider實(shí)例 B同時(shí)進(jìn)行同一個(gè)數(shù)據(jù)的讀取操作。按照先刪Cache,后寫(xiě)DB,最后延遲一定時(shí)間,再次刪Cache策略,則微服務(wù)Provider實(shí)例A、B可能會(huì)出現(xiàn)下面的執(zhí)行次序:

  • step 1:微服務(wù)A去執(zhí)行delete Cache

  • step 2:微服務(wù)B去執(zhí)行l(wèi)oad from DB

  • step 3:微服務(wù)B去執(zhí)行update Cache

  • step 4:微服務(wù)A去執(zhí)行update DB

  • step 5:微服務(wù)A去執(zhí)行 delay delete Cache

上面的執(zhí)行流程,具體如下圖所示:


圖:先刪Cache,后寫(xiě)DB,再次延遲刪Cache的并發(fā)執(zhí)行案例

在圖中的并發(fā)讀寫(xiě)的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B進(jìn)行數(shù)據(jù)的查詢(xún)。

微服務(wù)Provider實(shí)例A先刪Cache(操作1),再寫(xiě)DB(操作2),最后再二次延遲刪除Cache(操作3)。在操作2之前,如果發(fā)生并發(fā)讀,從DB讀取到過(guò)時(shí)數(shù)據(jù),可能出現(xiàn)DB和Cache數(shù)據(jù)不一致問(wèn)題。

出現(xiàn)這個(gè)DB和Cache的不一致問(wèn)題的根本原因,大致如下:

寫(xiě)操作是先刪Cache(操作1)再寫(xiě)DB(操作2),如果在此期間發(fā)生并發(fā)讀,讀操作容易發(fā)生操作1、操作2的中間,從DB讀到過(guò)時(shí)數(shù)據(jù),最終導(dǎo)致Cache和DB不一致。但是,這一輪的數(shù)據(jù)不一致,持續(xù)時(shí)間不會(huì)太長(zhǎng)。為啥呢?寫(xiě)操作還有一個(gè)兜底的動(dòng)作:二次延遲刪除Cache(操作3),從而保證數(shù)據(jù)一致。

所以,延遲雙刪也會(huì)存在數(shù)據(jù)不一致,不過(guò)是持續(xù)時(shí)間比較短而已。

那么,策略四的問(wèn)題是啥呢?

(1)如果寫(xiě)操作比較頻繁,可能會(huì)對(duì)Redis造成一定的壓力;

(2)極端情況下,第二次延遲刪Cache失敗,操作的效果退化到策略二。DB和Cache存在較長(zhǎng)時(shí)間的數(shù)據(jù)不一致,這個(gè)時(shí)間會(huì)一直持續(xù)到Cache過(guò)期,比如說(shuō)4個(gè)小時(shí)(以項(xiàng)目中的配置時(shí)間為準(zhǔn))。

如何解決策略四的以上兩個(gè)問(wèn)題呢?可以使用策略五:先更數(shù)據(jù)庫(kù),再基于隊(duì)列刪緩存。

策略五:邏輯刪除/邏輯過(guò)期的問(wèn)題

首先什么是邏輯過(guò)期時(shí)間呢。

邏輯代表什么,假的刪除,不是真正的刪除。而是空間換時(shí)間,設(shè)置一些額外的標(biāo)志

比如:

在存儲(chǔ)數(shù)據(jù)的時(shí)候加個(gè)字段,比如 logicExpireTime 給它設(shè)置值。

這個(gè)值,跟我們緩存key的有效時(shí)間肯定不一樣。

比如,永不過(guò)期

比如,當(dāng)前時(shí)間再加上 幾個(gè)小時(shí) 轉(zhuǎn)為時(shí)間戳的方式跟數(shù)據(jù)一起存入redis。

邏輯過(guò)期時(shí)間= 業(yè)務(wù)過(guò)期時(shí)間

物理過(guò)期時(shí)間= 邏輯過(guò)期時(shí)間 + 高并發(fā)冗余時(shí)間

查詢(xún)的時(shí)候,檢查 logicExpireTime ,如果發(fā)現(xiàn)到時(shí)間了,

另外有一個(gè)緩存的重建線(xiàn)程,進(jìn)行異步重建

更新的時(shí)候, 更改 邏輯過(guò)期時(shí)間 = 當(dāng)前時(shí)間

策略六:先更數(shù)據(jù)庫(kù),再基于隊(duì)列刪緩存

來(lái)到策略六:先更數(shù)據(jù)庫(kù),再基于隊(duì)列刪緩存。那么,如何基于任務(wù)隊(duì)列刪緩存呢?實(shí)質(zhì)上,策略六是基于策略三進(jìn)行改進(jìn)。首先回顧一下策略三的問(wèn)題?

(1)寫(xiě)DB(操作1)和刪Cache(操作2)之間,存在短時(shí)間的數(shù)據(jù)不一致;

(2)如果刪Cache失敗,存在較長(zhǎng)時(shí)間的數(shù)據(jù)不一致,這個(gè)時(shí)間會(huì)一直持續(xù)到Cache過(guò)期;

策略六主要的操作次序,和策略三保持一致,依然是先寫(xiě)DB后刪除Cache。不同的是,策略六引入隊(duì)列,把刪Cache的操作加入隊(duì)列,后臺(tái)會(huì)有一個(gè)異步線(xiàn)程、或者進(jìn)程去異步消費(fèi)隊(duì)列中的刪除任務(wù),去執(zhí)行刪Cache的操作。

基于隊(duì)列刪緩存,可以細(xì)分為:

  • 第1種細(xì)分的方案:基于內(nèi)存隊(duì)列刪除緩存

  • 第2種細(xì)分的方案:基于消息隊(duì)列刪除緩存

  • 第3種細(xì)分的方案:基于binlog+消息隊(duì)列刪除緩存

首先來(lái)看第一種細(xì)分的方案:基于內(nèi)存隊(duì)列刪除緩存。

此策略把刪Cache的操作加入任務(wù)隊(duì)列,后臺(tái)會(huì)有一個(gè)異步線(xiàn)程去異步消費(fèi)任務(wù)隊(duì)列里面的刪除任務(wù),去執(zhí)行刪Cache的操作,如果緩存刪除失敗,可以重試多次,確保刪除成功。

在實(shí)際的業(yè)務(wù)場(chǎng)景中,一種常見(jiàn)的并發(fā)場(chǎng)景是:微服務(wù)Provider實(shí)例A進(jìn)行數(shù)據(jù)的寫(xiě)入,而服務(wù)Provider實(shí)例 B同時(shí)進(jìn)行同一個(gè)數(shù)據(jù)的讀取操作。Provider實(shí)例A先寫(xiě)DB,然后將刪Cache加入任務(wù)隊(duì)列;Provider實(shí)例 B則是先讀緩存,沒(méi)有數(shù)據(jù)再讀DB。微服務(wù)Provider實(shí)例A、B可能會(huì)出現(xiàn)下面的執(zhí)行次序:

  • step 1:微服務(wù)A去執(zhí)行update DB

  • step 2:微服務(wù)A將delete Cache操作進(jìn)入任務(wù)隊(duì)列

  • step 3:微服務(wù)B去執(zhí)行l(wèi)oad from Cache

  • step 4:消費(fèi)線(xiàn)程從任務(wù)隊(duì)列提取delete Cache操作,執(zhí)行刪除Cache的操作,直到刪除成功。

上面的執(zhí)行流程,具體如下圖所示:


圖:先更數(shù)據(jù)庫(kù),后基于內(nèi)存隊(duì)列刪緩存的并發(fā)執(zhí)行案例

在圖中的并發(fā)讀寫(xiě)的場(chǎng)景中,Provider A進(jìn)行數(shù)據(jù)的寫(xiě)入,Provider B進(jìn)行數(shù)據(jù)的查詢(xún)。

微服務(wù)Provider實(shí)例A先寫(xiě)DB(操作1),再將刪Cache操作加入任務(wù)隊(duì)列(操作2)。在刪除Cache操作真正執(zhí)行完成之前,其他的數(shù)據(jù)讀取操作,都會(huì)讀取Cache中的過(guò)期數(shù)據(jù),出現(xiàn)DB和Cache數(shù)據(jù)不一致問(wèn)題。但是這種不一致,是短暫的。任務(wù)隊(duì)列的消費(fèi)線(xiàn)程,會(huì)異步執(zhí)行刪除Cache的任務(wù),并且會(huì)不斷重試確保成功,刪除Cache之后,DB和Cache數(shù)據(jù)不一致問(wèn)題就會(huì)得到解決。

說(shuō) 明

保存刪除Cache任務(wù)的隊(duì)列,建議使用阻塞隊(duì)列。任務(wù)隊(duì)列的消費(fèi)線(xiàn)程,可參考Rocketmq源碼中的ServiceThread異步服務(wù)線(xiàn)程,其設(shè)計(jì)思想和執(zhí)行性能都非常優(yōu)越。后面尼恩會(huì)通過(guò)視頻,介紹一下基于隊(duì)列刪除緩存的實(shí)操。

策略六也會(huì)出現(xiàn)這個(gè)DB和Cache的不一致問(wèn)題,尤其是如果寫(xiě)操作非常頻繁,隊(duì)列的任務(wù)比較多,可能消費(fèi)會(huì)比較慢,導(dǎo)致DB和Cache的不一致的時(shí)間會(huì)延長(zhǎng)。在這種情況下,可以根據(jù)任務(wù)隊(duì)列的擁塞程度,開(kāi)啟多個(gè)線(xiàn)程,提升并發(fā)執(zhí)行的效率。

與策略四相比,策略六的優(yōu)勢(shì)是:

(1)在寫(xiě)操作比較頻繁的場(chǎng)景,策略四有兩次刪Cache操作,可能會(huì)對(duì)Redis造成一定的壓力;策略六只有一次刪Cache操作,Redis壓力小一半。

(2)策略四如果刪Cache失敗,沒(méi)有引入重試策略;策略六會(huì)多次重試,確保刪Cache成功,如果重試多次仍然不成功,可以執(zhí)行運(yùn)維預(yù)警。

(3)策略四將寫(xiě)DB、刪Cache這兩個(gè)操作耦合在了一起,沒(méi)有很好的做到單一職責(zé);策略六將寫(xiě)DB、刪Cache兩個(gè)操作解耦,模塊職責(zé)更加單一。

那么,策略六的問(wèn)題是啥呢?

(1)如果寫(xiě)操作非常頻繁,隊(duì)列的任務(wù)比較多,可能消費(fèi)會(huì)比較慢;需要引入多線(xiàn)程機(jī)制,加快消費(fèi)速度。

(2)程序復(fù)雜度成倍上升,引入消費(fèi)線(xiàn)程、任務(wù)隊(duì)列,并且還需要不斷進(jìn)行性能優(yōu)化。

(3)內(nèi)存隊(duì)列是JVM進(jìn)程的內(nèi)部隊(duì)列,如果JVM崩潰,內(nèi)存隊(duì)列沒(méi)有來(lái)得及處理的Cache記錄刪除任務(wù)會(huì)丟失,這些數(shù)據(jù)的Cache記錄和DB記錄會(huì)長(zhǎng)時(shí)間不一致。

其次,來(lái)看第二種細(xì)分的方案:基于消息隊(duì)列刪除緩存。

在前面的第一種細(xì)分方案中,將刪除Cache的任務(wù)保存在內(nèi)存隊(duì)列,并不是高可靠的。

為了保證高可靠的刪除Cache記錄,這里引入高可用的獨(dú)立組件——Rocketmq消息隊(duì)列。需要注意的是,這里引入的RocketMq消息隊(duì)列是高可用的類(lèi)型消息隊(duì)列,不是單節(jié)點(diǎn)的類(lèi)型消息隊(duì)列,從而保障消息記錄的高可用,保障Cache的刪除操作只要沒(méi)有被執(zhí)行成功,就不會(huì)丟失。

引入高可用RocketMq消息隊(duì)列之后,執(zhí)行雙寫(xiě)操作的Provider A的操作流程,有小幅度的調(diào)整。Provider A需要將刪除Cache的操作,序列化成Rocketmq消息,然后寫(xiě)入高可用Rocketmq消息隊(duì)列中間件即可。然后,由專(zhuān)門(mén)的消費(fèi)者(Cache Delete Consumer)進(jìn)行消息的消費(fèi),根據(jù)消息內(nèi)容執(zhí)行Cache記錄刪除工作。

DB和Redis雙寫(xiě)的場(chǎng)景下,Provider A先更數(shù)據(jù)庫(kù),后基于消息隊(duì)列刪緩存的并發(fā)執(zhí)行案例的執(zhí)行流程,具體如下圖所示:


圖:先更數(shù)據(jù)庫(kù),后基于消息隊(duì)列刪緩存的并發(fā)執(zhí)行案例

引入高可用的獨(dú)立組件RocketMq消息隊(duì)列之后,Provider A的寫(xiě)入邏輯變得很簡(jiǎn)單,刪Cache的時(shí)候,只需要發(fā)送消息到RocketMq即可,大大簡(jiǎn)化了Provider A程序的寫(xiě)入邏輯。只是為了保證消息的高可靠傳遞,這里Provider A在發(fā)送消息的時(shí)候,需要使用同步發(fā)送模式,而不能使用異步發(fā)送的模式。

在消息投遞的環(huán)節(jié),由RocketMq高可用組件的ACK機(jī)制保證消息的高可靠投遞。如果消息第一次消費(fèi)失敗,RocketMq會(huì)重復(fù)多次進(jìn)行投遞,確保消息被正常消費(fèi),如果一直不能被成功消費(fèi),在重復(fù)投遞一定的次數(shù)之后(默認(rèn)16次),消息會(huì)進(jìn)入死信隊(duì)列。系統(tǒng)的監(jiān)控程序會(huì)對(duì)死信隊(duì)列進(jìn)行監(jiān)控,一旦發(fā)現(xiàn)死信消息,監(jiān)控程序會(huì)進(jìn)行運(yùn)維告警,由運(yùn)維人員解決最終的緩存刪除問(wèn)題。除非Redis集群崩潰,一般都不會(huì)出現(xiàn)這樣的極端情況。

和基于內(nèi)存隊(duì)列刪除緩存,基于消息隊(duì)列刪除緩存的方案的優(yōu)勢(shì)是:

增加了Cache刪除的可靠性,避免了因JVM崩潰所導(dǎo)致的內(nèi)存隊(duì)列中的記錄丟失的問(wèn)題。

那么,Provider在執(zhí)行DB和Cache雙寫(xiě)時(shí),能不能進(jìn)一步減少雙寫(xiě)的負(fù)擔(dān),將發(fā)送刪除Cache消息的操作,從雙寫(xiě)邏輯中剝離,交給其他的組件去完成呢?答案是可以的。具體來(lái)說(shuō),就是使用基于基于binlog+消息隊(duì)列去刪除Cache的方案。

最后,來(lái)看第三種細(xì)分的方案:基于binlog+消息隊(duì)列刪除緩存。

以Mysql為例,可以使用阿里的Canal中間件,采集在數(shù)據(jù)寫(xiě)入Mysql時(shí)生成的binlog日志,然后將日志發(fā)送到RocketMq隊(duì)列。在消費(fèi)端,可以編寫(xiě)一個(gè)專(zhuān)門(mén)的消費(fèi)者(Cache Delete Consumer)完成緩存binlog日志訂閱,篩選出其中的更新類(lèi)型log,解析之后進(jìn)行對(duì)應(yīng)Cache的刪除操作,并且通過(guò)RocketMq隊(duì)列ACK機(jī)制確認(rèn)處理這條更新log,保證Cache刪除能夠得到最終的刪除。

DB和Redis雙寫(xiě)的場(chǎng)景下,Provider A先更數(shù)據(jù)庫(kù),后基于基于binlog+消息隊(duì)列刪除緩存的并發(fā)執(zhí)行案例的執(zhí)行流程,具體如下圖所示:


圖:先更數(shù)據(jù)庫(kù),后基于消息隊(duì)列刪緩存的并發(fā)執(zhí)行案例

基于binlog+消息隊(duì)列去刪除Cache的方案的優(yōu)勢(shì)是:

微服務(wù)Provider在執(zhí)行DB和Cache雙寫(xiě)時(shí),只需要執(zhí)行寫(xiě)入DB的操作就可以了,大大簡(jiǎn)化了微服務(wù)Provider的業(yè)務(wù)邏輯。Cache的刪除工作已經(jīng)完全被Canal、RocketMq、專(zhuān)門(mén)的消費(fèi)者(Cache Delete Consumer)三者相互結(jié)合去接管了。

如何選型呢

這么多的Cache-Aside如何保證雙寫(xiě)的數(shù)據(jù)一致性方案,改如何選型呢?只有更合適、沒(méi)有最合適。大家可以根據(jù)項(xiàng)目和團(tuán)隊(duì)的情況選擇最合適的。具體的方案選型,大家可以在高并發(fā)社群——瘋狂創(chuàng)客圈的微信群里邊交流。

這里對(duì)應(yīng)到 《Java高并發(fā)核心編程 卷3 加強(qiáng)版》的 策略5,寫(xiě)書(shū)的時(shí)候, 沒(méi)有寫(xiě)邏輯刪除策略

策略6的代碼實(shí)操介紹,請(qǐng)參見(jiàn) 尼恩的 100Wqps三級(jí)緩存組件實(shí)操

基于隊(duì)列的方案,主要有兩種:

  • 第2種細(xì)分的方案:基于消息隊(duì)列刪除緩存

  • 第3種細(xì)分的方案:基于binlog+消息隊(duì)列刪除緩存

如何選型呢

很多小伙伴選擇基于binlog+消息隊(duì)列刪除緩存, 但是這種方案, 在很多場(chǎng)景下是不可以使用的。具體原因比較復(fù)雜,請(qǐng)參考尼恩的 100Wqps三級(jí)緩存組件實(shí)操,里邊有詳細(xì)的介紹。

從CAP視角分析DB與Cache的數(shù)據(jù)一致性

CAP理論作為分布式系統(tǒng)的基礎(chǔ)理論,它描述的是一個(gè)分布式系統(tǒng)在以下三個(gè)特性中:

  • 一致性(Consistency)

  • 可用性(Availability)

  • 分區(qū)容錯(cuò)性(Partition tolerance)

分布式系統(tǒng)最多滿(mǎn)足其中的兩個(gè)特性:要么滿(mǎn)足CA,要么CP,要么AP,無(wú)法同時(shí)滿(mǎn)足CAP。也就是說(shuō)AP和CP是一組天敵,要滿(mǎn)足AP高性能,只能舍棄CP。

在DB和Cache的分布式架構(gòu)中,加入分布式Cache的目的是為了獲得高性能、高吞吐,就是為了獲得分布式系統(tǒng)的AP特性。所以,如果需要數(shù)據(jù)庫(kù)和緩存數(shù)據(jù)保持強(qiáng)一致(強(qiáng)CP特性),就不適合使用緩存。

所以,從CAP的理論出發(fā),使用緩存提升性能,就是會(huì)有數(shù)據(jù)更新的延遲,就會(huì)產(chǎn)生數(shù)據(jù)的不一致。

使用分布式Cache,可以通過(guò)一些方案優(yōu)化,保證弱一致性,最終一致性的。我們只能通過(guò)不斷的方案迭代,減少不一致性的時(shí)間長(zhǎng)度。這需要Cache設(shè)計(jì)時(shí):結(jié)合業(yè)務(wù)仔細(xì)思考是否適合用緩存;結(jié)合業(yè)務(wù)仔細(xì)思考緩存過(guò)期時(shí)間。

緩存一定要設(shè)置過(guò)期時(shí)間,這個(gè)時(shí)間太短、或者太長(zhǎng)都不好。

如果過(guò)期時(shí)間太短,請(qǐng)求可能會(huì)比較多的落到數(shù)據(jù)庫(kù)上,這也意味著失去了緩存的優(yōu)勢(shì)。如果過(guò)期時(shí)間太長(zhǎng),緩存中的臟數(shù)據(jù)會(huì)使系統(tǒng)長(zhǎng)時(shí)間處于一個(gè)延遲的狀態(tài),而且,系統(tǒng)中長(zhǎng)時(shí)間沒(méi)有人訪(fǎng)問(wèn)的數(shù)據(jù)一直存在內(nèi)存中不過(guò)期,浪費(fèi)內(nèi)存。

為啥DB和Cache沒(méi)有辦法強(qiáng)一致呢?

主要是寫(xiě)DB和刪Cache是兩個(gè)獨(dú)立的操作,兩個(gè)操作并沒(méi)有保證原子性。如果一定要強(qiáng)CP,就需要非常復(fù)雜的低性能方案保證寫(xiě)DB和刪Cache兩個(gè)操作的原子性,比如引入分布式鎖,并且需要引入CP類(lèi)型的Zookeeper分布式鎖,或者引入CP類(lèi)型的Redis RedLock,而不是引入AP類(lèi)型的普通Redis分布式鎖。

所以,如果一定要強(qiáng)CP,就需要非常復(fù)雜的低性能方案,有點(diǎn)得不償失。

說(shuō)在后面

如果遇到難題,可以來(lái)找尼恩,給大家做起底式、絞殺式、系統(tǒng)化梳理, 幫大家真正讓面試官愛(ài)到死去活來(lái)。


美團(tuán)2面:如何保障 MySQL 和 Redis 數(shù)據(jù)一致性?這樣答,虐爆面試官的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
镇沅| 台北市| 鄂伦春自治旗| 桐柏县| 陈巴尔虎旗| 商洛市| 和林格尔县| 神农架林区| 桑植县| 肃宁县| 汶上县| 大足县| 贺州市| 江华| 彭泽县| 房产| 陆良县| 三亚市| 隆尧县| 怀宁县| 双江| 肇源县| 阿克| 铁力市| 卓尼县| 湘阴县| 遵义县| 东丰县| 麻栗坡县| 峡江县| 新晃| 肃宁县| 饶平县| 霸州市| 宁海县| 万山特区| 宜兴市| 安宁市| 蓬溪县| 景宁| 志丹县|