MySQL可重复读级别能够解决幻读吗

article/2025/9/28 11:22:03

引言

之前在深入了解数据库理论的时候,了解到事物的不同隔离级别可能存在的问题。为了更好的理解所以在MySQL数据库中测试复现这些问题。关于脏读和不可重复读在相应的隔离级别下都很容易的复现了。但是对于幻读,我发现在可重复读的隔离级别下没有出现,当时想到难道是MySQL对幻读做了什么处理?

测试:

创建一张测试用的表dept:


​​CREATE​​​ ​​TABLE​​​ ​​`dept` (​​​​`id` ​​​​int​​​​(11) ​​​​NOT​​​ ​​NULL​​​ ​​AUTO_INCREMENT,​​​​`​​​​name​​​​` ​​​​varchar​​​​(20) ​​​​DEFAULT​​​ ​​NULL​​​​,​​​​PRIMARY​​​ ​​KEY​​​ ​​(`id`)​​​​) ENGINE=InnoDB AUTO_INCREMENT=12 ​​​​DEFAULT​​​ ​​CHARSET=utf8​​​​insert​​​ ​​into​​​ ​​dept(​​​​name​​​​) ​​​​values​​​​(​​​​"后勤部"​​​​)​​

 

根据上面的流程执行,预期来说应该是事物1的第一条select查询出一条数据,第二个select查询出两条数据(包含事物2提交的数据)。

但是在实际测试中发现第二条select实际上也只查询处理一条数据。这是但是根据数据库理论的可重复读的实现(排他锁和共享锁)这是不应该的情况。

在了解实际原因前我们先复习下事物的相关理论。

数据库原理理论

事物

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。在关系数据库中,一个事务可以是一组SQL语句或整个程序。

为什么要有事物

一个数据库事务通常包含对数据库进行读或写的一个操作序列。它的存在包含有以下两个目的:

为数据库操作提供了一个从失败中恢复到正常状态的方法,同时提供了数据库在异常状态下仍能保持一致性的方法。
当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,保证彼此的操作互相干扰。


事物特性

事务具有4个特性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。

原子性(atomicity):
一个事务应该是一个不可分割的工作单位,事务中包括的操作要么都成功,要么都不成功。
一致性(consistency):
事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation):
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据在事物未提交前对并发的其他事务是隔离的,并发执行的各个事务之间不能互相影响。
持久性(durability):
一个事务一旦成功提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
事物之间的几个特性并不是一组同等的概念:

如果在任何时刻都只有一个事物,那么其天然是具有隔离性的,这时只要保证原子性就能具有一致性。

如果存在并发的情况下,就需要保证原子性和隔离性才能保证一致性。

数据库并发事物中存在的问题

如果不考虑事务的隔离性,会发生以下几种问题:

脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
不可重复读:不可重复读是指在对于数据库中的某条数据,一个事务范围内多次查询返回不同的数据值(这里不同是指某一条或多条数据的内容前后不一致,但数据条数相同),这是由于在查询间隔,该事物需要用到的数据被另一个事务修改并提交了。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了其他事务提交的数据。需要注意的是在某些情况下不可重复读并不是问题。
幻读:幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读可能发生在update,delete操作中,而幻读发生在insert操作中。


排他锁,共享锁

排它锁(Exclusive),又称为X 锁,写锁。

共享锁(Shared),又称为S 锁,读锁。

读写锁之间有以下的关系:

一个事务对数据对象O加了 S 锁,可以对 O进行读取操作,但是不能进行更新操作。加锁期间其它事务能对O 加 S 锁,但是不能加 X 锁。
一个事务对数据对象 O 加了 X 锁,就可以对 O 进行读取和更新。加锁期间其它事务不能对 O 加任何锁。
即读写锁之间的关系可以概括为:多读单写

事物的隔离级别

在事物中存在以下几种隔离级别:

读未提交(Read Uncommitted):解决更新丢失问题。如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据不需要加 S 锁。
读已提交(Read Committed):解决了脏读问题。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。这可以通过“瞬间共享读锁”和“排他写锁”实现, 即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成后立刻释放 S 锁,不用等到事物结束。
可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。Mysql默认使用该隔离级别。这可以通过“共享读锁”和“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成并不立刻释放 S 锁,而是等到事物结束后再释放。
串行化(Serializable):解决了幻读的问题的。提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。


MySQL中的隔离级别的实现

上面的内容解释了一些数据库理论的概念,但是在MySQL、ORACLE这样的数据库中,为了性能的考虑并不是完全按照上面介绍的理论来实现的。

MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。

实现(隔离级别为可重复读)

在说到如何实现前先引入两个概念:

系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。

事务版本号:事务开始时的系统版本号。

在MySQL中,会在表中每一条数据后面添加两个字段:

创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值

删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值

SELECT

select时读取数据的规则为:创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。

创建版本号<=当前事务版本号保证取出的数据不会有后启动的事物中创建的数据。这也是为什么在开始的示例中我们不会查出后来添加的数据的原因

删除版本号为空或>当前事务版本号保证了至少在该事物开启之前数据没有被删除,是应该被查出来的数据。

INSERT

insert时将当前的系统版本号赋值给创建版本号字段。

UPDATE

插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行,实际上这里的更新是通过delete和insert实现的。

DELETE

删除时将当前的系统版本号赋值给删除版本号字段,标识该行数据在那一个事物中会被删除,即使实际上在位commit时该数据没有被删除。根据select的规则后开启懂数据也不会查询到该数据。

MVCC真的解决了幻读?

从最开始我们的测试示例和上面的理论支持来看貌似在MySQL中通过MVCC就解决了幻读的问题,那既然这样串行化读貌似就没啥意义了,带着疑问继续测试。

测试前数据:

 

根据上面的结果我们期望的结果是这样的:

id  name
1   财务部
2   研发部

但是实际上我们的经过是:

本来我们希望得到的结果只是第一条数据的部门改为财务,但是结果确实两条数据都被修改了。这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。

快照读和当前读

出现了上面的情况我们需要知道为什么会出现这种情况。在查阅了一些资料后发现在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。

select 快照读

当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

当前读

对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致上面我们测试的那种情况。

select的当前读需要手动的加锁:

​​select​​​ ​​* ​​​​from​​​ ​​table​​​ ​​where​​​ ​​? lock ​​​​in​​​ ​​share mode;​​​​select​​​ ​​* ​​​​from​​​ ​​table​​​ ​​where​​​ ​​? ​​​​for​​​ ​​update​​​​;​​

有个问题说明下

在测试过程中最开始我以为使用begin语句就是开始一个事物了,所以在上面第二次测试中因为先开始的事物1,结果在事物1中却查到了事物2新增的数据,当时认为这和前面MVCC中的select的规则不一致了,所以做了如下测试:

​​SELECT​​​ ​​* ​​​​FROM​​​ ​​information_schema.INNODB_TRX //用于查询当前正在执行中的事物​​

可以看到如果只是执行begin语句实际上并没有开启一个事物。

下面在begin后添加一条select语句:

所以要明白实际上是对数据进行了增删改查等操作后才开启了一个事物。

如何解决幻读

很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:

使用串行化读的隔离级别
MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)
实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值



来源:https://blog.51cto.com/waxyz/5336702

“做程序员,圈子和学习最重要”因为有有了圈子可以让你少走弯路,扩宽人脉,扩展思路,学习他人的一些经验及学习方法!同时在这分享一下是一直以来整理的Java后端进阶笔记文档和学习资料免费分享给大家!需要资料的朋友私信我扣【06】
 


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

相关文章

mysql不可重复读和重复读_MySql隔离级别:RU / RC / RR / S + 脏读 / 不可重复读 / 幻读 / 可重复读...

MySQL 事务 本文所说的 MySQL 事务都是指在 InnoDB 引擎下&#xff0c;MyISAM 引擎是不支持事务的。 数据库事务指的是一组数据操作&#xff0c;事务内的操作要么就是全部成功&#xff0c;要么就是全部失败&#xff0c;什么都不做&#xff0c;其实不是没做&#xff0c;是可能做…

mysql可重复读 加锁_mysql可重复读隔离级别加锁分析

问题 myql可重复读隔离级别下可能会导致插入阻塞&#xff0c;问题复现如下 表中有3列都是int类型 其索引情况如下&#xff1a; id为主索引&#xff0c;c,d为普通索引 现在开始制作问题&#xff1a; 在这里我分别开启两个事务&#xff1a;第一个事务中执行一个update 语句更新一…

可重复读实现原理

不可重复读&#xff1a;事务A多次读取同一个数据&#xff0c;事务B在事务A多次读取的过程中&#xff0c;对数据作了更新&#xff0c;导致事务A多次读取同一个数据时&#xff0c;结果不一致。&#xff08;比如修改行数据&#xff09; 幻读&#xff1a;事务A 按照一定条件进行数据…

mysql “可重复读“ 解决了哪些问题,没有解决哪些问题?

可重复读解决了更新带来的不可重复读问题&#xff0c;但是没有解决插入或者删除带来的幻读问题。这句话&#xff0c;是老八股文了。 但真实情况是这样的吗&#xff1f;这个验证不麻烦&#xff0c;我们可以动手来验证一下。 我使用的是免安装版本的windows mysql8.0.31&#xff…

透彻解读mysql的可重复读、幻读及实现原理

目录 一、事务的隔离级别 二、mysql怎么实现的可重复读 举例说明MVCC的实现 MVCC逻辑流程-插入 MVCC逻辑流程-删除 MVCC逻辑流程-修改 MVCC逻辑流程-查询 三、幻读 快照读和当前读 四、如何解决幻读 事务隔离级别有四种&#xff0c;mysql默认使用的是可重复读&#x…

前端开发:a标签实现下载功能

应用背景 前端项目实现下载文件的功能&#xff0c;在后台没给我们撸接口的情况下&#xff0c;我们可以利用a标签实现下载功能&#xff0c;而且贼简单~ 实现原理 通过a标签的download的属性&#xff0c;将需要下载的文件放在前端项目中&#xff0c;然后href属性访问文件路径&a…

a标签/js下载文件(2020)

a标签/js 下载服务器文件 一、二进制式下载1、responseType&#xff08;请求&#xff09;2、Content-Type&#xff08;响应&#xff09;判断是普通数据还是文件流&#xff08;可选&#xff09;3、Content-Disposition&#xff08;响应&#xff09;和文件名&#xff08;可选&…

a标签下载pdf文件

通过a标签的download属性可以实现下载pdf文件&#xff0c;不过有一个弊端&#xff1a;网站和pdf文件必须在同一域名下才可行&#xff0c;不然就是先打开一个新标签预览&#xff0c;然后点击下载按钮进行下载。

a标签实现文件下载功能

文件下载原理&#xff1a; java后台只能做到返回二进制流或文件给前端&#xff0c;最终在前端页面创建一个a&#xff0c;然后触发a的点击事件实现点击下载效果。 1.无需token的 2.请求头需要token 接口&#xff1a; 点击事件:

前端-基于a标签实现下载功能

最近在一个项目中需要实现下载功能&#xff0c;在前期与后端多次联调尝试使用接口下载文件无果后&#xff0c;最后抱着试一试的心态使用了A标签下载&#xff0c;方法是有效的&#xff0c;但是有部分局限性&#xff01;&#xff01;&#xff01; 使用a标签实现下载的步骤如下&am…

a标签的download属性(荐)

在html 中 a 链接有 download 这样一个属性 它有什么用呢&#xff1f;&#xff01; 我们在页面中提供下载的时候&#xff0c;都需要去配置一些服务端的东西&#xff0c;比如指定 zip 文件就通知浏览器下载这个文件。 但是&#xff0c;比如 .jpg 这样的图片文件&#xff0c;如…

html a标签下载文件

<a href"/user/test/xxxx.txt" download"文件名.txt">点击下载</a>

前端a标签实现文件下载

a标签实现文件下载 如果想通过纯前端技术实现文件下载&#xff0c; 下载的静态文件放项目路径下&#xff0c;A标签下载&#xff0c;herf指定项目路径&#xff0c;加上download属性。a链接默认的是在同一页面打开&#xff0c;如果我们需要打开新的页面&#xff0c;就需要添加t…

html利用a标签实现下载本地的文件

在写html页面的时候&#xff0c;需要在网页上提供一个下载按钮可以下载我自己电脑中的文件。我已经知道了该文件的路径&#xff0c;但是之前看了很多文章都没找到正确办法&#xff0c;一直不知道如何能够下载本地的文件&#xff0c;经过不断实验发现&#xff0c;可以利用a标签中…

一、<a>标签如何实现下载

实习期间负责的第一个项目&#xff1a;广东互联网协会官网。其中有一个很常见的功能----点击下载。 页面截图 在此之前&#xff0c;我所认识的<a>标签只是用于页面跳转的&#xff0c;实现文件下载是如何做到的呢&#xff1f; 答案是&#xff1a;使用href与download属性 …

面试官:如何用a标签实现文件下载?(一文带你手撕知识点)

前言 大家好&#xff0c;今天给大家带来前端小知识&#xff1a;前端利用a标签实现文件&#xff08;图片&#xff09;下载&#xff0c;也就是教大家利用a标签或者是 window.open() 来实现下载功能。 文章目录 前言常用方式方法分析代码实现 常用方式 <a href"url"…

Jquery之遍历元素

J q u e r y Jquery Jquery之遍历元素 使用 e a c h ( ) each() each()方法传入函数两个参数分别为 i n d e x , d o m index,dom index,dom对象。 <body><div>1</div><div>2</div><div>3</div> </body> <script src&quo…

jQuery 遍历数据

在jQuery 中&#xff0c; $.each( )方法主要用于遍历数据&#xff0c;通过该方法&#xff0c;我们可以遍历任何一个对象&#xff0c;比如数组和对象。 语法格式&#xff1a; $.each(object,function(index,ele))示例&#xff1a; &#xff08;1&#xff09;遍历数组的数据 …

jQuery - 元素遍历

前言&#xff1a; 一提到“遍历”,大家一般都能联想到 each() 或者 for()等语法&#xff0c;但是在jQuery中究竟什么是遍历&#xff1f; 什么是遍历&#xff1a; jQuery 遍历&#xff0c;意为"移动"&#xff0c;用于根据其相对于其他元素的关系来"查找&qu…

Jquery-节点遍历4种方法

节点遍历 遍历子元素 遍历同辈元素 遍历前辈元素 其他遍历方法 遍历子元素 children()方法可以用来获取元素的所有子元素 $(selector).children([expr]); 获取<section>的子元素&#xff0c;但不包含子元素的子元素 var $section $("section").childr…