深度剖析 MySQL 與 Redis 緩存一致性:理論、方案與實戰

在當今的互聯網應用開發中,MySQL 作為可靠的關系型數據庫,與 Redis 這一高性能的緩存系統常常協同工作。然而,如何確保它們之間的數據一致性,成為了開發者們面臨的重要挑戰。本文將深入探討 MySQL 與 Redis 緩存一致性的相關問題,從不同的方案分析到實際項目的代碼實現,為你呈現全面的技術解析。

一、理論知識:探尋一致性方案的基石

(一)不佳的方案

  1. 先寫 MySQL,再寫 Redis
    在高并發場景下,當多個請求同時進行數據更新時,若請求 A 先寫 MySQL,接著在寫 Redis 過程中出現延遲,而請求 B 快速完成了 MySQL 和 Redis 的數據更新操作,就會導致數據不一致。
    在這里插入圖片描述
    這是一幅描述在高并發場景下,“先寫 MySQL,再寫 Redis” 方案可能出現數據不一致問題的時序圖 ,具體過程如下:
    1. 初始狀態:假設數據在 MySQL 和 Redis 中的初始值未明確提及,但后續操作是將其從某個值更新為 10 再到 11 。
    2. 請求 A 操作:請求 A 先對 MySQL 進行寫操作,將 MySQL 中的數據更新為 10 。之后請求 A 在向 Redis 寫數據時出現卡頓(延遲) 。
    3. 請求 B 操作:請求 B 在請求 A 寫 MySQL 之后開始操作。請求 B 先將 MySQL 中的數據更新為 11 ,接著順利將 Redis 中的數據也更新為 11 。
    4. 請求 A 后續操作:請求 A 卡頓結束后,繼續執行向 Redis 寫數據的操作,將 Redis 中的數據更新為 10 。這就導致 Redis 中的數據與 MySQL 中的數據(此時 MySQL 中為 11 )不一致 。

這種情況產生的原因在于高并發環境下,請求執行順序和延遲導致寫 Redis 操作的先后出現差異,使得最終 MySQL 和 Redis 中的數據狀態不一致。如果此時有讀請求,按照先讀 Redis 若沒有再讀 DB 且讀請求不回寫 Redis 的規則,就可能讀到不一致的數據 。

  1. 先寫 Redis,再寫 MySQL
    此方案與先寫 MySQL 再寫 Redis 類似,在高并發情況下,由于操作順序的原因,極易出現數據不一致的問題。例如,當 Redis 寫入成功但 MySQL 寫入失敗時,后續的讀操作可能會讀取到 Redis 中已更新但 MySQL 中未更新的數據,從而產生不一致。
    在這里插入圖片描述

  2. 先刪除 Redis,再寫 MySQL
    當存在更新請求 A 和讀請求 B 時,請求 A 先刪除 Redis 緩存,若此時更新 MySQL 的操作耗時較長,而請求 B 的讀請求快速執行,并且讀請求會回寫 Redis,那么在請求 A 的 MySQL 更新尚未完成時,請求 B 可能會將舊數據回寫到 Redis 中,導致數據不一致。
    在這里插入圖片描述

(二)可靠的方案

  1. 先刪除 Redis,再寫 MySQL,再刪除 Redis(緩存雙刪)
    為解決先刪除 Redis 再寫 MySQL 帶來的不一致問題,緩存雙刪方案應運而生。即先刪除 Redis 緩存,然后更新 MySQL 數據,最后再次刪除 Redis 緩存。為確保最后一次刪除操作在回寫緩存之后執行,不建議采用簡單的等待固定時間(如 500ms)的方式,推薦使用異步串行化刪除,將刪除請求放入隊列中,這樣既能保證異步操作不影響線上業務,又能通過串行化處理在并發情況下正確刪除緩存。若雙刪失敗,可借助消息隊列的重試機制,或者自建表記錄重試次數來實現重試。
    在這里插入圖片描述

  2. 先寫 MySQL,再刪除 Redis
    對于一些對一致性要求不是極高的業務場景,此方案下存在的短暫不一致是可以接受的。比如在秒殺、庫存服務等對一致性要求嚴格的業務中,這種方案可能不太適用。出現不一致的情況需要滿足緩存剛好自動失效,且請求 B 從數據庫查出舊數據回寫緩存的耗時比請求 A 寫數據庫并刪除緩存的時間更長,這種情況發生的概率相對較小。
    在這里插入圖片描述

  3. 先寫 MySQL,通過 Binlog,異步更新 Redis
    該方案通過監聽 MySQL 的 Binlog 日志,以異步的方式將數據更新到 Redis 中。它能保證 MySQL 和 Redis 的最終一致性,但無法保證實時性。在查詢過程中,若緩存中無數據,則直接查詢 DB;若緩存中有數據,也可能存在數據不一致的情況。
    在這里插入圖片描述

二、方案比較:抉擇最優解

  1. 先寫 Redis,再寫 MySQL:若數據庫出現故障,而數據僅存在于緩存中,會導致嚴重的數據不一致問題,且寫數據庫失敗后對 Redis 的逆操作若失敗,處理起來較為復雜,因此不建議使用。
  2. 先寫 MySQL,再寫 Redis:適用于并發量和一致性要求不高的項目。當 Redis 不可用時,需要及時報警并進行線下處理。
  3. 先刪除 Redis,再寫 MySQL:實際應用中使用較少,不推薦采用該方案。
  4. 先刪除 Redis,再寫 MySQL,再刪除 Redis:雖然方案可行,但實現較為復雜,需要借助消息隊列來實現異步刪除 Redis 的操作。
  5. 先寫 MySQL,再刪除 Redis:此方案較為推薦,刪除 Redis 失敗時可進行多次重試,若重試無效則報警。在實時性方面表現較好,適用于高并發場景。
  6. 先寫 MySQL,通過 Binlog,異步更新 Redis:適用于異地容災、數據匯總等場景,結合 binlog 和 kafka 可使數據一致性達到秒級,但不適合純粹的高并發場景,如搶購、秒殺等。
方案優點缺點適用場景
先寫Redis,再寫MySQL無明顯優點數據庫掛掉時,數據存在緩存但未寫入數據庫,會造成數據不一致;寫數據庫失敗后對Redis的逆操作若失敗,處理復雜不推薦用于任何場景
先寫MySQL,再寫Redis實現簡單高并發時易出現數據不一致;Redis不可用時需線下處理并發量和一致性要求不高的項目
先刪除Redis,再寫MySQL無明顯優點出現數據不一致的概率較大,實際應用中較少使用不推薦用于任何場景
先刪除Redis,再寫MySQL,再刪除Redis能解決部分數據不一致問題實現復雜,需借助消息隊列異步刪除Redis對一致性要求極高,且能接受復雜實現的場景
先寫MySQL,再刪除Redis實時性較好,刪除Redis失敗可重試,適用于高并發場景存在短暫不一致的情況,對強一致性要求的業務不適用對一致性要求不是特別強的高并發場景,如一般的電商商品展示等
先寫MySQL,通過Binlog,異步更新Redis能保證最終一致性,適用于異地容災、數據匯總等場景無法保證實時性,不適合高并發場景異地容災、數據匯總等對實時性要求不高的場景

三、項目實戰:代碼實現的精彩呈現

假設我們有一個簡單的博客文章管理系統,需要保證文章標簽數據在 MySQL 和 Redis 中的一致性。采取先寫 MySQL,再刪除 Redis方案,以下是相關的代碼實現示例:

(一)數據更新

  1. 寫操作
    優先操作MySQL:通過事務保證數據庫更新原子性。
    同步刪除Redis緩存:若刪除失敗觸發事務回滾(需結合業務驗證),防止臟數據。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import redis.clients.jedis.Jedis;public class DataUpdate {private static final String DB_URL = "jdbc:mysql://localhost:3306/blog";private static final String DB_USER = "root";private static final String DB_PASSWORD = "password";public static void updateArticleTags(String articleId, String newTags) {Connection conn = null;PreparedStatement pstmt = null;Jedis jedis = new Jedis("localhost", 6379);try {// 連接數據庫conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);// 開啟事務conn.setAutoCommit(false);// 更新 MySQL 數據String sql = "UPDATE articles SET tags =? WHERE id =?";pstmt = conn.prepareStatement(sql);pstmt.setString(1, newTags);pstmt.setString(2, articleId);pstmt.executeUpdate();// 刪除 Redis 緩存jedis.del("article:" + articleId + ":tags");// 提交事務conn.commit();} catch (SQLException e) {try {// 回滾事務if (conn != null) {conn.rollback();}} catch (SQLException ex) {ex.printStackTrace();}e.printStackTrace();} finally {// 關閉資源if (pstmt != null) {try {pstmt.close();} catch (SQLException e) {e.printStackTrace();}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}jedis.close();}}
}

(二)數據獲取

  1. 讀操作
    先查緩存:命中則直接返回數據。
    未命中查DB:查詢結果回寫Redis并設置過期時間,避免緩存穿透。
import redis.clients.jedis.Jedis;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class DataRetrieval {private static final String DB_URL = "jdbc:mysql://localhost:3306/blog";private static final String DB_USER = "root";private static final String DB_PASSWORD = "password";public static String getArticleTags(String articleId) {Jedis jedis = new Jedis("localhost", 6379);String tags = jedis.get("article:" + articleId + ":tags");if (tags == null) {Connection conn = null;PreparedStatement pstmt = null;ResultSet rs = null;try {// 連接數據庫conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);String sql = "SELECT tags FROM articles WHERE id =?";pstmt = conn.prepareStatement(sql);pstmt.setString(1, articleId);rs = pstmt.executeQuery();if (rs.next()) {tags = rs.getString("tags");// 將數據寫入 Redis 緩存,并設置過期時間(例如 60 秒)jedis.setex("article:" + articleId + ":tags", 60, tags);}} catch (SQLException e) {e.printStackTrace();} finally {// 關閉資源if (rs != null) {try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if (pstmt != null) {try {pstmt.close();} catch (SQLException e) {e.printStackTrace();}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}}return tags;}
}

四、總結

通過對 MySQL 與 Redis 緩存一致性的多種方案的分析和實際項目的代碼實現,我們了解到不同方案的優缺點和適用場景。在實際開發中,應根據項目的具體需求,如并發量、一致性要求、業務場景等,選擇合適的方案來保證數據的一致性。希望本文能為你在處理 MySQL 與 Redis 緩存一致性問題時提供有益的參考和幫助。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/75369.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/75369.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/75369.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

DAO 類的職責與設計原則

1. DAO 的核心職責 DAO(Data Access Object,數據訪問對象)的主要職責是封裝對數據的訪問邏輯,但它與純粹的數據實體類(如 DTO、POJO)不同,也與 Service 業務邏輯層不同。 DAO 應該做什么&…

【Kubernetes】如何使用 kubeadm 搭建 Kubernetes 集群?還有哪些部署工具?

使用 kubeadm 搭建 Kubernetes 集群是一個比較常見的方式。kubeadm 是 Kubernetes 提供的一個命令行工具,它可以簡化 Kubernetes 集群的初始化和管理。下面是使用 kubeadm 搭建 Kubernetes 集群的基本步驟: 1. 準備工作 確保你的環境中有兩臺或更多的機…

Pycharm(十二)列表練習題

一、門和鑰匙 小X在一片大陸上探險,有一天他發現了一個洞穴,洞穴里面有n道門, 打開每道門都需要對應的鑰匙,編號為i的鑰匙能用于打開第i道門, 而且只有在打開了第i(i>1)道門之后,才能打開第i1道門&#…

在未歸一化的線性回歸模型中,特征的尺度差異可能導致模型對特征重要性的誤判

通過數學公式來更清晰地說明歸一化對模型的影響,以及它如何改變特征的重要性評估。 1. 未歸一化的情況 假設我們有一個線性回歸模型: y β 0 β 1 x 1 β 2 x 2 ? y \beta_0 \beta_1 x_1 \beta_2 x_2 \epsilon yβ0?β1?x1?β2?x2?? 其…

JS—頁面渲染:1分鐘掌握頁面渲染過程

個人博客:haichenyi.com。感謝關注 一. 目錄 一–目錄二–頁面渲染過程三–DOM樹和渲染樹 二. 頁面渲染過程 瀏覽器的渲染過程可以分解為以下幾個關鍵步驟 2.1 解析HTML,形成DOM樹 瀏覽器從上往下解析HTML文檔,將標簽轉成DOM節點&#…

niuhe插件, 在 go 中渲染網頁內容

思路 niuhe 插件生成的 go 代碼是基于 github.com/ma-guo/niuhe 庫進行組織管理的, niuhe 庫 是對 go gin 庫的一個封裝,因此要顯示網頁, 可通過給 gin.Engine 指定 HTMLRender 來實現。 實現 HTMLRender 我們使用 gitee.com/cnmade/pongo2gin 實現 1. main.go …

openEuler24.03 LTS下安裝HBase集群

前提條件 安裝好Hadoop完全分布式集群,可參考:openEuler24.03 LTS下安裝Hadoop3完全分布式 安裝好ZooKeeper集群,可參考:openEuler24.03 LTS下安裝ZooKeeper集群 HBase集群規劃 node2node3node4MasterBackup MasterRegionServ…

LVGL移植說明

https://www.cnblogs.com/FlurryHeart/p/18104596 參考,里面說明了裸機移植以及freeRTOS系統移植。 移植到linux https://blog.csdn.net/sunchao124/article/details/144952514

ubuntu虛擬機裁剪img文件系統

1. 定制文件系統前期準備 將rootfs.img文件準備好,并創建target文件夾2. 掛載文件系統 sudo mount rootfs.img target #掛載文件系統 sudo chroot target #進入chroot環境3. 內裁剪文件系統 增刪裁剪文件系統 exit #退出chroot環境 sudo umount target…

esp826601s固件燒錄方法(ch340+面包板)

esp826601s固件燒錄方法(ch340面包板) 硬件 stm32f10c8t6,esp826601s,面包板,ch340(usb轉ttl),st_link(供電) 接線 燒錄時: stm32f10c8t6:gnd->負極, 3.3->正極…

Servlet 點擊計數器

Servlet 點擊計數器 引言 Servlet 是 Java 企業版(Java EE)技術中的一種服務器端組件,用于處理客戶端請求并生成動態內容。本文將詳細介紹如何使用 Servlet 實現一個簡單的點擊計數器,幫助讀者了解 Servlet 的基本用法和原理。 …

LangChain vs. LlamaIndex:深入對比與實戰應用

目錄 引言LangChain 與 LlamaIndex 概述 什么是 LangChain?什么是 LlamaIndex?兩者的核心目標與適用場景 架構與設計理念 LangChain 的架構設計LlamaIndex 的架構設計關鍵技術差異 核心功能對比 數據連接與處理查詢與檢索機制上下文管理能力插件與擴展性…

【Java中級】10章、內部類、局部內部類、匿名內部類、成員內部類、靜態內部類的基本語法和細節講解配套例題鞏固理解【5】

?? 【內部類】干貨滿滿,本章內容有點難理解,需要明白類的實例化,學完本篇文章你會對內部類有個清晰的認知 💕 內容涉及內部類的介紹、局部內部類、匿名內部類(重點)、成員內部類、靜態內部類 🌈 跟著B站一位老師學習…

內容中臺:驅動多渠道營銷的關鍵策略

在數字營銷快速發展的今天,企業需要在多個渠道(網站、社交媒體、移動應用等)上同步管理內容。盡管網站仍是品牌展示的核心,但信息分散、多平臺重復創建內容的問題,讓營銷人員面臨巨大的管理挑戰。 內容中臺&#xff0…

SvelteKit 最新中文文檔教程(17)—— 僅服務端模塊和快照

前言 Svelte,一個語法簡潔、入門容易,面向未來的前端框架。 從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1: Svelte …

CMake 中的置變量

在 CMake 中,變量是存儲和傳遞信息的重要方式。以下是一些常用的 CMake 變量,以表格形式列出,包括它們的名稱、含義和常見用途: 變量名稱含義常見用途CMAKE_CURRENT_SOURCE_DIR當前處理的 CMakeLists.txt 文件所在的源代碼目錄的…

深入解析C++類:面向對象編程的核心基石

一、類的本質與核心概念 1.1 類的基本定義 類是將**數據(屬性)與操作(方法)**封裝在一起的用戶自定義類型,是面向對象編程的核心單元。 // 基礎類示例 class BankAccount { private: // 訪問控制string owner; …

介紹 Docker 的基本概念和優勢,以及在應用程序開發中的實際應用及數組講解

Docker 是一種輕量級的容器化技術,能夠讓開發者將應用程序和其所有依賴項打包成一個獨立的容器,實現快速部署和運行。以下是 Docker 的基本概念和優勢: 基本概念: 鏡像(Image):鏡像是一個只讀的…

在msys2里面的mingw64下面編譯quickjs

其實非常的簡單,就是正常的make 和make install就行了,這里只是簡單的做個編譯過程記錄。 打開開始--程序--里面的msys64里面的mingw64控制臺窗口,切換到quickjs下載解壓縮后的目錄,執行make和make install ndyHP66G5 MINGW64 ~…

el-table實現表頭帶篩選功能,并支持分頁查詢

最開始嘗試了下面方法,發現這種方法僅支持篩選當前頁的數據,不符合產品要求 于是通過查詢資料發現可以結合filter-change事件,當表格的篩選條件發生變化的時候會觸發該事件,調接口獲取符合條件的數據,實現如下 1、表格…