深入分析與解決方案:快取與資料庫雙寫不一致問題

努力的小雨發表於2024-08-20

我們上次探討了 Redis 的常見問題,本章將深入分析更細緻的細節,例如如何從業務角度有效處理快取與資料庫之間的雙寫不一致問題。接下來,讓我們深入研究這個話題。

key重建最佳化

開發人員通常使用“快取+過期時間”的策略,以便既能加速資料讀寫,又能確保資料的定期更新。這種模式基本上能夠滿足絕大部分需求。然而,當以下兩個問題同時出現時,可能會對應用系統造成嚴重的影響:

  1. 熱點 key 的出現:當前的 key 是一個熱點 key,例如一條熱門的娛樂新聞,導致併發請求量非常大。這種情況會使得快取的讀取請求集中在這個熱點 key 上,造成快取的壓力顯著增加。
  2. 快取重建的複雜性:當快取失效後,重建快取的過程不能在短時間內完成。重建快取可能涉及複雜的計算任務,例如執行復雜的 SQL 查詢、多次 I/O 操作、以及處理多個資料依賴等。這種複雜的重建過程可能會導致系統效能下降,進而影響使用者體驗。

在快取失效的瞬間,如果大量執行緒同時啟動快取重建操作,會導致後端負載急劇增加,甚至可能使應用系統崩潰。這種情況會顯著影響系統的穩定性和效能。為了解決這一問題,關鍵在於避免大量執行緒同時進行快取重建。

一個有效的解決方案是使用互斥鎖機制,該方法確保在任何給定時刻只有一個執行緒被允許執行快取重建操作。其他執行緒則需要等待重建執行緒完成快取重建後,才能從快取中重新獲取資料。這種策略不僅能減輕後端系統的壓力,還能避免因併發重建引起的效能瓶頸,顯著提升系統的穩定性和響應速度。

示例虛擬碼:

String get(String key) {

    // 從Redis中獲取資料
    String value = redis.get(key);

    // 如果value為空,則開始重構快取
    if (value == null) {

        // 生成唯一的mutex key,確保只有一個執行緒能重建快取
        String mutexKey = "mutex:key:" + key;

        // 嘗試設定mutex key,使用NX(僅在不存在時設定)和EX(設定過期時間)
        boolean isMutexSet = redis.set(mutexKey, "1", "ex 180", "nx");

        if (isMutexSet) {
            try {
                // 從資料來源獲取資料
                value = db.get(key);

                // 回寫資料到Redis,設定過期時間
                redis.setex(key, timeout, value);

            } finally {
                // 刪除mutex key,確保其他執行緒可以繼續重建快取
                redis.delete(mutexKey);
            }

        } else {
            // 其他執行緒等待50毫秒後重試
            Thread.sleep(50);
            value = get(key);
        }
    }

    return value;
}

快取與資料庫雙寫不一致

在高併發場景下,同時進行資料庫與快取的操作可能會引發資料不一致性的問題。具體來說,當多個執行緒或程序同時嘗試更新快取和資料庫時,可能會導致快取與資料庫之間的資料不匹配。

雙寫不一致情況

當多個執行緒或程序同時進行快取和資料庫的更新時,可能出現以下問題:

  • 快取與資料庫的資料不一致:例如,兩個執行緒同時更新資料庫,但只一個執行緒更新了快取,這會導致快取中的資料和資料庫中的資料不一致。
  • 延遲問題:即使在更新快取和資料庫時都執行了操作,也可能由於網路延遲或其他因素,導致快取和資料庫之間的狀態不同步。

image

讀寫併發不一致

讀寫併發不一致是指在併發場景下,多個執行緒或程序對同一資料進行讀寫操作時,可能導致資料的不一致或錯誤。

image

以下是一些常見的讀寫併發不一致的解決方法:

  1. 針對併發機率較小的資料
    • 對於個人維度的訂單資料、使用者資料等,併發操作較少且對資料一致性的要求相對寬鬆。對於這類資料,可以透過設定快取的過期時間來解決快取與資料庫之間的資料不一致問題。具體做法是,在快取中設定合理的過期時間,快取資料會在過期後自動失效。每當快取失效時,系統將自動從資料庫中讀取最新的資料,並更新快取。這種策略簡單有效,可以大大減少快取不一致的發生機率。
  2. 在併發較高的場景下的快取資料一致性
    • 即使在業務場景下併發較高,但如果可以容忍短時間的快取資料不一致(例如商品名稱、商品分類選單等),則仍然可以透過設定快取的過期時間來滿足大部分業務需求。透過合理設定過期時間,雖然快取資料可能會在短時間內出現不一致,但這種不一致通常不會對業務造成嚴重影響。因此,快取過期策略仍然是一種有效的解決方案。
  3. 對於不能容忍快取資料不一致的場景
    • 如果業務對快取資料的一致性有嚴格要求,可以使用分散式讀寫鎖來保證併發讀寫操作的順序性。具體做法是,在進行寫操作時,透過分散式鎖機制來確保只有一個操作能夠執行,從而避免寫寫衝突。而對於讀操作,通常可以在不加鎖的情況下進行,以提高效能。分散式鎖能夠有效地控制併發寫操作,確保資料的一致性,儘管可能會對系統效能產生一定影響。
  4. 引入中介軟體以維護資料一致性
    • 可以使用阿里開源的 Canal 工具,透過監聽資料庫的 binlog 日誌來及時更新快取。這種方法可以在資料發生變化時自動更新快取,從而減少快取和資料庫之間的一致性問題。然而,引入 Canal 或類似的中介軟體會增加系統的複雜度,因此需要權衡其帶來的額外複雜性和對系統一致性的增強。使用這種方案時,應考慮中介軟體的維護、配置和潛在的效能影響,以確保系統的穩定性和可靠性。

image

總結

上述解決方案主要針對的是讀多寫少的場景,透過引入快取來提升效能。然而,對於寫多讀多且不能容忍快取資料不一致的情況,我們需要重新考慮快取的使用策略。以下是針對這種情況的最佳化建議:

  1. 避免使用快取
    • 在寫操作頻繁且讀操作也較多的場景中,如果業務對資料一致性的要求非常高,使用快取可能並不是最佳選擇。此時,直接運算元據庫可以避免快取資料與資料庫資料之間的不一致問題,因為所有的資料操作都直接在資料庫中進行,從而確保資料的一致性和準確性。
  2. 資料庫作為主儲存
    • 如果資料庫面臨著高負載的壓力,但仍然需要處理大量的讀寫操作,可以考慮將快取作為資料的主儲存,而將資料庫作為備份。具體做法是:所有的讀寫操作都先寫入快取,快取會非同步地將資料同步到資料庫中。這樣,快取可以在高併發讀寫操作中提供快速的響應,而資料庫則用於長期的資料儲存和備份。這種策略可以提高系統的讀寫效能,同時保持資料庫的資料完整性。
  3. 快取適用的資料型別
    • 將快取用於對實時性和一致性要求不是特別高的資料。例如,商品分類資訊、系統配置等資料可以快取,因為這些資料變化頻率較低,對一致性要求不是很高。快取能顯著提升訪問速度,但在資料不一致的情況下,對業務影響較小。避免將快取用於對一致性要求極高的關鍵業務資料,以減少因快取引發的複雜性和風險。
  4. 避免過度設計
    • 在設計快取系統時,要避免為了保證絕對一致性而進行過度設計和複雜控制。這種過度設計不僅會增加系統的複雜性,還可能影響系統的效能。應當根據實際業務需求,合理選擇快取策略,平衡效能和一致性要求,避免不必要的複雜性和資源浪費。

總之,在選擇是否使用快取及其設計時,需要根據業務場景和資料一致性要求進行權衡。快取應主要用於提升讀操作效能,而對於寫多讀多且對一致性要求高的場景,可能需要依賴資料庫本身的能力或採用其他策略來處理資料的一致性問題。


我是努力的小雨,一名 Java 服務端碼農,潛心研究著 AI 技術的奧秘。我熱愛技術交流與分享,對開源社群充滿熱情。同時也是一位掘金優秀作者、騰訊雲創作之星、阿里雲專家博主、華為云云享專家。

💡 我將不吝分享我在技術道路上的個人探索與經驗,希望能為你的學習與成長帶來一些啟發與幫助。

🌟 歡迎關注努力的小雨!🌟

相關文章