如何保证MySQL与Redis缓存的数据一致性?

news2024/11/16 7:18:43

文章目录

  • 一、引言
  • 二、场景来源
  • 三、高并发解决方案
    • 1. 先更新缓存,再更新数据库
    • 2. 先更新数据库,再更新缓存
    • 3. 先删除缓存,再更新数据库
    • 4. 先更新数据库,再删除缓存
    • 小结
  • 四、拓展方案
    • 1. 分布式锁与分布式事务
    • 2. 消息队列
    • 3. 监听binlog
  • 五、总结
  • 六、参考文章

一、引言

在现代互联网应用中,高并发场景下的数据访问是一个常见的挑战。为了提高数据访问速度,通常会使用 Redis 作为缓存层,但这也会带来数据一致性的难题。在四月份的时候,我参考现有资料编写了一篇数据库和缓存一致性处理方案的文档(原文链接:如何保证数据库、缓存的双写一致?),但总觉得内容有点空洞,介绍不够彻底。
本文将尝试重新介绍一下该问题。

二、场景来源

传统的系统通常基于 MySQL 和 Java 开发,虽然它们在数据持久化和事务处理方面表现出色,但在高并发场景下,单纯依赖数据库已经难以满足快速响应和高吞吐量的需求。而在现代互联网应用中,高性能和高可用性是系统设计的关键目标。
在这里插入图片描述
为了应对这一挑战,越来越多的系统开始引入 Redis 作为缓存层,以提升数据访问速度和系统整体性能。然而随着 Redis 的引入,读取数据的流程也随之变化。
在这里插入图片描述
如果是只读系统,这个流程也不错。但在高并发读写系统中,这个流程就有待完善啦!

三、高并发解决方案

引入Redis后,任何缓存数据的变更都可能会涉及如下三个操作:更新数据库、更新缓存和删除缓存。如果使用排列组合,可能的解决方案有四种:

  • 先更新缓存,再更新数据库
  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存

我们逐个分析上述方案。

1. 先更新缓存,再更新数据库

该方案的步骤如下:
在这里插入图片描述
如果更新缓存成功后,数据库更新失败,就会出现数据库为旧值,缓存为新值的情况。后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持脏数据(数据库中的值为旧值,而Redis缓存为新值),业务应该以数据库数据为准。
在这里插入图片描述
如果更新缓存成功,数据库更新失败,我们重新更新缓存呢?
抛开重新更新缓存时,要单表或多表重新查询数据,再更新数据带来的潜在性能问题,我们直接使用旧值更新,还可能更新失败,也有其他请求更新数据再次陷入脏数据的情况。
在这里插入图片描述
只要缓存进行了更新,后续的读请求在更新数据库前、更新数据库失败并重新更新缓存成功前,如果命中缓存,返回的数据都是未落库的脏数据。
结论:该方案不考虑

2. 先更新数据库,再更新缓存

该方案的步骤如下:
在这里插入图片描述
如果数据库更新成功,缓存更新失败,会出现数据库为最新值,缓存为旧值的情况。后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持数据不一致!
就算上述更新数据库、更新缓存的操作都成功,还是存在并发引发的一致性问题:
在这里插入图片描述
如上图,可以看到经过两次更新后,数据库n更新为3,而缓存n更新为2。在并发读写的场景下,数据存在不一致性问题。
结论:该方案不考虑

3. 先删除缓存,再更新数据库

该方案的步骤如下:
在这里插入图片描述
这是一种很常见的方法。它逻辑较为简单,也易于理解和实现,理论上删除旧缓存后,下次读取时将从数据库获取最新数据。
但在高并发的极端情况下,删除缓存成功后,如果再有大量的并发请求进来,那么请求便会直接到达数据库,对数据库造成巨大的压力。即便使用了一些并发访问策略保障了只有一个请求到达数据库,那也相当于上述第一步的删除的数据又重新加载到Redis中,而且此方案还可能会发生数据不一致性问题。
在这里插入图片描述
通过上图发现删除缓存后,如果有并发读请求进来,那么查询缓存肯定是不存在,则去读取数据库。此时更新数据库n=2的操作还未完成,所以读取到的仍然是旧值n=1。设置缓存n=1后,更新数据库n=2完成。此时数据库n是新值2,而缓存是旧值1,出现了数据不一致的问题。
对此,我们采用延时双删策略优化。即在更新数据库之后,先延迟等待一会儿(等待时间参考该读请求的响应时间+几十毫秒),再继续删除缓存。这样做的目的是确保读请求结束(它已经在数据库中读取到了旧数据,后续会在该请求中更新缓存),写请求可以删除读请求造成的缓存脏数据,保证再删除缓存之后的所有读请求都能读到最新值。
在这里插入图片描述
可以看出此优化的关键在于再次删除前需要等待多长时间。这个时间一般都是根据历史查询请求的响应时间判断的,但实际情况会有浮动。这也导致如果等待的时间过短,则仍然会出现数据不一致的情况;等待时间过长,则等待期间出现数据不一致的时间变长。
另外延时双删策略还需要考虑如果再次删除缓存失败的情况如何处理?
如果再次删除失败将导致后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持了数据的完全不一致!
这个在下文讨论。
结论:该方案不考虑

4. 先更新数据库,再删除缓存

该方案的步骤如下:
在这里插入图片描述
对比以上方案,在大多数情况下,这种方案被认为是一个更好的选择。原因如下:

  • 数据的一致性:这种方法更倾向于保持数据的最终一致性,即使缓存删除失败,也能保证数据的一致性不会长期受损。
  • 用户体验:在方案3并发读写都成功的情况下,还是会出现数据不一致的情况,用户可能会一直看到旧数据,直到缓存过期。相比之下,该方案可以在某种程度上避免这种情况。

但该方案同样也会出现数据不一致性问题,如下图:
在这里插入图片描述

当数据库被更新后,缓存也被删除。接下来的出现读请求3.1和写请求3.2同时进来。
读请求先执行,读取缓存发现未命中后查询数据库并获取数值2,在准备更新缓存n=2时,写请求执行并完成了更新数据库和删除缓存,然后读请求才更新缓存n=2。此时,数据库为新值3,缓存为旧值2。
其实延迟双删策略,算是融合“先删除缓存,再更新数据库”和“先更新数据库,再删除缓存”的策略,可以解决大部分的数据一致性的业务逻辑处理问题。如果再次删除缓存失败,也可以通过重试机制进行一定程度的补救。
结论:推荐使用该方案

小结

从上面的四种方案看,似乎没有一种方案真正能解决并发场景下MySQL数据与Redis缓存数据一致性的问题。如果业务要求必须要满足强一致性,那么不管如何优化缓存策略,似乎都无法满足,那最好的办法是不用缓存。

强一致性:它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。
解决方案是读写串行化,而此方案会大大增加系统的处理效率,吞吐量也会大大降低。

另外在大型分布式系统中,其实分布式事务大多数情况都不会使用,因为维护成本太高了、复杂度也高。所以在分布式系统,我们一般都会推崇最终一致性,即这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。

四、拓展方案

这种双写的场景,其实还有另外三种方案,虽然应用场景并不多,但也确实提供了不同的思路,可以参考下。

1. 分布式锁与分布式事务

使用分布式事务可以确保两个操作的原子性。步骤如下:

  1. 开启事务。
  2. 更新数据库。
  3. 更新 Redis 缓存。
  4. 提交事务。

读写操作流程如下:

写操作读操作
写请求读请求

这种方式确保了数据库和缓存的一致性,适用于对数据一致性要求较高的场景。但它的实现比较复杂,增加了系统的复杂性。而且这种方式也会产生额外的性能开销。

2. 消息队列

使用消息队列可以异步处理数据更新操作,确保数据库和缓存的一致性。通过消息队列可以解耦数据更新的逻辑,提高系统的可扩展性和可靠性。
步骤如下:

  1. 更新数据库。
  2. 发送更新数据的消息到消息队列。
  3. 消费者从消息队列中读取更新数据的消息,删除 Redis 缓存。

3. 监听binlog

canal是阿里巴巴 MySQL binlog 增量订阅&消费组件。它基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。通过监听binlog,也可以实现双写一致性,步骤如下:

  1. 更新数据库
  2. 通过canal采集binlog日志,订阅更新信息并发送到消息队列
  3. 通过ACK手动机制确认处理这条更新消息,删除Redis缓存数据

在这里插入图片描述
尽管该方案看起来也不错了,但是因为引入额外的组件(如Canal、消息队列)复杂性增加了也不少,需要维护和监控这些组件的运行状态,保证组件运行正常。

五、总结

在高并发场景下,确保 MySQL 和 Redis 之间的数据一致性是分布式系统设计中的一个重要挑战。本文介绍了多种解决方案,每种方案都有其适用场景和优缺点。其实并没有一个最优解,更多的是需要综合考虑系统的具体需求、可用资源、性能要求、业务复杂性、维护成本等因素,最后确定出来的方案才是最适合的。
希望看到这里的你有所收获。

六、参考文章

  • 如何下保证MySQL数据库与Redis缓存数据一致性?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2241352.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Chromium 中sqlite数据库操作演示c++

本文主要演示sqlite数据库 增删改查创建数据库以及数据库表的基本操作,仅供学习参考。 一、sqlite数据库操作类封装: sql\database.h sql\database.cc // Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-sty…

pycharm分支提交操作

一、Pycharm拉取Git远程仓库代码 1、点击VCS > Get from Version Control 2、输入git的url,选择自己的项目路径 3、点击Clone,就拉取成功了 默认签出分支为main 选择develop签出即可进行开发工作 二、创建分支(非必要可以不使用&#xf…

河道无人机雷达测流监测系统由哪几部分组成?

在现代水利管理中,河道无人机雷达监测系统正逐渐成为一种重要的工具,为河道的安全和管理提供了强大的技术支持。那么,这个先进的监测系统究竟由哪几部分组成呢? 河道无人机雷达监测系统工作原理 雷达传感器通过发射电磁波或激光束…

使用pycrawlers下载huggingface报错list index out of range问题解决

使用pycrawlers下载huggingface失败 错误:list index out of range 问题描述:当批量下载整个huggingface仓库的时候会报错,并且没有报错提示 分析:应该是哪个链接请求结果不存在数据 结果:当下载仓库存在文件夹下面只…

清华大学提出基于ESKF的松耦合里程计RINO:一种具有非迭代估计的精确、稳健的雷达惯性里程计

Abstract 精确的定位和建图对于实现自动驾驶车辆的自主导航至关重要。然而,当GNSS信号失效或在极端天气条件(例如雾、雨和雪)下,车体运动估计仍然面临重大挑战。近年来,扫描雷达因其较强的穿透能力成为一种有效的解决…

微信小程序之路由跳转传数据及接收

跳转并传id或者对象 1.home/index.wxml <!--点击goto方法 将spu_id传过去--> <view class"item" bind:tap"goto" data-id"{{item.spu_id}}"> 结果: 2.home/index.js goto(event){// 路由跳转页面,并把id传传过去//获取商品idlet i…

tensorflow案例6--基于VGG16的猫狗识别(准确率99.8%+),以及tqdm、train_on_batch的简介

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 本次还是学习API和如何搭建神经网络为主&#xff0c;这一次用VGG16去对猫狗分类&#xff0c;效果还是很好的&#xff0c;达到了99.8% 文章目录 1、tqdm…

海康大华宇视视频平台EasyCVR私有化视频平台服务器选购主要参数有哪些?

在构建现代服务器和视频监控系统时&#xff0c;选择合适的硬件配置和关键技术是确保系统性能和稳定性的基础。服务器选购涉及到多个关键参数&#xff0c;这些参数直接影响到服务器的处理能力、数据存储、网络通信等多个方面。 同时&#xff0c;随着视频监控技术的发展&#xf…

Redisson的可重入锁

初始状态&#xff1a; 表示系统或资源在没有线程持有锁的情况下的状态&#xff0c;任何线程都可以尝试获取锁。 线程 1 获得锁&#xff1a; 线程 1 首次获取了锁并进入受保护的代码区域。 线程 1 再次请求锁&#xff1a; 在持有锁的情况下&#xff0c;线程 1 再次请求锁&a…

Java-01 深入浅出 MyBatis - MyBatis 概念 ORM映射关系 常见ORM 详细发展历史

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

C语言第13节:指针(3)

1. 回调函数 回调函数的基本思想是&#xff0c;将函数指针作为参数传递给另一个函数&#xff0c;并在需要时通过这个函数指针调用对应的函数。这种方式允许一个函数对执行的内容进行控制&#xff0c;而不需要知道具体的实现细节。 回调函数在以下场景中尤为有用&#xff1a; …

Tensorflow基本概念

简介&#xff1a;本文从Graph讲到Session&#xff0c;同时讲解了tf.constant创建tensor的用法和variable需要初始化的知识点&#xff0c;可以给你打好一个学习Tensorflow的基础。本文都是基于TensorFlow1.14.0的版本下运行。 本专栏将会系统的讲解TensorFlow在1.14.0版本下的各…

【包教包会】CocosCreator3.x框架——带翻页特效的场景切换

一、效果演示 二、如何获取 1、https://gitee.com/szrpf/TurnPage 2 2、解压&#xff0c;导入cocos creator&#xff08;版本3.8.2&#xff09;&#xff0c;可以直接运行Demo演示 三、算法思路 1、单场景 页面预制体 通过loadScene来切换页面&#xff0c;无法实现页面特效…

【MySQL 保姆级教学】事务的自动提交和手动提交(重点)--上(13)

目录 1. 什么是事务&#xff1f;2. 事务的版本支持3. 事务提交的方式3.1 事务提交方式的分类3.2 演示的准备的工作3.2.1 创建表3.2.2 MySQL的服务端和客户端3.2.3 调低事务的隔离级别 4. 手动提交4.1 手动提交的命令说明4.2 示例一4.3 示例二4.4 示例三4.5 示例四 5. 自动提交5…

几何合理的分片段感知的3D分子生成 FragGen - 评测

FragGen 来源于 2024 年 3 月 25 日 预印本的文章&#xff0c;文章题目是 Deep Geometry Handling and Fragment-wise Molecular 3D Graph Generation&#xff0c; 作者是 Odin Zhang&#xff0c;侯廷军&#xff0c;浙江大学药学院。FragGen 是一个基于分子片段的 3D 分子生成模…

数据结构笔记(其八)--一般树的存储及其遍历

1.知识总览 一般的树会有多个孩子&#xff0c;所以存储结构也会与二叉树略有不同。 一般树的遍历。 2.双亲表示法 双亲表示法&#xff0c;也是父亲表示法&#xff0c;即每个节点中都存储了其父节点的地址信息。 特性&#xff1a;可以轻易地找到父节点&#xff0c;但寻找孩子节…

Linux系统Centos设置开机默认root用户

目录 一. 教程 二. 部分第三方工具配置也无效 一. 教程 使用 Linux 安装Centos系统的小伙伴大概都知道&#xff0c;我们进入系统后&#xff0c;通常都是自己设置的普通用户身份&#xff0c;而不是 root 超级管理员用户&#xff0c;导致我们在操作文件夹时往往爆出没有权限&am…

医院信息化与智能化系统(21)

医院信息化与智能化系统(21) 这里只描述对应过程&#xff0c;和可能遇到的问题及解决办法以及对应的参考链接&#xff0c;并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图&#xff0c;可以试试PlantUML&#xff0c;告诉GPT你的文件结构&#xff0c;让他给你对应…

【论文阅读】利用SEM二维图像表征黏土矿物三维结构

导言 在油气储层研究中&#xff0c;黏土矿物对流体流动的影响需要在微观尺度上理解&#xff0c;但传统的二维SEM图像难以完整地表征三维孔隙结构。常规的三维成像技术如FIB-SEM&#xff08;聚焦离子束扫描电子显微镜&#xff09;虽然可以获取高精度的3D图像&#xff0c;但成本…

Yocto - 使用Yocto开发嵌入式Linux系统_13 创建定制层

Creating Custom Layers 除了使用社区或供应商提供的现有图层外&#xff0c;我们还将在本章中学习如何为我们的产品创建图层。此外&#xff0c;我们还将了解如何创建机器定义和分布&#xff0c;并从中获益&#xff0c;从而更好地组织我们的源代码。 In addition to using exist…