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

article/2025/9/28 11:18:19

MySQL 事务

本文所说的 MySQL 事务都是指在 InnoDB 引擎下,MyISAM 引擎是不支持事务的。

数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败,什么都不做,其实不是没做,是可能做了一部分但是只要有一步失败,就要回滚所有操作,有点一不做二不休的意思。

假设一个网购付款的操作,用户付款后要涉及到订单状态更新、扣库存以及其他一系列动作,这就是一个事务,如果一切正常那就相安无事,一旦中间有某个环节异常,那整个事务就要回滚,总不能更新了订单状态但是不扣库存吧,这问题就大了。

事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID,缺一不可。今天要说的就是隔离性。

概念说明

以下几个概念是事务隔离级别要实际解决的问题,所以需要搞清楚都是什么意思。

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

可重复读

可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。

不可重复读

对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。

幻读

幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。

事务隔离级别

SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:

读未提交(READ UNCOMMITTED)

读提交 (READ COMMITTED)

可重复读 (REPEATABLE READ)

串行化 (SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。

75d09b2b3f7f25afedc414f91647d200.png

只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。

一探究竟

下面,我们来一一分析这 4 种隔离级别到底是怎么个意思。

如何设置隔离级别

我们可以通过以下语句查看当前数据库的隔离级别,通过下面语句可以看出我使用的 MySQL 的隔离级别是 REPEATABLE-READ,也就是可重复读,这也是 MySQL 的默认级别。

# 查看事务隔离级别 5.7.20 之后

show variables like 'transaction_isolation';

SELECT @@transaction_isolation

# 5.7.20 之后

SELECT @@tx_isolation

show variables like 'tx_isolation'

+---------------+-----------------+

| Variable_name | Value |

+---------------+-----------------+

| tx_isolation | REPEATABLE-READ |

+---------------+-----------------+

稍后,我们要修改数据库的隔离级别,所以先了解一下具体的修改方式。

修改隔离级别的语句是:set [作用域] transaction isolation level [事务隔离级别],

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。

其中作用于可以是

SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只针对当前回话窗口。隔离级别是 {READ

UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

这四种,不区分大小写。

比如下面这个语句的意思是设置全局隔离级别为读提交级别。

mysql> set global transaction isolation level read committed;

MySQL 中执行事务

事务的执行过程如下,以 begin 或者 start transaction 开始,然后执行一系列操作,最后要执行 commit 操作,事务才算结束。当然,如果进行回滚操作(rollback),事务也会结束。

d45ec6738466e267078279aa4c112c17.png

需要注意的是,begin 命令并不代表事务的开始,事务开始于 begin 命令之后的第一条语句执行的时候。例如下面示例中,select * from xxx 才是事务的开始,

begin;

select * from xxx;

commit; -- 或者 rollback;

另外,通过以下语句可以查询当前有多少事务正在运行。

select * from information_schema.innodb_trx;

好了,重点来了,开始分析这几个隔离级别了。

接下来我会用一张表来做一下验证,表结构简单如下:

CREATE TABLE `user` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`name` varchar(30) DEFAULT NULL,

`age` tinyint(4) DEFAULT NULL,

PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

初始只有一条记录:

mysql> SELECT * FROM user;

+----+-----------------+------+

| id | name | age |

+----+-----------------+------+

| 1 | 古时的风筝 | 1 |

+----+-----------------+------+

读未提交

MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。

任何事务对数据的修改都会第一时间暴露给其他事务,即使事务还没有提交。

下面来做个简单实验验证一下,首先设置全局隔离级别为读未提交。

set global transaction isolation level read uncommitted;

设置完成后,只对之后新起的 session 才起作用,对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL,如果用 Navicat 那就要创建新的查询窗口。

启动两个事务,分别为事务A和事务B,在事务A中使用 update 语句,修改 age 的值为10,初始是1 ,在执行完 update 语句之后,在事务B中查询 user 表,会看到 age 的值已经是 10 了,这时候事务A还没有提交,而此时事务B有可能拿着已经修改过的 age=10 去进行其他操作了。在事务B进行操作的过程中,很有可能事务A由于某些原因,进行了事务回滚操作,那其实事务B得到的就是脏数据了,拿着脏数据去进行其他的计算,那结果肯定也是有问题的。

顺着时间轴往表示两事务中操作的执行顺序,重点看图中 age 字段的值。

508f5d98f4c02ff1c41a1646d6e8441c.png

读未提交,其实就是可以读到其他事务未提交的数据,但没有办法保证你读到的数据最终一定是提交后的数据,如果中间发生回滚,那就会出现脏数据问题,读未提交没办法解决脏数据问题。更别提可重复读和幻读了,想都不要想。

读提交

既然读未提交没办法解决脏数据问题,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。

读提交事务隔离级别是大多数流行数据库的默认事务隔离界别,比如 Oracle,但是不是 MySQL 的默认隔离界别。

我们继续来做一下验证,首先把事务隔离级别改为读提交级别。

set global transaction isolation level read committed;

之后需要重新打开新的 session 窗口,也就是新的 shell 窗口才可以。

同样开启事务A和事务B两个事务,在事务A中使用 update 语句将 id=1 的记录行 age 字段改为 10。此时,在事务B中使用 select 语句进行查询,我们发现在事务A提交之前,事务B中查询到的记录 age 一直是1,直到事务A提交,此时在事务B中 select 查询,发现 age 的值已经是 10 了。

这就出现了一个问题,在同一事务中(本例中的事务B),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务A的提交影响了事务B的查询结果,这就是不可重复读,也就是读提交隔离级别。

865161da3f3910ae102ab784e71d5876.png

每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。

读提交解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。

可重复读

可重复是对比不可重复而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,及时其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。

同样的,需改全局隔离级别为可重复读级别。

set global transaction isolation level repeatable read;

在这个隔离级别下,启动两个事务,两个事务同时开启。

首先看一下可重复读的效果,事务A启动后修改了数据,并且在事务B之前提交,事务B在事务开始和事务A提交之后两个时间节点都读取的数据相同,已经可以看出可重复读的效果。

05c99037e2feaa528c0822770090e507.png

可重复读做到了,这只是针对已有行的更改操作有效,但是对于新插入的行记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:

事务A开始后,执行 update 操作,将 age = 1 的记录的 name 改为“风筝2号”;

事务B开始后,在事务执行完 update 后,执行 insert 操作,插入记录 age =1,name = 古时的风筝,这和事务A修改的那条记录值相同,然后提交。

事务B提交后,事务A中执行 select,查询 age=1 的数据,这时,会发现多了一行,并且发现还有一条 name = 古时的风筝,age = 1 的记录,这其实就是事务B刚刚插入的,这就是幻读。

5844a123a2fd854af2d6646a325f95c8.png

要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别其实解决了幻读问题,这会在后面的内容说明

串行化

串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。

MySQL 中是如何实现事务隔离的

首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。

最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。

实现可重复读

为了解决不可重复读,或者为了实现可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。

我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。

2c48b0fc5c3b0ebf2cfd553617229438.png

按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。

在上面介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照。

对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:

当前事务内的更新,可以读到;

版本未提交,不能读到;

版本已提交,但是却在快照创建后提交的,不能读到;

版本已提交,且是在快照创建前提交的,可以读到;

利用上面的规则,再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调,两者主要的区别就是在快照的创建上,可重复读仅在事务开始是创建一次,而读提交每次执行语句的时候都要重新创建一次。

并发写问题

存在这的情况,两个事务,对同一条数据做修改。最后结果应该是哪个事务的结果呢,肯定要是时间靠后的那个对不对。并且更新之前要先读数据,这里所说的读和上面说到的读不一样,更新之前的读叫做“当前读”,总是当前版本的数据,也就是多版本中最新一次提交的那版。

假设事务A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常。如下图所示。

1d5116d4df307b3074520d306c0009e4.png

加锁的过程要分有索引和无索引两种情况,比如下面这条语句

update user set age=11 where id = 1

id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落的加上行锁就可以了。

而下面这条语句

update user set age=11 where age=10

表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据。那怎么办呢,当然也不是加表锁了。MySQL 会为这张表中所有行加行锁,没错,是所有行。但是呢,在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。

解决幻读

上面介绍可重复读的时候,那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现,MySQL 已经在可重复读隔离级别下解决了幻读的问题。

前面刚说了并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。

假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。

2095e7c0042394d559667e5b1e5dcd54.png

此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。

c254a3399041df3439ed32562bb9ebf8.png

如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。

之后,我用下面的两个事务演示一下加锁过程。

2a06d4fb48cbc79fed2d9a6cd7230543.png

在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行update user set name='风筝2号’ where age = 10; 的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10

这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。

总结

MySQL 的 InnoDB 引擎才支持事务,其中可重复读是默认的隔离级别。

读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。

读提交解决了脏读问题,行锁解决了并发更新的问题。并且 MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。


http://chatgpt.dhexx.cn/article/2PRyvdxl.shtml

相关文章

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…

jq遍历元素

jq遍历元素 通过jq遍历元素、并控制一些元素的属性&#xff08;显示/隐藏、value、src等等&#xff09;&#xff0c;是我们在开发之中比较常见的操作&#xff0c;也为我们的业务功能扩宽了方向&#xff0c;接下来我将结合近段时间在工作上的际遇粗略介绍一下一些稍稍复杂的jq遍…