Redis 和 MySQL 一致性問題是企業級應用中常見的挑戰之一,特別是在高併發、高可用的場景下。由於 Redis 是記憶體型資料庫,具備極高的讀寫速度,而 MySQL 作為持久化資料庫,通常用於資料的可靠儲存,如何保證兩者資料的一致性需要具體業務場景的設計與最佳化。
下面我們將結合幾個典型的業務場景,逐步分析如何在不同的場景下保證 Redis 和 MySQL 之間的資料一致性。
1. 快取更新策略:Cache Aside Pattern(旁路快取模式)
場景:
在大部分業務系統中,Redis 作為快取層用於提升系統的讀取效能,而 MySQL 作為持久化儲存,用於保證資料的可靠性。最常見的場景是:
- 系統先查詢 Redis 快取,如果快取中沒有資料,再從 MySQL 中查詢並將資料寫入 Redis 快取。
- 更新資料時,更新 MySQL 並刪除 Redis 快取,使快取資料失效,保證下次讀取時能拿到最新資料。
典型業務場景:
- 商品詳情頁面:當使用者請求某個商品詳情時,首先查詢 Redis 快取,如果快取中沒有,則查詢 MySQL,將查詢結果快取到 Redis 中;如果商品資訊發生變更時,更新 MySQL 並刪除 Redis 中的快取。
方案分析:
- 讀取路徑:從 Redis 獲取快取,如果快取命中則直接返回資料;如果快取未命中,則查詢 MySQL,將結果寫入 Redis,並返回資料。
- 寫入路徑:更新時先操作 MySQL,然後刪除 Redis 快取中的資料。下次讀取時,由於快取未命中,會重新從 MySQL 中獲取最新資料。
如何保障一致性:
-
快取淘汰策略:MySQL 資料更新後立即刪除 Redis 快取,確保下次讀取時能獲取到最新資料。即透過 "刪除快取" 的方式避免髒資料存在於快取中。
-
併發問題:當併發請求較高時,可能會出現“快取雪崩”或“快取擊穿”問題。例如:A 更新 MySQL 資料,B 在快取失效的瞬間讀取了舊資料,再次快取到 Redis。為解決此問題,可以採用 延遲雙刪策略:
- 刪除 Redis 快取。
- 更新 MySQL。
- 適當延遲(如 500ms),再次刪除 Redis 快取,確保在併發情況下不存在快取不一致問題。
-
業務例項:
// 更新商品詳情的虛擬碼 public void updateProduct(Product product) { // 1. 更新資料庫 updateProductInMySQL(product); // 2. 刪除快取 deleteProductCache(product.getId()); // 3. 延遲雙刪,解決併發下不一致問題 try { Thread.sleep(500); // 可以根據實際業務場景調整 } catch (InterruptedException e) { // handle exception } deleteProductCache(product.getId()); }
2. 先更新快取再更新資料庫
場景:
在某些實時性要求較高的場景中,可以考慮先更新 Redis 快取,然後再非同步更新 MySQL 資料庫。
典型業務場景:
- 秒殺系統:例如商品庫存的扣減,使用者購買商品時,首先更新 Redis 中的庫存數量,保證極低延遲的實時性體驗。然後將變更非同步寫入 MySQL,確保持久化儲存的一致性。
方案分析:
- 讀取路徑:讀取 Redis 快取的庫存資訊,能夠提供快速的讀取響應。
- 寫入路徑:更新 Redis 中的庫存數量後,使用訊息佇列或其他非同步機制將更新同步到 MySQL。
如何保障一致性:
-
資料最終一致性:Redis 作為前端實時資料的快取,MySQL 作為後端資料的持久化儲存,採用非同步更新策略時,一致性無法保證是強一致性,但可以透過使用訊息佇列等手段來保證最終一致性。非同步寫入 MySQL 時,如果操作失敗,可以透過重試機制或補償機制恢復一致性。
-
業務例項:
// 扣減庫存的虛擬碼 public void reduceStock(Long productId, int amount) { // 1. 先更新 Redis 中的庫存 redisTemplate.decrement("stock:" + productId, amount); // 2. 透過訊息佇列非同步更新 MySQL 中的庫存 sendUpdateStockMessage(productId, amount); } // 消費訊息佇列更新 MySQL @RabbitListener(queues = "stock_update_queue") public void updateStockInMySQL(UpdateStockMessage msg) { // 從 MySQL 中扣減庫存 productRepository.reduceStock(msg.getProductId(), msg.getAmount()); }
一致性保證策略:
- 冪等性保障:確保訊息的處理是冪等的,即相同的訊息即使被處理多次,也不會導致庫存重複扣減。
- 訊息重試機制:如果消費訊息時更新 MySQL 失敗,可以設定重試機制或訊息補償機制,保證最終資料一致性。
3. 雙寫操作(快取與資料庫同時更新)
場景:
有時業務需要同時更新 Redis 和 MySQL 的資料,如使用者餘額更新、積分獎勵系統等場景中,Redis 和 MySQL 需要同步寫入。
典型業務場景:
- 積分系統:使用者消費時增加或減少積分,需要同時更新 Redis 和 MySQL 中的積分記錄。
方案分析:
- 同步寫入:當更新使用者積分時,Redis 和 MySQL 同時更新資料。由於需要保證兩個儲存的同步性,必須考慮事務性問題。
- 分散式事務:如果系統架構分散式,可能需要使用分散式事務(如
2PC
,或者更輕量的解決方案如TCC
)來確保一致性。
如何保障一致性:
-
雙寫一致性問題:如果同時寫 Redis 和 MySQL,可能會面臨一致性問題。常見解決方案是透過事務補償機制來實現。具體步驟:
- 使用資料庫事務保證 MySQL 寫入成功。
- 如果 Redis 寫入失敗,可以嘗試重試,或在事務結束後透過補償機制將失敗的資料寫入 Redis。
-
業務例項:
@Transactional public void updateUserPoints(Long userId, int points) { // 1. 更新 MySQL 中的積分 userRepository.updatePoints(userId, points); // 2. 同步更新 Redis 中的積分 redisTemplate.opsForValue().set("user:points:" + userId, points); }
事務性保障:
- 本地事務:在單體系統中,可以依賴資料庫事務和 Redis 的操作保證一致性。如果操作失敗,透過重試機制來恢復一致性。
- 分散式事務:在微服務架構中,雙寫操作涉及分散式事務,可能需要使用
TCC
(Try, Confirm, Cancel)等模式,或使用訊息佇列進行最終一致性補償。
4. 資料回寫(Write Back)策略
場景:
資料回寫模式適用於 Redis 作為快取層,MySQL 作為持久化儲存層,但 Redis 中資料修改後並不立即同步更新 MySQL,而是在特定時機觸發資料回寫。
典型業務場景:
- 廣告計費系統:廣告點選量儲存在 Redis 中,以減少頻繁的資料庫寫入壓力,定期將 Redis 中的統計資料批次寫入 MySQL。
方案分析:
- 延遲迴寫:可以透過定時任務或者觸發器將 Redis 中的資料定期回寫到 MySQL,這樣既減少了 MySQL 的壓力,又保證了資料一致性。
如何保障一致性:
- 持久化與批次同步:透過 Redis 的持久化機制(如 RDB、AOF),在 Redis 崩潰時不會丟失資料。透過定時器或事件驅動系統觸發批次同步 MySQL。
總結
Redis 和 MySQL 的一致性保障在不同的業務場景中需要結合場景特性來進行權衡,主要的策略包括:
- Cache Aside Pattern(旁路快取模式):常用於讀多寫少的場景,寫操作時刪除快取。
- 非同步更新(Write Behind):先更新快取再非同步寫入 MySQL,保證最終一致性。
- 雙寫策略:同時更新 Redis 和 MySQL,配合事務機制確保一致性。
- 延遲迴寫:透過定時批次寫入 MySQL 減少頻繁資料庫操作。
每種策略有不同的適用場景,設計時需要考慮一致性、效能和可用性之間的平衡。這算得上是全網最全最詳細的,貨真價實的同步方案分析了,完全結合真實業務場景來考慮設計。所謂贈人玫瑰,手留餘香,希望對你有幫助作用。