當前位置: 華文問答 > 遊戲

分布式的環境下, MySQL和Redis如何保持數據的一致性?

2015-10-12遊戲

這又是一個高頻的場景面試題,同時,它也是一個沒有標準答案的、可以跟面試官討論很久的面試題。

對於這個面試題,我的觀點是:「 沒有完美的方案,只有最適合某場景的方案。」

這個問題表面上看是數據一致性的問題,其實根本上,又是數據一致性、系統效能和系統復雜度的選擇與取舍。

下面我們先歷數一下各種技術方案,環肥燕瘦兩相宜,總有一款適合你。

首先,分享一套我自己逐字寫的、深入淺出、細致易懂的高頻面試題詳解,旨在以 一站式刷題 + 解惑 的方式幫你提升學習效率,需要的請自取。

強烈建議近期有求職訴求的Javaer好好看看。


1、先更新MySQL,再更新Redis

這個方案存在如下問題:

(1)如果先更新MySQL成功了,還未對Redis進行更新的間隙期,這時如果請求過來,讀到的都是Redis的更新前數據。

(2)如果先更新MySQL成功了,再更新Redis失敗了的話,後面的請求讀到的都是Redis的更新前數據,並且後續的補救方案很難做。

補救方案一:為Redis更新失敗,將MySQL中的對應數據也回滾了,以此達到兩者數據的一致性。但MySQL是主資料來源,它代表的是數據的「權威性」,這樣做顯然並不合理。

補救方案二:透過Redis重試更新的方式進行補救。但如果重試也失敗了,還要繼續重試嗎?是設定固定的重試次數,還是一直重試到成功為止?

另外,重試時間間隔設定多少?時間間隔設定長了,影響業務的時間也會變長;時間間隔設定短了,重試成功率又會降低。這些其實都是問題。

(3)兩個執行緒同時更新的並行問題,如下:

2、先更新Redis,再更新MySQL

這個方案,要比方案一的「先更新MySQL,再更新Redis」合理一些。原因在於更新完Redis的話,哪怕還沒更新MySQL,這時如果請求過來,讀到的都是Redis更新後的新數據。

另外,先更新Redis成功,再更新MySQL失敗,可以透過再刪除Redis所對應的數據進行補救。

但其依然存在如下問題:

(1)如果先更新Redis成功了,再更新MySQL失敗了的話,還未對Redis所對應的數據進行刪除補救的間隙期,這時如果請求過來,讀到的都是Redis未生效的新數據。

(2)如果先更新Redis成功了,再更新MySQL失敗了的話,然後再刪除Redis對應的數據也失敗的時候,應該如何處理?如果透過重試機制繼續進行刪除Redis的話,又會面臨之前說的重試次數和間隔期的問題。

(3)兩個執行緒同時更新的並行問題,如下:

3、先更新MySQL,再刪除Redis

這個方案可以解決「並行更新」的問題,但依然會存在下面的兩個問題:

(1)如果先更新MySQL成功了,還未對Redis進行刪除的間隙期,這時如果請求過來,讀到的都是Redis的刪除前數據。

(2)如果先更新MySQL成功了,再刪除Redis失敗了的話,後面的請求讀到的都是Redis的刪除前數據,並且後續的補救方案很難做。

4、先刪除Redis,再更新MySQL

這個方案能解決方案3中遺留的兩個棘手的問題:

(1)如果先刪除Redis成功了,還未對MySQL進行更新的間隙期,此時對於該條數據而言,只存在於MySQL一個儲存載體中,也就沒有了數據一致性的問題。

(2)如果先刪除Redis成功了,再更新MySQL失敗了的話,此時對於該條數據而言,只存在於MySQL一個儲存載體中,所謂的補救方案也就不需要了,直接當這條數據沒更新成功。

OK,整體看起來似乎「天下無賊」了,但真的如此嗎?其實不然,如果配合上Redis的「讀策略」,還是會有數據一致性的問題。

4.1 先刪除Redis,再更新MySQL + Redis讀策略

Redis的讀策略:

  • 如果讀取的數據在Redis中存在,則直接返回數據;
  • 如果讀取的數據在Redis中不存在,則從MySQL中讀取數據,然後將數據寫入到Redis,並返回數據。
  • 這樣一來,就會存在如下問題:

    某商品的庫存數為10個,使用者A購買一件商品時進行庫存扣減,因此第一步先刪除了Redis中的庫存數。

    這時,使用者B查詢該商品的庫存,發現Redis中並沒有該商品的庫存,於是從MySQL中讀取庫存數後,寫入到了Redis中(10個)。

    然後,使用者A更新數據庫,將庫存數從10個扣減為9個。

    最終,Redis中的庫存數是10個,MySQL中的庫存數是9個。

    4.2 先刪除Redis,再更新MySQL + Binlog同步

    該方案將填充Redis的操作,改為透過DataBus和Canal同步Binlog的方式,這樣可以解決方案4.1中的Redis讀策略帶來的數據一致性問題。

    但是,這種方案的適用於數據量不大,可以完全吃進Redis緩存中,並設定為永不過期的場景。

    而那種數據量龐大到不能全部吃進Redis緩存中,需要在數據讀取的時機來寫入Redis,長時間未被讀取的數據則過期淘汰的場景,就不適合了。

    因為這種方案的緩存命中率太低了,也就失去了其應有的價值。

    4.3 先刪除Redis,再更新MySQL + Redis讀策略 + 延時雙刪

    這個方案稍復雜了一些,是在方案4.1中「先刪除Redis,再更新MySQL + Redis讀策略」,又增加了最後一步Redis刪除的操作。

    它可以解決「最終,Redis中的庫存數是10個,MySQL中的庫存數是9個」的數據一致性場景。

    也可以跟方案4.2中 「數據量龐大到不能全部吃進Redis緩存中,需要在數據讀取的時機來寫入Redis,長時間未被讀取的數據則過期淘汰」的不適合場景進行互補。

    有人會說,這種方案也不能100%保證解決數據一致性的問題,如果最後一步刪除操作失敗了怎麽辦?

    確實,它並不能保證100%。

    但如果第一步刪除Redis成功了,第二步Redis讀策略恰好在這個間隙期發生並寫入Redis成功了,而第三步刪除Redis又失敗了,這種概率有多大?0.0000001%的可能性有沒有?

    btw:此處請杠精留言,我最喜歡看到你們面紅耳赤、聲嘶力竭地杠的樣子,很性感。

    5、分布式鎖

    有人說,直接用分布式鎖,不就把問題都解決了嗎?幹嗎嘰嘰歪歪地寫這麽多字?

    是的,分布式鎖完全可以解決一致性問題,但你別忘了,引入鎖機制的最大弊端是什麽?是效能。

    而我們用Redis當緩存的初衷是什麽?還是效能。有句話怎麽說的來著?勿忘初心,方得始終。


    結語

    還是那句話,系統架構設計中,沒有銀彈,也沒有完美的方案,只有最適合某場景的方案。

    分享一套我自己逐字寫的、深入淺出、細致易懂的高頻面試題詳解,旨在以 一站式刷題 + 解惑 的方式幫你提升學習效率,需要的請自取。

    強烈建議近期有求職訴求的Javaer好好看看。