缓存(本地缓存、分布式缓存)与数据库之间的数据一致性问题

article/2025/10/5 21:43:15

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 
  • 原创 | Java 2021 超神之路,很肝~

  • 中文详细注释的开源项目

  • RPC 框架 Dubbo 源码解析

  • 网络应用框架 Netty 源码解析

  • 消息中间件 RocketMQ 源码解析

  • 数据库中间件 Sharding-JDBC 和 MyCAT 源码解析

  • 作业调度中间件 Elastic-Job 源码解析

  • 分布式事务中间件 TCC-Transaction 源码解析

  • Eureka 和 Hystrix 源码解析

  • Java 并发源码

来源:blog.csdn.net/ynzhang_it/

article/details/104009994/

  • 我们先看一个最常见的读缓存的例子

  • 方法一、先更新数据库,在更新缓存

  • 方法二、先删除缓存再更新数据库

  • 方法三、延时双删

  • 方法四、队列删除缓存

  • 方法五、binlog订阅删除缓存


在现在的系统架构中,缓存的地位可以说是非常高的。因为在互联网的时代,请求的并发量可能会非常高,但是关系型数据库对于高并发的处理能力并不是非常强,而缓存由于是在内存中处理,并不需要磁盘的IO,所以非常适合于高并发的处理,也就成为了各个系统中必不可少的一部分了。

不过,由此产生的问题也是非常多的,其中一个就是如何保证数据库和缓存之间的数据一致性。

由于数据库的操作和缓存的操作不可能在一个事务中,也就势必会出现数据库写入失败,缓存不能更新,缓存写入失败的补偿机制。具体我们应该怎么做呢?

我们先看一个最常见的读缓存的例子

f173b815bdb286aeccbef1d7f744f4bb.png

在读取缓存的方式中,上图这种方式可以说是最为广泛使用的了。读本身是没有什么问题的,但是,写入缓存的方式,就是保证数据一致性的重中之重了。

这里我们不考虑定时刷新缓存的方式,也就是下面这类方式:

43f81da87726552d8fae13eea4b208c9.png

写入数据库和写入缓存是独立的,写入数据库操作后,需要等待定时服务执行,执行完成后缓存数据才会刷新。

这种方式会导致数据的不一致时间较长,数据刷新时,不管有没有改变的数据,都会重新加载,效率差。当然,并不是说这种方式就没用,还是有一些场景是可以使用的,例如一些系统配置的缓存,而且,这样做缓存刷新,代码量非常少,也便于维护。

我们今天只考虑双写的数据一致性如何来考虑。由于不同的写入方式,可能带来的结果也就是不同的。通常情况下,我们都有哪些写入数据并刷新缓存的方式呢?

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

方法一、先更新数据库,在更新缓存

这套方案是最简单的一种缓存双写方案,我们先来看看流程图

809d52e206822a52141bafb6e84e0b15.png

使用这种双写的方案,只要在数据成功写入数据库后,刷新缓存就可以了,代码简单,维护也很简单。但是,简单的前提下,带来的问题也是很直接的。

首先,线程数据安全无法保证

例如:我们现在同时有两个请求会操作同一条数据,一个是请求A,一个是请求B。请求A需要先执行,请求B后执行,那么数据库的记录就是请求B执行后的记录。

但是,由于一些网络原因或者其他情况,最终执行的顺序可能就变成了:

c336ac4c977a473e1c8d33c6c4cc1a76.png

请求A Update 数据库 -> 请求B Update 数据库 -> 请求B Update 缓存 -> 请求A Update 缓存。

这样的结果会导致:

  1. 数据库和缓存中的数据不一致,从而缓存中的数据就成为了脏数据。

  2. 写入操作多于读操作,就会频繁的刷新缓存,但是这些数据根本没有被读过。这样就会浪费服务器的资源。

因此,这种双写方式很难保证数据一致性,不建议使用。

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:https://github.com/YunaiV/onemall

方法二、先删除缓存再更新数据库

由于上述方式存在的问题,那么我们就考虑,能不能先删除缓存,在更新数据库,这样,在更新数据库的前后,由于缓存中没有数据了,请求就会穿透到数据库直接读取数据然后放入缓存,这样,缓存就不会被频繁的刷新了。

于是,我们就设置了一个新的执行顺序:

a21a3d176834259636ebc1902279f6e3.png

不过,这样一来,新问题又出现了。有两个请求,一个请求A,一个请求B,请求A去写数据,请求B去读数据。当并发量高的时候,就会出现以下情况:

394c584031a82e1f1dcb1b183d38eb91.png

请求A进行写操作,删除缓存 -> 请求B查询发现缓存不存在 -> 请求B去数据库查询得到旧值 -> 请求B将旧值写入缓存 -> 请求A将新值写入数据库

这是,脏数据又出现了。如果我们没有设置缓存的过期时间,那么在下一次下入数据前,脏数据就会一直的存在。针对这种脏数据出现的情况,我们决定在写入数据后,增加一点延时,再删除一次数据,于是就有了方法三。

方法三、延时双删

ac3369384c3b0969ea8286e7a1c48c0d.png

使用延时双删的策略,就能够很好的解决之前我们应该并发所引起的数据不一致的情况。那是不是延时双删就完全没有问题呢?不。

我们来假设一个场景,就是我们做了读写分离,那么使用延时双删可能问出现什么情况呢?

356f1cc0de124d57848a6369b08e40e0.png

请求A进行写操作,删除缓存 -> 请求A将数据写入数据库了 -> 请求B查询缓存发现,缓存没有值 -> 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值 -> 请求B将旧值写入缓存 -> 数据库完成主从同步,从库变为新值。

糟糕,又出现数据不一致了。

然后在看看性能如何,由于需要延时,如果是同步执行,性能必定很差,所以第二次删除只有做成异步,避免影响性能。那异步执行删除就会出现新问题,如果异步线程执行失败了,那么旧数据就不会被删除,数据不一致又出现了。

不行,我们需要向一个一劳永逸的办法,单纯的双删还是不可靠。

方法四、队列删除缓存

90629aa03d9663138ae2916ea59f990c.png

我们在把数据更新到数据库后,把删除缓存的消息加入到队列中,如果队列执行失败,就再次加入到队列执行直到成功为止。

这样,我们就能够有效的保证数据库和缓存的数据一致性了,不管是读写分离还是其他情况,只要队列消息能够保证安全,那么缓存就一定会被刷新。

当然,根据这个方案,我们还可以进一步优化。因为这里我们的缓存刷新时基于业务代码的,也就是说,业务代码和缓存刷新的耦合度很高。有没有办法能够把缓存刷新独立出来,不基于业务代码执行呢?

方法五、binlog订阅删除缓存

为了保证业务代码的独立性,我们可以通过订阅binlog日志的方式来刷新缓存。我们先启动mysql的binlog日志,然后如下图方式设计流程:

2518e83c8cea1881a2d22bdb7a54983e.png

通过binlog的订阅,我们就把业务代码和缓存刷新的非业务代码独立开来。代码量小了,也方便维护了。程序员们也不需要去关心什么时候应该刷新缓存,是不是需要刷新缓存。

当然,实战中,我们还有很多不同的业务场景,可能需要的数据一致性同步方案也不同,这里也只算是一个案例。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

7c277b31e8a697a2a4e8e68619e3f885.png

已在知识星球更新源码解析如下:

c7c1352c4d87ae62b65cd342073aac1d.png

f8834eefce0ac3b788c8da78010402d6.png

b9d895ef2db29d67d700787df6a947d2.png

babc78f88d41d6787d69945a2486c1d0.png

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)

http://chatgpt.dhexx.cn/article/VKbtpD8v.shtml

相关文章

MySQL缓存机制

1. MySQL缓存简介 MySQL缓存机制说明 MySQL缓存机制即缓存sql 文本及缓存结果,用KV形式保存再服务器内存中,如果运行相同的sql,服务器直接从缓存中去获取结果,不需要再去解析、优化、执行sql MySQL缓存失效 在表的结构或数据发生…

为什么数据库缓存如此重要?

您在数据库中拥有的信息越多,它随着时间的推移就会越慢。即使是为支持许多并发请求而精心设计的数据库管理系统也将最终达到极限。 数据库缓存是处理这些性能问题的最常见策略之一。缓存涉及将数据库查询的结果保存在更快,更容易访问的位置。正确完成后…

缓存 (模拟两种:数据库提供数据到缓存、外界提供数据到缓存)

一、缓存的作用 因此就有了缓存的作用: 代码模拟上面缓存的作用如下所示: 就拿以前我们做的通过id查询单条数据功能: 当开启服务器后,客户端访问该通过id查询单条数据功能时(假设连续访问两次)&#xff1a…

mysql的查询缓存

提高单台节点的性能无外乎就那么几种方式,缓存是首当其冲的,因为内存的性能比磁盘高的太多。缓存也是一种典型的空间换时间的策略 缓存的实现也有太多的方式,从静态页面缓存到服务端动态缓存,再到数据库级别缓存等等。随着大数据的…

缓存(本地缓存、分布式缓存)与数据库之间的数据一致性问题?

在现在的系统架构中,缓存的地位可以说是非常高的。因为在互联网的时代,请求的并发量可能会非常高,但是关系型数据库对于高并发的处理能力并不是非常强,而缓存由于是在内存中处理,并不需要磁盘的IO,所以非常…

mysql数据库缓存

一、开启缓存 mysql 开启查询缓存可以有两种方法来开启一种是使用set命令来进行开启,另一种是直接修改my.ini文件来直接设置都是非常的简单的哦。 开启缓存,设置缓存大小,具体实施如下: 1、修改配置文件my.ini windows下是my.i…

mysql 缓存机制

mysql缓存机制就是缓存sql 文本及缓存结果,用KV形式保存再服务器内存中,如果运行相同的sql,服务器直接从缓存中去获取结果,不需要在再去解析、优化、执行sql。 如果这个表修改了,那么使用这个表中的所有缓存将不再有效&#xff0c…

数据库缓存层

一 常见的缓存形式 : 1.文件缓存 (为了避免I/O开销,尽量使用内存缓存) 2.内存缓存 二 为什么要使用缓存 缓存数据是为了让客户端很少甚至不访问数据库服务器进行的数据查询,高并发下,能最大程度降低对数据库服务器的访问压力 一般的数据请求: 用户请求->数据查询->…

SQL查询缓存

适合读者 本教程适合于那些对缓存SQL查询以减少数据库连接与执行的负载、提高脚本性能感兴趣的PHP程序员。 概述 许多站点使用数据库作为站点数据存储的容器。数据库包含了产器信息、目录结构、文章或者留言本,有些数据很可能是完全静态的,这些将会从一个…

数据库之查询缓存

查询缓存配置 查看当前的MySQL数据库是否支持查询缓存SHOW VARIABLES LIKE have_query_cache; 查看当前MySQL是否开启了查询缓存SHOW VARIABLES LIKE query_cache_type; 查看查询缓存的占用大小SHOW VARIABLES LIKE query_cache_size; 查看查询缓存的状态信息show status li…

数据库缓存

一、什么是数据库缓存 我们知道常见的数据库,比如oracle、mysql等,数据都是存放在磁盘中。虽然在数据库层也做了对应的缓存,但这种数据库层次的缓存一般针对的是查询内容,而且粒度也太小,一般只有表中数据没有变更的时…

【技术干货】缓存随谈系列之一:数据库缓存

本文作者: 乔锐杰 现担任上海驻云信息科技有限公司运维总监/架构师。曾任职过黑客讲师、java软件工程师/网站架构师、高级运维、阿里云架构师等职位。维护过上千台服务器,主导过众安保险、新华社等千万级上云架构。在云端运维、分布式集群架构等方面有着丰富的经验。 以…

内连接、外连接、全连接图示语法

外连接 外连接分为外左连接(left outer join)和外右连接(right outer join) left outer join 与 left join 等价, 一般写成left join right outer join 与 right join等价, 一般写成right join左连接:左侧交集部分 语法:…

MySQL 内连接、外连接、左连接、右连接、全连接

建表语句: CREATE TABLE a_table (a_id int(11) DEFAULT NULL,a_name varchar(10) DEFAULT NULL,a_part varchar(10) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8CREATE TABLE b_table (b_id int(11) DEFAULT NULL,b_name varchar(10) DEFAULT NULL,b_part v…

SQL中的各种连接的区别总结(内连接,左连接,左外连接,右连接,右外连接,全连接,全外连接)

在数据库中建立两张表方便大家理解,teacher和student表(student表中的teacherid字段是对应teacher表中的ID,举个例子张三的老师就是李四,没有teacherid就是这个人暂时没有老师) INNER JOIN: 返回两个表的匹配得上的数据,不匹配不…

MYSQL语法:左连接、右连接、内连接、全外连接

文章目录 概念上手使用left join(左连接)rint join(右连接)inner join(内连接,等同join)full join(全连接,等同full outer join) 概念 left join(左连接):返回包括左表中…

MySQL 的等值连接、交叉连接、左外连接 、右外连接、全外连接实例

1. 测试数据 测试数据如下所示,数据库脚本(含数据)在文章最后的附录中给出。 测试工具:MySQL8.0 , Navicat Premium。 首先是一个班级表:class,class表中的数据如下所示。 学生表: student&a…

数据库学习之MySQL (十六)—— SQL99 外连接 左外连接 右外连接 全外连接 交叉连接

文章目录 外连接 之 左外连接 与 右外连接为啥要用外连接全外连接总结 内连接 外连接交叉连接 外连接 之 左外连接 与 右外连接 我们先来看个之前的 女神男朋友的案例 传送:数据库学习之MySQL (十三)——多表查询 SQL92 SQL99 连接种类划分 我们先考虑…

ORACLE的左右连接,全外连接

先上概念 oracle连接分为: 左外连接:左表不加限制,保留左表的数据,匹配右表,右表没有匹配到的行中的列显示为null。 右外连接:右表不加限制,保留右表的数据。匹配左表,左表没有匹配…

SQL的内连接、左连接、右连接、 交叉连接、全外连接

sql表连接分成 外连接 、 内连接 和 交叉连接。. 外连接包括三种,分别是左外连接、右外连接、全外连接。. 对应的sql关键字:LEFT/RIGHT/FULL OUTER JOIN,通常我们都省略OUTER关键字,写成LEFT/RIGHT/FULL JOIN。. 在左、右外连接中都会以一种表为基表,基表的所有行、…