大家好,我是星星。
今天我們來探討一下,併發場景下,緩存和數據庫一致性是如何保證的?那麼我將會給你一個實際的業務場景,並給出多種方案,最終選擇一種合適的方案。注意:不同的業務場景下,選取的方案也是不同的。
1.業務背景
業務背景是這樣子的,為了提高用户查詢車票餘票的請求速度,我們選擇將餘票信息存儲在Redis中,以便於用户可以快速的查詢。那麼在用户創建訂單並完成支付的時候呢,我們需要同時從數據庫和緩存中扣減相應的列車站點餘票。這種設計不僅提高了查詢的效率,也保證了數據的一致性,確保訂單操作的準確性。所以在這個業務場景下,我們就需要解決併發場景下緩存和數據庫的一致性問題。那麼我給出這麼幾種方案,我們一起來比對一下,一起選出這個場景下的最優解。
那麼為了方便探討呢,我就假設現在只有10張餘票了,兩個用户各自想買一張票。也就是用户扣減之後應該還剩下8張票。那麼我將主要討論下面的方案的線程不安全的情況。
2.方案一:先寫緩存再寫數據庫
那麼你可以看着我圖然後去思考,這兩個併發請求打下來的話,我們假設請求A先去更新緩存的餘票,更新為9張票,這時候還來不及更新數據庫,請求B下來了,它把緩存更新為8張票,然後更新數據庫為8張票,但這個時候A請求繼續執行,就會把數據庫更新為9張票了,這時候就出現不一致了。
3.方案二:先寫數據庫再寫緩存
同上所訴,參考對應的業務場景和多請求併發場景,不同的是前者先更新緩存,後者先更新的是數據庫,相同的是都存在併發問題,導致結果與預期並不相符。
4.方案三:先刪緩存再寫數據庫
假設有兩個併發的讀寫操作,一個是寫操作,另一個是讀操作。
- 併發讀寫的情況下,寫操作首先刪除緩存,接下來需要執行更新數據庫操作。
- 讀操作發生,由於緩存已經被刪除,讀操作不得不從數據庫中讀取數據。然而,由於寫操作尚未完成,數據庫中的數據仍然是過時的。
- 寫操作這時需要更新數據庫中的值,更新後 MySQL 數據庫是最新的值。
- 讀操作將從數據庫中查詢到的過時數據再回寫到緩存。
在這種情況下,讀操作獲取到的是過時的數據,儘管寫操作已經完成。因為緩存被刪除,讀操作不得不從數據庫中讀取舊值,而不是最新的值。
5.方案四:先刪除緩存再寫數據庫,再刪緩存
看着名字雖然有點長,但是如果換個詞大家估計就懂了:緩存雙刪技術方案。
如果説上圖的讀請求回寫緩存在寫請求第二次刪除緩存之前,那這種技術方案是比較好的,而且也不用引入過多複雜的中間件。問題就在於,第二次刪除緩存,不一定在讀請求回寫緩存之後。所以我們需要保證第二次刪除要在請求回寫緩存之後。假設讀請求回寫緩存大概需要 300ms,那我們是否可以在寫請求第二次刪除緩存前進行一個延遲操作,比如睡眠 500ms 後再刪除?這樣就可以規避讀請求回寫緩存在第二次刪除之後了。這種方案理論上是可以的,不過把這個睡眠操作使用延遲隊列或者引入三方消息隊列去做。
最新技術架構流程如下所示:
6.方案五:先寫數據庫再刪除緩存
讀請求第一次查詢時,會查詢到一個錯誤的數據,因為寫請求還沒有更新到緩存,寫請求寫入 MySQL 成功後會刪除緩存中的歷史數據。後續讀請求查詢緩存沒有值就會再請求數據庫 MySQL 進行重新加載,並將正確的值放到緩存中。
也就是説這種模型會存在一個很小週期的緩存與數據庫不一致的情況,不過對於絕大多數的情況來説,是可以容忍的。除去一些電商庫存、列車餘票等對數據比較敏感的情況,比較適合絕大多數業務場景。
當然,這種模型也不是完全沒問題,如果説恰巧讀緩存失效了,就會出現這種情況。
當緩存過期(可能是緩存正常過期也可能是 Redis 內存滿了觸發清理策略)條件滿足,同時讀請求的回寫緩存 Redis 的執行週期在數據庫刪除之前,那麼就有可能觸發緩存數據庫不一致問題。
上面説的兩種情況,缺一不可,不過能同時滿足這兩種情況概率極低,低到可以忽略這種情況。
7.方案六:先寫數據庫,通過BinLog異步更新緩存
這種方案是我認為最終一致性最為值得嘗試以及使用的。但是有一句話説的是沒有絕對合適的技術,只有相對適合的技術,這種方案實現是也存在一些技術問題,稍後會給大家詳細説明。
如果是扣減庫存的方案,比如説你將列車餘票扣減為 9,但是同時又有一個請求將列車餘票扣減為 8,這個時候,扣減為 8 的這個請求先到消息隊列執行,將緩存更新為餘票 8,但是隨之而來的是第一個請求餘票為 9,會將緩存餘票為 8 給覆蓋掉。
類似於這種邏輯,會存在一些數據一致性的問題,需要我們通過其它技術手段完善,比如數據庫添加版本號,或者根據最後修改時間等技術規避這些問題。
另外,如果在寫入數據庫餘票 9 前,同時有個查詢請求,也會存在數據庫不一致問題。比如在寫入數據庫餘票 9 前,將數據庫餘票 10 獲取到,然後等消息隊列更新到緩存餘票 9 後,再將數據庫餘票 10 更新到緩存。
這種出問題的概率比較小,因為跨的週期太長了。也是類似於存在一個很小週期的數據不一致性。
8.方案總結:選取哪種方案
以下這三種在實際工作中不建議使用,存在比較大的數據不一致隱患:
- [先寫緩存再寫數據庫]
- [先寫數據庫再寫緩存]
- [先刪除緩存再寫數據庫]
可以根據業務場景選擇下述緩存一致性方案:
- [緩存雙刪]:如果公司現有消息隊列中間件,可以考慮使用該方案,反之則不需要考慮。
- [先寫數據庫再刪緩存]:這種方案從實時性以及技術實現複雜度來説都比較不錯,推薦大家使用這種方案。
- [Binlog 異步更新緩存]:如果希望實現最終一致性以及數據多中心模式,該方案無疑是最合適的。
9.小結
總結一下關於緩存與數據庫一致性的方案:如果你想要最終一致性可以使用[Binlog 異步更新緩存]方案,如果緩存實時性要求比較高,使用[先寫數據庫再刪緩存]方案。
真實場景中根據具體業務需求和系統架構,可以選擇適合的方案或組合多種方案。這些方案最終目的是在解決緩存與數據庫之間的一致性問題,以確保數據的正確性和可靠性。
好的,那今天的內容到這裏就結束了,如果對您有幫助,可以給個贊支持一下,我們下期再見!