深度剖析 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.mzph.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、表格…