1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        萬(wàn)字圖文講透數(shù)據(jù)庫(kù)緩存一致性問(wèn)題

        共 7567字,需瀏覽 16分鐘

         ·

        2022-11-19 02:30

        點(diǎn)個(gè)關(guān)注??跟騰訊工程師學(xué)技術(shù)
        導(dǎo)語(yǔ)| 緩存合理使用確提升了系統(tǒng)的吞吐量和穩(wěn)定性,然而這是有代價(jià)的。這個(gè)代價(jià)便是緩存和數(shù)據(jù)庫(kù)的一致性帶來(lái)了挑戰(zhàn),本文將針對(duì)最常見的cache-aside策略下如何維護(hù)緩存一致性徹底講透。


        客觀上,我們的業(yè)務(wù)規(guī)模很可能要求著更高的 QPS,有些業(yè)務(wù)的規(guī)模本身就非常大,也有些業(yè)務(wù)會(huì)遇到一些流量高峰,比如電商會(huì)遇到大促的情況。


        而這時(shí)候大部分的流量實(shí)際上都是讀請(qǐng)求,而且大部分?jǐn)?shù)據(jù)也是沒有那么多變化的,如熱門商品信息、微博的內(nèi)容等常見數(shù)據(jù)就是如此。此時(shí),緩存就是我們應(yīng)對(duì)此類場(chǎng)景的利器。



        緩存的意義


        所謂緩存,實(shí)際上就是用空間換時(shí)間,準(zhǔn)確地說(shuō)是用更高速的空間來(lái)?yè)Q時(shí)間,從而整體上提升讀的性能。


        何為更高速的空間呢?


        更快的存儲(chǔ)介質(zhì)。通常情況下,如果說(shuō)數(shù)據(jù)庫(kù)的速度慢,就得用更快的存儲(chǔ)組件去替代它,目前最常見的就是Redis(內(nèi)存存儲(chǔ))。Redis 單實(shí)例的讀 QPS 可以高達(dá) 10w/s,90% 的場(chǎng)景下只需要正確使用 Redis 就能應(yīng)對(duì)。


        就近使用本地內(nèi)存。就像 CPU 也有高速緩存一樣,緩存也可以分為一級(jí)緩存、二級(jí)緩存。即便 Redis 本身性能已經(jīng)足夠高了,但訪問(wèn)一次 Redis 畢竟也需要一次網(wǎng)絡(luò) IO,而使用本地內(nèi)存無(wú)疑有更快的速度。不過(guò)單機(jī)的內(nèi)存是十分有限的,所以這種一級(jí)緩存只能存儲(chǔ)非常少量的數(shù)據(jù),通常是最熱點(diǎn)的那些 key 對(duì)應(yīng)的數(shù)據(jù)。這就相當(dāng)于額外消耗寶貴的服務(wù)內(nèi)存去換取高速的讀取性能。



        引入緩存后的一致性挑戰(zhàn)


        用空間換時(shí)間,意味著數(shù)據(jù)同時(shí)存在于多個(gè)空間。最常見的場(chǎng)景就是數(shù)據(jù)同時(shí)存在于 Redis 與 MySQL 上(為了問(wèn)題的普適性,后面舉例中若沒有特別說(shuō)明,緩存均指 Redis 緩存)。


        實(shí)際上,最權(quán)威最全的數(shù)據(jù)還是在 MySQL 里的。而萬(wàn)一 Redis數(shù)據(jù)沒有得到及時(shí)的更新(例如數(shù)據(jù)庫(kù)更新了沒更新到Redis),就出現(xiàn)了數(shù)據(jù)不一致。


        大部分情況下,只要使用了緩存,就必然會(huì)有不一致的情況出現(xiàn),只是說(shuō)這個(gè)不一致的時(shí)間窗口是否能做到足夠的小。有些不合理的設(shè)計(jì)可能會(huì)導(dǎo)致數(shù)據(jù)持續(xù)不一致,這是我們需要改善設(shè)計(jì)去避免的。


        這里的一致性實(shí)際上對(duì)于本地緩存也是同理的,例如數(shù)據(jù)庫(kù)更新后沒有及時(shí)更新本地緩存,也是有一致性問(wèn)題的,下文統(tǒng)一以Redis緩存作為引子講述,實(shí)際上處理本地緩存原理基本一致。


        (一)緩存不一致性無(wú)法客觀地完全消滅


        為什么我們幾乎沒辦法做到緩存和數(shù)據(jù)庫(kù)之間的強(qiáng)一致呢?


        理想情況下,我們需要在數(shù)據(jù)庫(kù)更新完后把對(duì)應(yīng)的最新數(shù)據(jù)同步到緩存中,以便在讀請(qǐng)求的時(shí)候能讀到新的數(shù)據(jù)而不是舊的數(shù)據(jù)(臟數(shù)據(jù))。但是很可惜,由于數(shù)據(jù)庫(kù)和 Redis 之間是沒有事務(wù)保證的,所以我們無(wú)法確保寫入數(shù)據(jù)庫(kù)成功后,寫入 Redis 也是一定成功的;即便 Redis 寫入能成功,在數(shù)據(jù)庫(kù)寫入成功后到 Redis 寫入成功前的這段時(shí)間里,Redis 數(shù)據(jù)也肯定是和 MySQL 不一致的。如下兩圖所示:


        無(wú)法事務(wù)保持一致


        所以說(shuō)這個(gè)時(shí)間窗口是沒辦法完全消滅的,除非我們付出極大的代價(jià),使用分布式事務(wù)等各種手段去維持強(qiáng)一致,但是這樣會(huì)使得系統(tǒng)的整體性能大幅度下降,甚至比不用緩存還慢,這樣不就與我們使用緩存的目標(biāo)背道而馳了嗎?


        不過(guò)雖然無(wú)法做到強(qiáng)一致,但是我們能做到的是緩存與數(shù)據(jù)庫(kù)達(dá)到最終一致,而且不一致的時(shí)間窗口我們能做到盡可能短,按照經(jīng)驗(yàn)來(lái)說(shuō),如果能將時(shí)間優(yōu)化到 1ms 之內(nèi),這個(gè)一致性問(wèn)題帶來(lái)的影響我們就可以忽略不計(jì)。




        更新緩存的手段


        通常情況我們?cè)?/span>處理查詢請(qǐng)求的時(shí)候,使用緩存的邏輯如下:


        data = queryDataRedis(key);if (data ==null) {     data = queryDataMySQL(key); //緩存查詢不到,從MySQL做查詢     if (data!=null) {         updateRedis(key, data);//查詢完數(shù)據(jù)后更新MySQL最新數(shù)據(jù)到Redis     }}


        也就是說(shuō)優(yōu)先查詢緩存,查詢不到才查詢數(shù)據(jù)庫(kù)。如果這時(shí)候數(shù)據(jù)庫(kù)查到數(shù)據(jù)了,就將緩存的數(shù)據(jù)進(jìn)行更新。這是我們常說(shuō)的cache aside的策略,也是最常用的策略。


        這樣的邏輯是正確的,而一致性的問(wèn)題一般不來(lái)源于此,而是出現(xiàn)在處理寫請(qǐng)求的時(shí)候。所以我們簡(jiǎn)化成最簡(jiǎn)單的寫請(qǐng)求的邏輯,此時(shí)你可能會(huì)面臨多個(gè)選擇,究竟是直接更新緩存,還是失效緩存?而無(wú)論是更新緩存還是失效緩存,都可以選擇在更新數(shù)據(jù)庫(kù)之前,還是之后操作。


        這樣就演變出 4 個(gè)策略:更新數(shù)據(jù)庫(kù)后更新緩存、更新數(shù)據(jù)庫(kù)前更新緩存、更新數(shù)據(jù)庫(kù)后刪除緩存、更新數(shù)據(jù)庫(kù)前刪除緩存。下面我們來(lái)分別講述。


        (一)更新數(shù)據(jù)庫(kù)后更新緩存的不一致問(wèn)題


        一種常見的操作是,設(shè)置一個(gè)過(guò)期時(shí)間,讓寫請(qǐng)求以數(shù)據(jù)庫(kù)為準(zhǔn),過(guò)期后,讀請(qǐng)求同步數(shù)據(jù)庫(kù)中的最新數(shù)據(jù)給緩存。那么在加入了過(guò)期時(shí)間后,是否就不會(huì)有問(wèn)題了呢?并不是這樣。


        大家設(shè)想一下這樣的場(chǎng)景。


        假如這里有一個(gè)計(jì)數(shù)器,把數(shù)據(jù)庫(kù)自減 1,原始數(shù)據(jù)庫(kù)數(shù)據(jù)是 100,同時(shí)有兩個(gè)寫請(qǐng)求申請(qǐng)計(jì)數(shù)減一,假設(shè)線程 A 先減數(shù)據(jù)庫(kù)成功,線程 B 后減數(shù)據(jù)庫(kù)成功。那么這時(shí)候數(shù)據(jù)庫(kù)的值是 98,緩存里正確的值應(yīng)該也要是 98。


        但是特殊場(chǎng)景下,你可能會(huì)遇到這樣的情況:


        線程 A 和線程 B 同時(shí)更新這個(gè)數(shù)據(jù)

        更新數(shù)據(jù)庫(kù)的順序是先 A 后 B

        更新緩存時(shí)順序是先 B 后 A

        如果我們的代碼邏輯還是更新數(shù)據(jù)庫(kù)后立刻更新緩存的數(shù)據(jù),那么——

        updateMySQL();updateRedis(key, data);


        就可能出現(xiàn):數(shù)據(jù)庫(kù)的值是 100->99->98,但是緩存的數(shù)據(jù)卻是 100->98->99,也就是數(shù)據(jù)庫(kù)與緩存的不一致。而且這個(gè)不一致只能等到下一次數(shù)據(jù)庫(kù)更新或者緩存失效才可能修復(fù)。


        時(shí)間

        線程A(寫請(qǐng)求)

        線程B(寫請(qǐng)求)

        問(wèn)題

        T1

        更新數(shù)據(jù)庫(kù)為99



        T2


        更新數(shù)據(jù)庫(kù)為98


        T3


        更新緩存數(shù)據(jù)為98


        T4

        更新緩存數(shù)據(jù)為99


        此時(shí)緩存的值被顯式更新為99,但是實(shí)際上數(shù)據(jù)庫(kù)的值已經(jīng)是98,數(shù)據(jù)不一致


        當(dāng)然,如果更新Redis本身是失敗的話,兩邊的值固然也是不一致的,這個(gè)前文也闡述過(guò),幾乎無(wú)法根除。




        (二)更新數(shù)據(jù)庫(kù)前更新緩存的不一致問(wèn)題


        那你可能會(huì)想,這是否表示,我應(yīng)該先讓緩存更新,之后再去更新數(shù)據(jù)庫(kù)呢?類似這樣:


        updateRedis(key, data);//先更新緩存updateMySQL();//再更新數(shù)據(jù)庫(kù)

        這樣操作產(chǎn)生的問(wèn)題更是顯而易見的,因?yàn)槲覀儫o(wú)法保證數(shù)據(jù)庫(kù)的更新成功,萬(wàn)一數(shù)據(jù)庫(kù)更新失敗了,你緩存的數(shù)據(jù)就不只是臟數(shù)據(jù),而是錯(cuò)誤數(shù)據(jù)了。


        你可能會(huì)想,是否我在更新數(shù)據(jù)庫(kù)失敗的時(shí)候做 Redis 回滾的操作能夠解決呢?這其實(shí)也是不靠譜的,因?yàn)槲覀円膊荒鼙WC這個(gè)回滾的操作 100% 被成功執(zhí)行。

        同時(shí),在寫寫并發(fā)的場(chǎng)景下,同樣有類似的一致性問(wèn)題,請(qǐng)看以下情況:


        • 線程 A 和線程 B 同時(shí)更新同這個(gè)數(shù)據(jù)


        • 更新緩存的順序是先 A 后 B


        • 更新數(shù)據(jù)庫(kù)的順序是先 B 后 A


        舉個(gè)例子。線程 A 希望把計(jì)數(shù)器置為 0,線程 B 希望置為 1。而按照以上場(chǎng)景,緩存確實(shí)被設(shè)置為 1,但數(shù)據(jù)庫(kù)卻被設(shè)置為 0。


        時(shí)間

        線程A(寫請(qǐng)求)

        線程B(寫請(qǐng)求)

        問(wèn)題

        T1

        更新緩存為0



        T2


        更新緩存為1


        T3


        更新數(shù)據(jù)庫(kù)為1


        T4

        更新數(shù)據(jù)庫(kù)數(shù)據(jù)為0


        此時(shí)緩存的值被顯式更新為1,但是實(shí)際上數(shù)據(jù)庫(kù)的值是0,數(shù)據(jù)不一致


        所以通常情況下,更新緩存再更新數(shù)據(jù)庫(kù)是我們應(yīng)該避免使用的一種手段。



        (三)更新數(shù)據(jù)庫(kù)前刪除緩存的問(wèn)題


        那如果采取刪除緩存的策略呢?也就是說(shuō)我們?cè)诟聰?shù)據(jù)庫(kù)的時(shí)候失效對(duì)應(yīng)的緩存,讓緩存在下次觸發(fā)讀請(qǐng)求時(shí)進(jìn)行更新,是否會(huì)更好呢?同樣地,針對(duì)在更新數(shù)據(jù)庫(kù)前和數(shù)據(jù)庫(kù)后這兩個(gè)刪除時(shí)機(jī),我們來(lái)比較下其差異。


        最直觀的做法,我們可能會(huì)先讓緩存失效,然后去更新數(shù)據(jù)庫(kù),代碼邏輯如下:


        deleteRedis(key);//先刪除緩存讓緩存失效updateMySQL();//再更新數(shù)據(jù)庫(kù)


        這樣的邏輯看似沒有問(wèn)題,畢竟刪除緩存后即便數(shù)據(jù)庫(kù)更新失敗了,也只是緩存上沒有數(shù)據(jù)而已。然后并發(fā)兩個(gè)寫請(qǐng)求過(guò)來(lái),無(wú)論怎么樣的執(zhí)行順序,緩存最后的值也都是會(huì)被刪除的,也就是說(shuō)在并發(fā)寫寫的請(qǐng)求下這樣的處理是沒問(wèn)題的。


        然而,這種處理在讀寫并發(fā)的場(chǎng)景下卻存在著隱患。


        還是剛剛更新計(jì)數(shù)的例子。例如現(xiàn)在緩存的數(shù)據(jù)是 100,數(shù)據(jù)庫(kù)也是 100,這時(shí)候需要對(duì)此計(jì)數(shù)減 1,減成功后,數(shù)據(jù)庫(kù)應(yīng)該是 99。如果這之后觸發(fā)讀請(qǐng)求,緩存如果有效的話,里面應(yīng)該也要被更新為 99 才是正確的。


        那么思考下這樣的請(qǐng)求情況:


        線程 A 更新這個(gè)數(shù)據(jù)的同時(shí),線程 B 讀取這個(gè)數(shù)據(jù)


        線程 A 成功刪除了緩存里的老數(shù)據(jù),這時(shí)候線程 B 查詢數(shù)據(jù)發(fā)現(xiàn)緩存失效


        線程 A 更新數(shù)據(jù)庫(kù)成功


        時(shí)間

        線程A(寫請(qǐng)求)

        線程B(讀請(qǐng)求)

        問(wèn)題

        T1

        刪除緩存值



        T2


        1.讀取緩存數(shù)據(jù),緩存缺失,從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)100


        T3

        更新數(shù)據(jù)庫(kù)中的數(shù)據(jù)X的值為99



        T4


        將數(shù)據(jù)100的值寫入緩存

        此時(shí)緩存的值被顯式更新為100,但是實(shí)際上數(shù)據(jù)庫(kù)的值已經(jīng)是99了


        可以看到,在讀寫并發(fā)的場(chǎng)景下,一樣會(huì)有不一致的問(wèn)題。


        針對(duì)這種場(chǎng)景,有個(gè)做法是所謂的“延遲雙刪策略”,就是說(shuō),既然可能因?yàn)樽x請(qǐng)求把一個(gè)舊的值又寫回去,那么我在寫請(qǐng)求處理完之后,等到差不多的時(shí)間延遲再重新刪除這個(gè)緩存值。


        時(shí)間

        線程A(寫請(qǐng)求

        線程C(新的讀請(qǐng)求)

        線程D(新的讀請(qǐng)求)

        問(wèn)題

        T5

        sleep(N)

        緩存存在,讀取到緩存舊值100


        其他線程可能在雙刪成功前讀到臟數(shù)據(jù)

        T6

        刪除緩存值




        T7



        緩存缺失,從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)的最新值(99)



        這種解決思路的關(guān)鍵在于對(duì) N 的時(shí)間的判斷,如果 N 時(shí)間太短,線程 A 第二次刪除緩存的時(shí)間依舊早于線程 B 把臟數(shù)據(jù)寫回緩存的時(shí)間,那么相當(dāng)于做了無(wú)用功。而 N 如果設(shè)置得太長(zhǎng),那么在觸發(fā)雙刪之前,新請(qǐng)求看到的都是臟數(shù)據(jù)。



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


        那如果我們把更新數(shù)據(jù)庫(kù)放在刪除緩存之前呢,問(wèn)題是否解決?我們繼續(xù)從讀寫并發(fā)的場(chǎng)景看下去,有沒有類似的問(wèn)題。


        時(shí)間

        線程A(寫請(qǐng)求)

        線程B(讀請(qǐng)求)

        線程C(讀請(qǐng)求)

        潛在問(wèn)題

        T1

        更新主庫(kù) X = 99(原值 X = 100)




        T2



        讀取數(shù)據(jù),查詢到緩存還有數(shù)據(jù),返回100

        線程C實(shí)際上讀取到了和數(shù)據(jù)庫(kù)不一致的數(shù)據(jù)

        T3

        刪除緩存




        T4


        查詢緩存,緩存缺失,查詢數(shù)據(jù)庫(kù)得到當(dāng)前值99



        T5


        99寫入緩存




        可以看到,大體上,采取先更新數(shù)據(jù)庫(kù)再刪除緩存的策略是沒有問(wèn)題的,僅在更新數(shù)據(jù)庫(kù)成功到緩存刪除之間的時(shí)間差內(nèi)——[T2,T3)的窗口 ,可能會(huì)被別的線程讀取到老值。


        而在開篇的時(shí)候我們說(shuō)過(guò),緩存不一致性的問(wèn)題無(wú)法在客觀上完全消滅,因?yàn)槲覀儫o(wú)法保證數(shù)據(jù)庫(kù)和緩存的操作是一個(gè)事務(wù)里的,而我們能做到的只是盡量縮短不一致的時(shí)間窗口。


        在更新數(shù)據(jù)庫(kù)后刪除緩存這個(gè)場(chǎng)景下,不一致窗口僅僅是 T2 到 T3 的時(shí)間,內(nèi)網(wǎng)狀態(tài)下通常不過(guò) 1ms,在大部分業(yè)務(wù)場(chǎng)景下我們都可以忽略不計(jì)。因?yàn)榇蟛糠智闆r下一個(gè)用戶的請(qǐng)求很難能再1ms內(nèi)快速發(fā)起第二次。


        但是真實(shí)場(chǎng)景下,還是會(huì)有一個(gè)情況存在不一致的可能性,這個(gè)場(chǎng)景是讀線程發(fā)現(xiàn)緩存不存在,于是讀寫并發(fā)時(shí),讀線程回寫進(jìn)去老值。并發(fā)情況如下:


        時(shí)間

        線程A(寫請(qǐng)求)

        線程B(讀請(qǐng)求--緩存不存在場(chǎng)景)

        潛在問(wèn)題

        T1


        查詢緩存,緩存缺失,查詢數(shù)據(jù)庫(kù)得到當(dāng)前值100


        T2

        更新主庫(kù) X = 99(原值 X = 100)



        T3

        刪除緩存



        T4


        將100寫入緩存

        此時(shí)緩存的值被顯式更新為100,但是實(shí)際上數(shù)據(jù)庫(kù)的值已經(jīng)是99了


        總的來(lái)說(shuō),這個(gè)不一致場(chǎng)景出現(xiàn)條件非常嚴(yán)格,因?yàn)椴l(fā)量很大時(shí),緩存不太可能不存在;如果并發(fā)很大,而緩存真的不存在,那么很可能是這時(shí)的寫場(chǎng)景很多,因?yàn)閷憟?chǎng)景會(huì)刪除緩存。


        所以待會(huì)我們會(huì)提到,寫場(chǎng)景很多時(shí)候?qū)嶋H上并不適合采取刪除策略。



        (五)總結(jié)四種更新策略


        終上所述,我們對(duì)比了四個(gè)更新緩存的手段,做一個(gè)總結(jié)對(duì)比,其中應(yīng)對(duì)方案也提供參考,具體不做展開,如下表:


        策略

        并發(fā)場(chǎng)景

        潛在問(wèn)題

        應(yīng)對(duì)方案

        更新數(shù)據(jù)庫(kù)+更新緩存

        寫+讀

        線程A未更新完緩存之前,線程B的讀請(qǐng)求會(huì)短暫讀到舊值

        可以忽略

        寫+寫

        更新數(shù)據(jù)庫(kù)的順序是先A后B,但更新緩存時(shí)順序是先B后A,數(shù)據(jù)庫(kù)和緩存數(shù)據(jù)不一致

        分布式鎖(操作重)

        更新緩存+更新數(shù)據(jù)庫(kù)

        無(wú)并發(fā)

        線程A還未更新完緩存但是更新數(shù)據(jù)庫(kù)可能失敗

        利用MQ確認(rèn)數(shù)據(jù)庫(kù)更新成功(較復(fù)雜)

        寫+寫

        更新緩存的順序是先A后B,但更新數(shù)據(jù)庫(kù)時(shí)順序是先B后A

        分布式鎖(操作很重)

        刪除緩存值+更新數(shù)據(jù)庫(kù)

        寫+讀

        寫請(qǐng)求的線程A刪除了緩存在更新數(shù)據(jù)庫(kù)之前,這時(shí)候讀請(qǐng)求線程B到來(lái),因?yàn)榫彺嫒笔?,則把當(dāng)前數(shù)據(jù)讀取出來(lái)放到緩存,而后線程A更新成功了數(shù)據(jù)庫(kù)

        延遲雙刪(但是延遲的時(shí)間不好估計(jì),且延遲的過(guò)程中依舊有不一致的時(shí)間窗口)

        更新數(shù)據(jù)庫(kù)+刪除緩存值

        寫+讀(緩存命中)

        線程A完成數(shù)據(jù)庫(kù)更新成功后,尚未刪除緩存,線程B有并發(fā)讀請(qǐng)求會(huì)讀到舊的臟數(shù)據(jù)

        可以忽略

        寫+讀(緩存不命中)

        讀請(qǐng)求不命中緩存,寫請(qǐng)求處理完之后讀請(qǐng)求才回寫緩存,此時(shí)緩存不一致

        分布式鎖(操作重)


        從一致性的角度來(lái)看,采取更新數(shù)據(jù)庫(kù)后刪除緩存值,是更為適合的策略。因?yàn)槌霈F(xiàn)不一致的場(chǎng)景的條件更為苛刻,概率相比其他方案更低。


        那么是否更新緩存這個(gè)策略就一無(wú)是處呢?不是的!


        刪除緩存值意味著對(duì)應(yīng)的 key 會(huì)失效,那么這時(shí)候讀請(qǐng)求都會(huì)打到數(shù)據(jù)庫(kù)。如果這個(gè)數(shù)據(jù)的寫操作非常頻繁,就會(huì)導(dǎo)致緩存的作用變得非常小。而如果這時(shí)候某些 Key 還是非常大的熱 key,就可能因?yàn)榭覆蛔?shù)據(jù)量而導(dǎo)致系統(tǒng)不可用。


        如下圖所示:


        刪除策略頻繁的緩存失效導(dǎo)致讀請(qǐng)求無(wú)法利用緩存


        所以做個(gè)簡(jiǎn)單總結(jié),足以適應(yīng)絕大部分的互聯(lián)網(wǎng)開發(fā)場(chǎng)景的決策:


        針對(duì)大部分讀多寫少場(chǎng)景,建議選擇更新數(shù)據(jù)庫(kù)后刪除緩存的策略。


        針對(duì)讀寫相當(dāng)或者寫多讀少的場(chǎng)景,建議選擇更新數(shù)據(jù)庫(kù)后更新緩存的策略。



        最終一致性如何保證?


        緩存設(shè)置過(guò)期時(shí)間


        第一個(gè)方法便是我們上面提到的,當(dāng)我們無(wú)法確定 MySQL 更新完成后,緩存的更新/刪除一定能成功,例如 Redis 掛了導(dǎo)致寫入失敗了,或者當(dāng)時(shí)網(wǎng)絡(luò)出現(xiàn)故障,更常見的是服務(wù)當(dāng)時(shí)剛好發(fā)生重啟了,沒有執(zhí)行這一步的代碼。

        這些時(shí)候 MySQL 的數(shù)據(jù)就無(wú)法刷到 Redis 了。為了避免這種不一致性永久存在,使用緩存的時(shí)候,我們必須要給緩存設(shè)置一個(gè)過(guò)期時(shí)間,例如 1 分鐘,這樣即使出現(xiàn)了更新 Redis 失敗的極端場(chǎng)景,不一致的時(shí)間窗口最多也只是 1 分鐘。

        這是我們最終一致性的兜底方案,萬(wàn)一出現(xiàn)任何情況的不一致問(wèn)題,最后都能通過(guò)緩存失效后重新查詢數(shù)據(jù)庫(kù),然后回寫到緩存,來(lái)做到緩存與數(shù)據(jù)庫(kù)的最終一致。



        如何減少緩存刪除/更新的失?。?/span>


        萬(wàn)一刪除緩存這一步因?yàn)榉?wù)重啟沒有執(zhí)行,或者 Redis 臨時(shí)不可用導(dǎo)致刪除緩存失敗了,就會(huì)有一個(gè)較長(zhǎng)的時(shí)間(緩存的剩余過(guò)期時(shí)間)是數(shù)據(jù)不一致的。


        那我們有沒有什么手段來(lái)減少這種不一致的情況出現(xiàn)呢?這時(shí)候借助一個(gè)可靠的消息中間件就是一個(gè)不錯(cuò)的選擇。


        因?yàn)橄⒅虚g件有 ATLEAST-ONCE 的機(jī)制,如下圖所示。



        我們把刪除 Redis 的請(qǐng)求以消費(fèi) MQ 消息的手段去失效對(duì)應(yīng)的 Key 值,如果 Redis 真的存在異常導(dǎo)致無(wú)法刪除成功,我們依舊可以依靠 MQ 的重試機(jī)制來(lái)讓最終 Redis 對(duì)應(yīng)的 Key 失效。


        而你們或許會(huì)問(wèn),極端場(chǎng)景下,是否存在更新數(shù)據(jù)庫(kù)后 MQ 消息沒發(fā)送成功,或者沒機(jī)會(huì)發(fā)送出去機(jī)器就重啟的情況?


        這個(gè)場(chǎng)景的確比較麻煩,如果 MQ 使用的是 RocketMQ,我們可以借助 RocketMQ 的事務(wù)消息,來(lái)讓刪除緩存的消息最終一定發(fā)送出去。而如果你沒有使用 RocketMQ,或者你使用的消息中間件并沒有事務(wù)消息的特性,則可以采取消息表的方式讓更新數(shù)據(jù)庫(kù)和發(fā)送消息一起成功。事實(shí)上這個(gè)話題比較大了,我們不在這里展開。


        如何處理復(fù)雜的多緩存場(chǎng)景?


        有些時(shí)候,真實(shí)的緩存場(chǎng)景并不是數(shù)據(jù)庫(kù)中的一個(gè)記錄對(duì)應(yīng)一個(gè) Key 這么簡(jiǎn)單,有可能一個(gè)數(shù)據(jù)庫(kù)記錄的更新會(huì)牽扯到多個(gè) Key 的更新。還有另外一個(gè)場(chǎng)景是,更新不同的數(shù)據(jù)庫(kù)的記錄時(shí)可能需要更新同一個(gè) Key 值,這常見于一些 App 首頁(yè)數(shù)據(jù)的緩存。


        我們以一個(gè)數(shù)據(jù)庫(kù)記錄對(duì)應(yīng)多個(gè) Key 的場(chǎng)景來(lái)舉例。


        假如系統(tǒng)設(shè)計(jì)上我們緩存了一個(gè)粉絲的主頁(yè)信息、主播打賞榜 TOP10 的粉絲、單日 TOP 100 的粉絲等多個(gè)信息。如果這個(gè)粉絲注銷了,或者這個(gè)粉絲觸發(fā)了打賞的行為,上面多個(gè) Key 可能都需要更新。只是一個(gè)打賞的記錄,你可能就要做:


        updateMySQL();//更新數(shù)據(jù)庫(kù)一條記錄deleteRedisKey1();//失效主頁(yè)信息的緩存updateRedisKey2();//更新打賞榜TOP10deleteRedisKey3();//更新單日打賞榜TOP100

        這就涉及多個(gè) Redis 的操作,每一步都可能失敗,影響到后面的更新。甚至從系統(tǒng)設(shè)計(jì)上,更新數(shù)據(jù)庫(kù)可能是單獨(dú)的一個(gè)服務(wù),而這幾個(gè)不同的 Key 的緩存維護(hù)卻在不同的 3 個(gè)微服務(wù)中,這就大大增加了系統(tǒng)的復(fù)雜度和提高了緩存操作失敗的可能性。最可怕的是,操作更新記錄的地方很大概率不只在一個(gè)業(yè)務(wù)邏輯中,而是散發(fā)在系統(tǒng)各個(gè)零散的位置。


        針對(duì)這個(gè)場(chǎng)景,解決方案和上文提到的保證最終一致性的操作一樣,就是把更新緩存的操作以 MQ 消息的方式發(fā)送出去,由不同的系統(tǒng)或者專門的一個(gè)系統(tǒng)進(jìn)行訂閱,而做聚合的操作。如下圖:


        不同業(yè)務(wù)系統(tǒng)訂閱MQ消息單獨(dú)維護(hù)各自的緩存Key

        專門更新緩存的服務(wù)訂閱MQ消息維護(hù)所有相關(guān)Key的緩存操作



        通過(guò)訂閱MySQL binlog的方式處理緩存


        上面講到的 MQ 處理方式需要業(yè)務(wù)代碼里面顯式地發(fā)送 MQ 消息。還有一種優(yōu)雅的方式便是訂閱 MySQL 的 binlog,監(jiān)聽數(shù)據(jù)的真實(shí)變化情況以處理相關(guān)的緩存。


        例如剛剛提到的例子中,如果粉絲又觸發(fā)打賞了,這時(shí)候我們利用 binlog 表監(jiān)聽是能及時(shí)發(fā)現(xiàn)的,發(fā)現(xiàn)后就能集中處理了,而且無(wú)論是在什么系統(tǒng)什么位置去更新數(shù)據(jù),都能做到集中處理。


        目前業(yè)界類似的產(chǎn)品有 Canal,具體的操作圖如下:



        利用Canel訂閱數(shù)據(jù)庫(kù)binlog變更從而發(fā)出MQ消息,讓一個(gè)專門消費(fèi)者服務(wù)維護(hù)所有相關(guān)Key的緩存操作


        到這里,針對(duì)大型系統(tǒng)緩存設(shè)計(jì)如何保證最終一致性,我們已經(jīng)從策略、場(chǎng)景、操作方案等角度進(jìn)行了細(xì)致的講述,希望能對(duì)你起到幫助。



        點(diǎn)擊下方空白 ▼ 查看明日開發(fā)者黃歷


        summer

        time

        2022

        /

        07.23




        注:本文基于本人博客https://jaskey.github.io/blog/2022/04/14/cache-consistency/

        作者個(gè)人郵箱[email protected],微信:JaskeyLam


        瀏覽 40
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            天天日日综合 | 免费看美女操逼的网站 | 国产精品丝袜美腿一区二区三区 | A片在线观看网址 | 狠狠躁日日躁夜夜躁A片男男视频 | 日韩中文视频 | 噼啪啦噼啪啦叭叭叭啦叭 | 精品国产一区二区三区四区四 | 美女被艹逼| 老外几下就让我高潮了 |