MySQL脏读,幻读和不可重复读


Posted in MySQL onMay 11, 2022

MySQL 中事务的隔离

在 MySQL 中事务的隔离级别有以下 4 种:

  • 读未提交(READ UNCOMMITTED)
  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 序列化(SERIALIZABLE)

MySQL 默认的事务隔离级别是可重复读(REPEATABLE READ),这 4 种隔离级别的说明如下。

1.READ UNCOMMITTED

读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。

2.READ COMMITTED

读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。

3.REPEATABLE READ

可重复读,是 MySQL 的默认事务隔离级别,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read)。

4.SERIALIZABLE

序列化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。​

简单总结一下,MySQL 的 4 种事务隔离级别对应脏读、不可重复读和幻读的关系如下:

事务隔离级别 脏读 不可重复读 幻读
读未提交(READ UNCOMMITTED)
读已提交(READ COMMITTED) ×
可重复读(REPEATABLE READ) × ×
串行化(SERIALIZABLE) × × ×

只看以上概念会比较抽象,接下来,咱们一步步通过执行的结果来理解这几种隔离级别的区别。

前置知识

1.事务相关的常用命令

# 查看 MySQL 版本
select version();
# 开启事务
start transaction;
# 提交事务
commit;
# 回滚事务
rollback;

2.MySQL 8 之前查询事务的隔离级别

查看全局 MySQL 事务隔离级别和当前会话的事务隔离级别的 SQL 如下:

select @@global.tx_isolation,@@tx_isolation;

以上 SQL 执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

3.MySQL 8 之后查询事务的隔离级别

select @@global.transaction_isolation,@@transaction_isolation;

4.查看连接的客户端详情

每个 MySQL 命令行窗口就是一个 MySQL 客户端,每个客户端都可以单独设置(不同的)事务隔离级别,这也是演示 MySQL 并发事务的基础。以下是查询客户端连接的 SQL 命令:

show processlist;

以上 SQL 执行结果如下: 

MySQL脏读,幻读和不可重复读

5.查询连接客户端的数量

可以使用以下 SQL 命令,查询连当前接 MySQL 服务器的客户端数量:

show status like 'Threads%';

以上 SQL 执行结果如下: 

MySQL脏读,幻读和不可重复读

6.设置客户端的事务隔离级别

通过以下 SQL 可以设置当前客户端的事务隔离级别:

set session transaction isolation level 事务隔离级别;

事务隔离级别的值有 4 个:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。

7.新建数据库和测试数据

创建测试数据库和表信息,执行 SQL 如下:

-- 创建数据库
drop database if exists testdb;
create database testdb;
use testdb;
-- 创建表
create table userinfo(
  id int primary key auto_increment,
  name varchar(250) not null,
  balance decimal(10,2) not null default 0
);
-- 插入测试数据
insert into userinfo(id,name,balance) values(1,'Java',100),(2,'MySQL',200);

创建的表结构和数据如下: 

MySQL脏读,幻读和不可重复读

8.名称约定

接下来会使用两个窗口(两个客户端)来演示事务不同隔离级别中脏读、不可重复读和幻读的问题。其中左边的黑底绿字的客户端下文将使用“窗口 1”来指代,而右边的蓝底白字的客户端下文将用“窗口 2”来指代,

如下图所示: 

MySQL脏读,幻读和不可重复读

脏读

一个事务读到另外一个事务还没有提交的数据,称之为脏读。 脏读演示的执行流程如下:

执行步骤 客户端1(窗口1) 客户端2(窗口2) 说明
第 1 步   set session transaction isolation level read uncommitted;start transaction;select * from userinfo; 设置事务隔离级别为读未提交;开启事务;查询用户列表,其中 Java 用户的余额为 100 元。
第 2 步 start transaction;update userinfo set balance=balance+50 where name='Java';   开启事务;给 Java 用户的账户加 50 元;
第 3 步   select * from userinfo; 查询用户列表,其中 Java 用户的余额变成了 150 元。

1.脏读演示步骤1

设置窗口 2 的事务隔离级别为读未提交,设置命令如下:

set session transaction isolation level read uncommitted;

PS:事务隔离级别读未提交存在脏读的问题。

然后使用命令来检查当前连接窗口的事务隔离界别,如下图所示: 

MySQL脏读,幻读和不可重复读

 开启事务并查询用户列表信息,如下图所示: 

MySQL脏读,幻读和不可重复读

2.脏读演示步骤2

在窗口 1 中开启一个事务,并给 Java 账户加 50 元,但不提交事务,执行的 SQL 如下: 

MySQL脏读,幻读和不可重复读

3.脏读演示步骤3

在窗口 2 中再次查询用户列表,执行结果如下: 

MySQL脏读,幻读和不可重复读

 从上述结果可以看出,在窗口 2 中读取到了窗口 1 中事务未提交的数据,这就是脏读。

4.不可重复读

不可重复读是指一个事务先后执行同一条 SQL,但两次读取到的数据不同,就是不可重复读。

不可重复读演示的执行流程如下:

执行步骤 客户端1(窗口1) 客户端2(窗口2) 说明
第 1 步   set session transaction isolation level read committed;start transaction;select * from userinfo; 设置事务隔离级别为读已提交;开启事务;查询用户列表,其中 Java 用户的余额是 100 元。
第 2 步 start transaction;update userinfo set balance=balance+20 where name='Java';commit;   开启事务;给 Java 用户的余额加 20 元;提交事务。
第 3 步   select * from userinfo; 查询用户列表,其中 Java 用户的余额变成了 120 元。

窗口 2 同一个事务中的两次查询,得到了不同的结果这就是不可重复读,具体执行步骤如下。

5.不可重复读演示步骤1

设置窗口 2 的事务隔离级别为读已提交,设置命令如下:

set session transaction isolation level read committed;

PS:读已提交可以解决脏读的问题,但存在不可重复读的问题。

使用命令来检查当前连接窗口的事务隔离界别,如下图所示: 

MySQL脏读,幻读和不可重复读

在窗口 2 中开启事务,并查询用户表,执行结果如下: 

MySQL脏读,幻读和不可重复读

 此时查询的列表中,Java 用户的余额为 100 元。

6.不可重复读演示步骤2

在窗口 1 中开启事务,并给 Java 用户添加 20 元,但不提交事务,再观察窗口 2 中有没有脏读的问题,

具体执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

 从上述结果可以看出,当把窗口的事务隔离级别设置为读已提交,已经不存在脏读问题了。 接下来在窗口 1 中提交事务,执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

7.不可重复读演示步骤3

切换到窗口 2 中再次查询用户列表,执行结果如下: 

MySQL脏读,幻读和不可重复读

 从上述结果可以看出,此时 Java 用户的余额已经变成 120 元了。在同一个事务中,先后查询的两次结果不一致就是不可重复读。

8.不可重复读和脏读的区别

脏读可以读到其他事务中未提交的数据,而不可重复读是读取到了其他事务已经提交的数据,但前后两次读取的结果不同。

幻读

幻读名如其文,它就像发生了某种幻觉一样,在一个事务中明明没有查到主键为 X 的数据,但主键为 X 的数据就是插入不进去,就像某种幻觉一样。

幻读演示的执行流程如下:

执行步骤 客户端1(窗口1) 客户端2(窗口2) 说明
第 1 步   set session transaction isolation level repeatable read;start transaction;select * from userinfo where id=3; 设置事务隔离级别为可重复读;开启事务;查询用户编号为 3 的数据,查询结果为空。
第 2 步 start transaction;insert into userinfo(id,name,balance) values(3,'Spring',100);commit;   开启事务;添加用户,用户编号为 3;提交事务。
第 3 步   insert into userinfo(id,name,balance) values(3,'Spring',100); 窗口 2 添加用户编号为 3 的数据,执行失败。
第 4 步   select * from userinfo where id=3; 查询用户编号为 3 的数据,查询结果为空。

具体执行结果如下步骤所示!

1.幻读演示步骤1

设置窗口 2 为可重复读,可重复有幻读的问题,查询编号为 3 的用户,具体执行 SQL 如下:

set session transaction isolation level repeatable read;
start transaction;
select * from userinfo where id=3;

以上 SQL 执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

 从上述结果可以看出,查询的结果中 id=3 的数据为空。

2.幻读演示步骤2

开启窗口 1 的事务,插入用户编号为 3 的数据,然后成功提交事务,执行 SQL 如下:

start transaction;
insert into userinfo(id,name,balance) values(3,'Spring',100);
commit;

以上 SQL 执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

3.幻读演示步骤3

在窗口 2 中插入用户编号为 3 的数据,执行 SQL 如下:

insert into userinfo(id,name,balance) values(3,'Spring',100);

以上 SQL 执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

添加用户数据失败,提示表中已经存在了编号为 3 的数据,且此字段为主键,不能添加多个。

4.幻读演示步骤4

在窗口 2 中,重新执行查询:

select * from userinfo where id=3;

以上 SQL 执行结果如下图所示: 

MySQL脏读,幻读和不可重复读

/ 在此事务中查询明明没有编号为 3 的用户,但插入的时候却却提示已经存在了,这就是幻读。

5.不可重复读和幻读的区别

二者描述的则重点不同,不可重复读描述的侧重点是修改操作,而幻读描述的侧重点是添加和删除操作。

总结

本文演示了 MySQL 的 4 种事务隔离级别:读未提交(有脏读问题)、读已提交(有不可重复读的问题)、可重复读(有幻读的问题)和序列化,其中可重复读是 MySQL 默认的事务隔离级别。脏读是读到了其他事务未提交的数据,而不可重复读是读到了其他事务已经提交的数据,但前后查询的结果不同,而幻读则是明明查询不到,但就是插入不了。

到此这篇关于一文搞懂MySQL脏读,幻读和不可重复读的文章就介绍到这了!


Tags in this post...

MySQL 相关文章推荐
MySQL性能压力基准测试工具sysbench的使用简介
Apr 21 MySQL
MySQL慢查询的坑
Apr 28 MySQL
详解mysql三值逻辑与NULL
May 19 MySQL
52条SQL语句教你性能优化
May 25 MySQL
MySql开发之自动同步表结构
May 28 MySQL
Unity连接MySQL并读取表格数据的实现代码
Jun 20 MySQL
浅析MySQL如何实现事务隔离
Jun 26 MySQL
MySQL约束超详解
Sep 04 MySQL
一次MySQL启动导致的事故实战记录
Sep 15 MySQL
教你使用VS Code的MySQL扩展管理数据库的方法
Jan 22 MySQL
MySQL 主从复制数据不一致的解决方法
Mar 18 MySQL
mysql insert 存在即不插入语法说明
Mar 25 MySQL
MySql数据库 查询时间序列间隔
May 11 #MySQL
Mysql中常用的join连接方式
May 11 #MySQL
MySQL的prepare使用以及遇到的bug
May 11 #MySQL
MySQL批量更新不同表中的数据
May 11 #MySQL
mysql查找连续出现n次以上的数字
May 11 #MySQL
mysql如何查询连续记录
May 11 #MySQL
mysql 体系结构和存储引擎介绍
You might like
echo(),print(),print_r()之间的区别?
2006/11/19 PHP
PHP小技巧之JS和CSS优化工具Minify的使用方法
2014/05/19 PHP
php ajax实现文件上传进度条
2016/03/29 PHP
什么是PHP文件?如何打开PHP文件?
2017/06/27 PHP
PHP结合jquery ajax实现上传多张图片,并限制图片大小操作示例
2019/03/01 PHP
js 页面刷新location.reload和location.replace的区别小结
2009/12/24 Javascript
firefox事件处理之自动查找event的函数(用于onclick=foo())
2010/08/05 Javascript
JavaScript类和继承 prototype属性
2010/09/03 Javascript
JavaScript实现页面实时显示当前时间的简单实例
2013/07/20 Javascript
JavaScript实现显示函数调用堆栈的方法
2016/04/21 Javascript
基于JS代码实现实时显示系统时间
2016/06/16 Javascript
详解微信小程序开发之下拉刷新 上拉加载
2016/11/24 Javascript
vue2.0嵌套路由实现豆瓣电影分页功能(附demo)
2017/03/13 Javascript
新手vue构建单页面应用实例代码
2017/09/18 Javascript
浅谈Vue2.0中v-for迭代语法的变化(key、index)
2018/03/06 Javascript
微信小程序实现美团菜单
2018/06/06 Javascript
js限制input只能输入有效的数字(第一个不能是小数点)
2018/09/28 Javascript
解决vue动态路由异步加载import组件,加载不到module的问题
2020/07/26 Javascript
JS如何生成动态列表
2020/09/22 Javascript
python网络编程学习笔记(九):数据库客户端 DB-API
2014/06/09 Python
Pycharm导入Python包,模块的图文教程
2018/06/13 Python
python使用Matplotlib画饼图
2018/09/25 Python
linux安装python修改默认python版本方法
2019/03/31 Python
python类中super() 的使用解析
2019/12/19 Python
sqlalchemy实现时间列自动更新教程
2020/09/02 Python
Booking.com西班牙:全球酒店预订
2018/03/30 全球购物
万宝龙英国官网:Montblanc手表、书写工具、皮革和珠宝
2018/10/16 全球购物
初中同学聚会感言
2014/02/11 职场文书
公务员政审单位鉴定材料
2014/05/16 职场文书
三好学生先进事迹材料
2014/08/28 职场文书
2014年煤矿工人工作总结
2014/12/08 职场文书
母亲节主题班会
2015/08/14 职场文书
FFmpeg视频处理入门教程(新手必看)
2022/01/22 杂记
Ruby序列化和持久化存储 Marshal和Pstore介绍
2022/04/18 Ruby
彻底弄懂Python中的回调函数(callback)
2022/06/25 Python
keepalived + nginx 实现高可用方案
2022/12/24 Servers