此文章轉: https://www.cnblogs.com/notchangeworld/p/12483194.html\
如何保障mysql和redis之間的數據一致性?
在高並發的業務場景下,數據庫大多數情況都是用戶並發訪問最薄弱的環節。所以,就需要使用redis做一個緩沖操作,讓請求先訪問到redis,而不是直接訪問Mysql等數據庫。這樣可以大大緩解數據庫的壓力。Redis緩存數據的加載可以分為懶加載和主動加載兩種模式,下面分別介紹在這兩種模式下的數據一致性如何處理。
懶加載
讀取緩存步驟一般沒有什么問題,但是一旦涉及到數據更新:數據庫和緩存更新,就容易出現緩存和數據庫間的數據一致性問題。不管是先寫數據庫,再刪除緩存;還是先刪除緩存,再寫庫,都有可能出現數據不一致的情況。舉個例子:
- 如果刪除了緩存Redis,還沒有來得及寫庫MySQL,另一個線程就來讀取,發現緩存為空,則去數據庫中讀取數據寫入緩存,此時緩存中為臟數據。
- 如果先寫了庫,在刪除緩存前,寫庫的線程宕機了,沒有刪除掉緩存,則也會出現數據不一致情況。(比如:下單時會去查詢庫存(數量為100),萬一此時還有其他的人同時也在操控(別人已經下單),即數據庫(99),但是此時緩存中為100,造成了數據不一致,即臟數據)
因為寫和讀是並發的,沒法保證順序,就會出現緩存和數據庫的數據不一致的問題。如何解決?
所以結合前面例子的兩種刪除情況,我們就考慮前后雙刪加懶加載模式。
那么什么是懶加載?就是當業務讀取數據的時候再從存儲層加載的模式,而不是更新后主動刷新,它涉及的業務流程如下如所示:
理解了懶加載機制后,結合上面的業務流程圖,我們講解下前后雙刪如何做?
延遲雙刪
在寫庫前后都進行redis.del(key)操作,並且第二次刪除通過延遲的方式進行。
方案一(一種思路,不嚴謹)具體步驟是:
1)先刪除緩存;
2)再寫數據庫;
3)休眠500毫秒(根據具體的業務時間來定);
4)再次刪除緩存。
那么,這個500毫秒怎么確定的,具體該休眠多久呢?
需要評估自己的項目的讀數據業務邏輯的耗時。這么做的目的,就是確保讀請求結束,寫請求可以刪除讀請求造成的緩存臟數據。
當然,這種策略還要考慮 redis 和數據庫主從同步的耗時。最后的寫數據的休眠時間:則在讀數據業務邏輯的耗時的基礎上,加上幾百ms即可。比如:休眠1秒。
方案二,異步延遲刪除:
1)先刪除緩存;
2)再寫數據庫;
3)觸發異步寫人串行化mq(也可以采取一種key+version的分布式鎖);
4)mq接受再次刪除緩存。
異步刪除對線上業務無影響,串行化處理保障並發情況下正確刪除。
為什么要雙刪?
- db更新分為兩個階段,更新前及更新后,更新前的刪除很容易理解,在db更新的過程中由於讀取的操作存在並發可能,會出現緩存重新寫入數據,這時就需要更新后的刪除。
雙刪失敗如何處理?
1、設置緩存過期時間
從理論上來說,給緩存設置過期時間,是保證最終一致性的解決方案。所有的寫操作以數據庫為准,只要到達緩存過期時間,則后面的讀請求自然會從數據庫中讀取新值然后回填緩存。
結合雙刪策略+緩存超時設置,這樣最差的情況就是在超時時間內數據存在不一致。
2、重試方案
重試方案有兩種實現,一種在業務層做,另外一種實現中間件負責處理。
業務層實現重試如下:
1)更新數據庫數據;
2)緩存因為種種問題刪除失敗;
3)將需要刪除的key發送至消息隊列;
4)自己消費消息,獲得需要刪除的key;
5)繼續重試刪除操作,直到成功。
然而,該方案有一個缺點,對業務線代碼造成大量的侵入。於是有了方案二,在方案二中,啟動一個訂閱程序去訂閱數據庫的binlog,獲得需要操作的數據。在應用程序中,另起一段程序,獲得這個訂閱程序傳來的信息,進行刪除緩存操作。
中間件實現重試如下:
流程說明:
1)更新數據庫數據;
2)數據庫會將操作信息寫入binlog日志當中;
3)訂閱程序提取出所需要的數據以及key;
4)另起一段非業務代碼,獲得該信息;
5)嘗試刪除緩存操作,發現刪除失敗;
6)將這些信息發送至消息隊列;
7)重新從消息隊列中獲得該數據,重試操作。
主動加載
主動加載模式就是在db更新的時候同步或者異步進行緩存更新,常見的模式如下:
寫流程:
第一步先刪除緩存,刪除之后再更新DB,之后再異步將數據刷回緩存。
讀流程:
第一步先讀緩存,如果緩存沒讀到,則去讀DB,之后再異步將數據刷回緩存。
這種模式簡單易用,但是它有一個致命的缺點就是並發會出現臟數據。
試想一下,同時有多個服務器的多個線程進行’步驟1.2更新DB’,更新DB完成之后,它們就要進行異步刷緩存,我們都知道多服務器的異步操作,是無法保證順序的,所以后面的刷新操作存在相互覆蓋的並發問題,也就是說,存在先更新的DB操作,反而很晚才去刷新緩存,那這個時候,數據也是錯的。
讀寫並發:再試想一下,服務器A在進行’讀操作’,在A服務器剛完成2.2時,服務器B在進行’寫操作’,假設B服務器1.3完成之后,服務器A的2.3才被執行,這個時候就相當於更新前的老數據寫入緩存,最終數據還是錯的。
而對於這種臟數據的產生歸其原因還是在於這種模式的主動刷新緩存屬於非冪等操作,那么要解決這個問題怎么辦?
- 前面介紹的雙刪操作方案,因為刪除每次操作都是無狀態的,所以是冪等的。
- 將刷新操作串行處理。
這里把基於串行處理的刷新操作方案介紹一下:
寫流程:
第一步先刪除緩存,刪除之后再更新DB,我們監聽從庫(資源少的話主庫也ok)的binlog,通過分析binlog我們解析出需要需要刷新的數據標識,然后將數據標識寫入MQ,接下來就消費MQ,解析MQ消息來讀庫獲取相應的數據刷新緩存。
關於MQ串行化,大家可以去了解一下 Kafka partition 機制 ,這里就不詳述了。
讀流程:
第一步先讀緩存,如果緩存沒讀到,則去讀DB,之后再異步將數據標識寫入MQ(這里MQ與寫流程的MQ是同一個),接下來就消費MQ,解析MQ消息來讀庫獲取相應的數據刷新緩存。
總結
- 懶加載模式緩存可采取雙刪+TTL失效來實現;
- 雙刪失敗情況下可采取重試措施,重試有業務通過mq重試以及組件消費mysql的binlog再寫入mq重試兩種方式;
- 主動加載由於操作本身不具有冪等性,所以需要考慮加載的有序性問題,采取mq的分區機制實現串行化處理,實現緩存和mysql數據的最終一致,此時讀和寫操作的緩存加載事件是走的同一個mq。