1.概述
在現代分散式業務系統中,MySQL 和 Redis 通常被組合使用,以利用各自的優點。MySQL 是一種關係型資料庫,提供強一致性和持久化儲存,而 Redis 是一種記憶體資料儲存,具有快速讀取和寫入的特點。然而,在使用 MySQL 和 Redis 的同時,確保它們之間的資料一致性是一個重要且具有挑戰性的問題。本文將總結一些常見的資料同步一致性問題,並探討解決這些問題的方法。
當面試官問:你們系統是怎麼實現MySQL和Redis雙寫的?沒有做過高併發的小夥伴聽到這個問題都懵了~~~因為一般大家對MySQL和Redis配合使用的認知如下:
讀取資料
讀取數據流程大概是這樣的:先查詢快取,獲取到資料直接返回,查詢不到,再從資料庫DB查詢資料,拿到資料之後更新快取,再返回資料。流程圖如下所示:
平時開發過程中讀取資料基本上也都是按照這個流程操作的,因該不會存在異議吧。
寫入資料
對MySQL和Redis進行資料雙寫同步操作流程就沒麼固定了,存在以下情況:
先更新資料庫DB還是先更新快取?
是更新快取還是刪除快取?如果是刪除快取,那麼是先更新資料庫DB還是先刪除快取呢?
綜合來看寫入資料就存在了4種情況,如下圖所示:
我們在平時開發過程中一般都會選擇上面其中一種方式進行資料庫與快取的雙寫操作,普通的業務系統、後臺管理這些系統選擇什麼方式怎麼操作都是可以的,因為併發量不大,幾乎不會存在資料庫和快取資料不一致的情況,即使存在不一致,我們透過設定一個有效的快取過期時間,從而保證不一致只是短暫的,在某些系統業務場景下是完全可以容忍的。
所以接下來下面的討論都是基於在併發量比較大且不能接受資料不一致情況下進行總結的
在分散式系統中保證資料一致性就是一個老難的問題,自古難以做到兩全,即保證強一致性,就是要求寫入什麼,看到的就是什麼,這就要求加鎖,或者透過分散式事務機制(如兩階段提交協議)來確保 MySQL 和 Redis 更新操作的一致性,但是這些方式都效能開銷大,實施複雜,對系統的響應時間有較大影響。所以現在大部分的分散式系統都會選擇最終一致性方案,即系統保證在一段時間內使得資料一致。前面所說的設定合理快取過期時間就是這種方案的體現所在。
2.高併發下資料庫與快取雙寫同步存在的問題
基於上面的概述可以知道讀取資料的流程幾乎是固定的,但是對於資料庫與快取雙寫就存在4種方式可供選擇,下面我們就來看看這4種方式在高併發場景下進行資料雙寫同步怎麼就有問題了:
2.1 先寫 MySQL,再寫 Redis
請求 A、B 都是先寫 MySQL,然後再寫 Redis,在高併發情況下,如果請求 A 在寫 Redis 時卡了一會(網路抖動或服務阻塞),請求 B 已經依次完成資料的更新,就會出現圖中的問題。併發場景下,這樣的情況是很容易出現的,每個執行緒的操作先後順序不同,這樣就導致請求B的快取值被請求A給覆蓋了,資料庫中是請求B的新值,快取中是請求A的舊值,並且會一直這麼髒下去直到快取失效(如果你設定了過期時間的話)。
2.2 先寫Redis,再寫MySQL
和上面的情況差不多,這裏就只是調換資料庫和快取的寫入順序:先寫Redis,再寫MySQL,快取存的是請求B的值,資料庫存的是請求A的值,也是不一致了。
出現數據不一致的原因是因為寫資料庫和寫快取不是原子操作(原子不可再分割),即要麼一起成功,要麼一起失敗。寫資料庫和寫快取是兩個操作,分別操作不同的元件,在高併發場景下我們是沒辦法控制這些操作的執行順序的,也就會出現上面敘述的資料不一致情況了。
既然寫快取都會存在資料不一致問題,索性就不要同步寫快取,而是刪除快取,讓下一次讀取資料按照上面所說的流程命中不了快取查詢資料庫獲取資料,寫入快取。下面兩種方式就是基於刪除快取操作的。
2.3 先刪除Redis,再更新MySQL
刪除快取是爲了下一次讀取資料命中不了快取透過查詢資料庫重建快取,這就變成刪除快取和讀取資料的先後順序的事情了:這裏我們假設請求A是寫資料,請求B是讀取資料,如下圖所示:
這個情況快取存入的是舊值10,但是資料庫已經寫入了新值20,資料不一致了。
2.4 先更新MySQL,再刪除快取
這個方式就是我們使用快取最常見最推薦的策略方式,Cache Aside 策略(也叫旁路快取策略)。它的提出是爲了儘可能地解決快取與資料庫的資料不一致問題。注意是儘可能,說明還是會出現極端情況下資料不一致的情況:
不過出現這種情況的機率並不高,因為寫入快取的操作通常比寫入資料庫快很多,很難出現上面這種,你寫入一個快取時長竟然大於運算元據庫和刪除快取的時長,因為運算元據庫一般都比操作快取慢很多,即使是快取服務的問題,那麼寫入快取的也會早於刪除快取操作完成的。除非是你查詢資料庫和寫入快取
所以如果資料一致性要求比較高的業務功能,使用這種策略去實現即可,也比較推薦。
2.5 延時雙刪
流程大概如下,就不畫圖了,因為確定不了延時多久沒辦法準確畫圖反應
先刪除快取
更新資料庫
延時:睡眠一段時間
再次刪除快取
延時主要是爲了儘量保證讀取資料的請求A的操作在寫入資料請求B之前完成,這樣再刪除快取,讓下一次讀取資料請求重建索引即可。但是這個具體睡眠多久時間不可控,也不能設定一個比較大的時間來保證讀取請求早於寫入請求完成,這嚴重影響了寫入介面的效能,得不償失,所以這種方案不太推薦,瞭解下即可。
3.資料一致性解決方案
可以這麼說,資料庫與快取強一致性是做不到的,因為這涉及兩個元件,典型的分散式系統中兩個非原子操作,沒辦法一定保證要麼都成功,要麼都失敗。只能保證最終一致性的同時儘量縮短延遲。
當然如果你採用是更新資料庫的同時更新快取來保證快取命中率,爲了解決併發問題,在更新快取前先加一個分散式鎖,因為這樣在同一時間只允許一個執行緒更新快取,就不會產生併發問題了。當然這麼做對於寫入的效能會有影響。不過給快取加一個較短的過期時間,這樣即使出現快取不一致的情況,快取的資料也會很快地過期,對業務的影響也是可以接受。
接下來我們說說兩種保證最終一致性的同步方案。
3.1 訊息佇列
當下大部分的系統都有使用訊息佇列MQ(kakfa、rocketMQ、rabbitMQ...),並且我們一般雙寫快取的資料都是業務系統的核心資料,因為高頻使用才透過快取來提高系統的嘛,比如訂單系統的訂單資料就是很好的一個例子,一般都會有關於訂單變更的MQ訊息topic供消費者訂閱處理對應邏輯,所以我們就可以很方便的增加一個消費者訂閱,該消費者完成同步寫入快取的邏輯。
唯一需要擔心的一個問題是,如果丟訊息了怎麼辦?因為現在訊息是快取資料的唯一來源,一旦出現丟訊息,快取裡缺失的那條資料永遠不會被補上。所以,必須保證整個訊息鏈條的可靠性,不過好在現在的 MQ 叢集,比如像 Kafka 或者 RocketMQ,它都有高可用和高可靠的保證機制,只要你正確配置好,是可以滿足資料可靠性要求的。
3.2 使用canal訂閱binlog實時同步快取
canal [kə'næl],譯意為水道/管道/溝渠,主要用途是基於 MySQL 資料庫增量日誌解析,提供增量資料訂閱和消費
Canal 模擬 MySQL 主從複製的互動協議,把自己偽裝成一個 MySQL 的從節點,向MySQL 主節點發送 dump 請求,MySQL 收到請求後,就會開始推送 Binlog 給 Canal,Canal 解析 Binlog 位元組流之後,轉換為便於讀取的結構化資料,供下游程式訂閱使用。
4.總結
MySQL 和 Redis 的資料一致性問題是分散式系統中的一個重要挑戰。透過合理的設計和實現,可以有效地解決這些問題,保障系統的資料一致性。在實際應用中,需要根據具體場景選擇適當的方法,並結合多種技術手段,以實現最佳的一致性保障。當然在併發量不大的業務系統中其實不需要這個資料一致性問題,採用上面的Cache Aside 旁路快取策略,再設定一個合理的快取過期時間即可。