MySQL的MVCC是否解决幻读

MySQL的MVCC是否解决幻读

MySQL事务隔离级别

✓ 读未提交(Read Uncommitted):最低的隔离级别,会读取到其他事务还未提交的内容,存在脏读。

✓ 读已提交(Read Committed):读取到的内容都是已经提交的,可以解决脏读,但是存在不可重复读。

✓ 可重复读(Repeatable Read):在一个事务中多次读取时看到相同的内容,可以解决不可重复读,但是存在幻读。但是在 InnoDB 中不存在幻读问题,对于快照读,InnoDB 使用 MVCC 解决幻读,对于当前读,InnoDB 通过 gap locks 或 next-key locks 解决幻读。

✓ 串行化(Serializable):最高的隔离级别,串行的执行事务,没有并发事务问题。

MySQL 默认的事务隔离级别是可重复读(REPEATABLE READ)

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

事务隔离级别

脏读

不可重复读

幻读

读未提交(READ UNCOMMITTED)

读已提交(READ COMMITTED)

×

可重复读(REPEATABLE READ)

×

×

串行化(SERIALIZABLE)

×

×

×

不可重复度和幻读的区别

不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被修改过。

幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。(可以参考)

模拟幻读问题

环境准备

事务相关的命令

# 查看 MySQL 版本
select version();

# 开启事务
start transaction;

# 提交事务
commit;

# 回滚事务
rollback;

MySQL8查询事务隔离级别的命令

select @@global.transaction_isolation,@@transaction_isolation;

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

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

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

测试数据准备

创建测试数据库和表信息,执行 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> select * from userinfo;
+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
|  1 | Java  | 100.00  |
|  2 | MySQL | 200.00  |
+----+-------+---------+
2 rows in set (0.00 sec)

幻读模拟

接下来会使用两个窗口(两个客户端)来演示事务在可重复读隔离级别下的幻读的问题。

幻读场景1

在一个事务中明明没有查到主键为 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 的数据,查询结果为空。

在窗口2中查询id为3的记录为空

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo where id=3;
Empty set

在窗口1中插入一条id为3的记录

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into userinfo(id,name,balance) values(3,'Spring',100);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

在窗口2中插入id为3的记录

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

发现插入失败

mysql> insert into userinfo(id,name,balance) values(3,'Spring',100);
1062 - Duplicate entry '3' for key 'userinfo.PRIMARY'

继续在窗口2查询id为3的记录,发现查询不到

mysql> select * from userinfo where id=3;
Empty set

幻读场景2

在一个事务A中一开始查询不到id为X的记录,但在另一个事务B中插入id为X的记录并提交事务后,在事务A中再更新id为X的记录可以更新到,再次查询id为X的记录也可以查询到。

执行步骤

客户端1(窗口1)

客户端2(窗口2)

说明

第 1 步

set session transaction isolation

 level repeatable read;

start transaction;

select * from userinfo where id=4;

设置事务隔离级别为可重复读;开启事务;查询用户编号为 4 的数据,查询结果为空。

第 2 步

set session transaction isolation

 level repeatable read;

start transaction;

select * from userinfo where id=4;

insert into userinfo(id,name,balance)

 values(4,'Redis',100);

commit;

开启事务;添加用户,用户编号为 4;提交事务。

第 3 步

select * from userinfo where id=4;

查询用户编号为 3 的数据,查询结果为空。此时还未出现幻读问题

第 4 步

update userinfo set name='kafka' 

where id = 4;

更新用户编号为4的记录,更新成功

第 5 步

select * from userinfo where id=4;

查询到用户编号为4的记录,查询到数据,出现幻读问题

第一步,在窗口1中查询id为4的记录,查询结果为空

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.00 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from userinfo where id=4;

Empty set

第二步,在窗口2中查询id为4的记录,查询结果为空。

插入一条id为4 的记录并提交事务

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.00 sec)
 
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from userinfo where id=4;

Empty set
 
mysql> insert into userinfo(id,name,balance) values(4,'Redis',100);

Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
 
mysql> 

第三步,在窗口1中查询id为4 的记录,查询结果为空,此时还没有幻读问题

mysql> select * from userinfo where id=4;

Empty set

第四步,在窗口1中更新id为4 的记录,更新成功

mysql> update userinfo set name='kafka' where id = 4;

Query OK, 1 row affected (0.00 sec)
Rows matched1  Changed: 1  Warnings0

第五步,在窗口1中查询id为4 的记录,查询到更新后的数据,出现幻读问题

mysql> select * from userinfo where id=4;

+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
|  4 | kafka | 100.00  |
+----+-------+---------+
1 row in set (0.00 sec)

幻读场景3

在事务A中查询id为X的记录查询不到,在事务B中插入一条id为X的记录后,再到事务A中对表执行当前读 for update可以查询到id为X的记录。

执行步骤

客户端1(窗口1)

客户端2(窗口2)

说明

第 1 步

set session transaction isolation
 level repeatable read;

start transaction;

select * from userinfo;

设置事务隔离级别为可重复读;开启事务;查询用户表所有数据,查询结果为4条。

第 2 步

set session transaction isolation

 level repeatable read;

start transaction;

select * from userinfo where id=5;

insert into userinfo(id,name,balance) 

values(5,'Pulsar',100);

commit;

开启事务;添加用户,用户编号为 5;提交事务。

第 3 步

select * from userinfo for update;

执行当前读for update,查询出了id为5的记录,出现幻读问题。

第一步,在窗口1中查询用户表的所有记录,查询结果为4条

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.00 sec)
 
mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from userinfo;

+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | Java   | 100.00  |
|  2 | MySQL  | 200.00  |
|  3 | Spring | 100.00  |
|  4 | kafka  | 100.00  |
+----+--------+---------+
4 rows in set (0.00 sec)

第二步,在窗口2中查询id为5的记录,查询结果为空。

插入一条id为5 的记录并提交事务

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.00 sec)
 
mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from userinfo where id=5;

Empty set
 
mysql> insert into userinfo(id,name,balance) values(5,'Pulsar',100);

Query OK, 1 row affected (0.00 sec)
mysql> commit;

Query OK, 0 rows affected (0.00 sec)

第三步,窗口1执行当前读for update,查询出了id为5的记录,出现了幻读问题。

mysql> select * from userinfo for update;

+----+--------+---------+
| id | name   | balance |
+----+--------+---------+
|  1 | Java   | 100.00  |
|  2 | MySQL  | 200.00  |
|  3 | Spring | 100.00  |
|  4 | kafka  | 100.00  |
|  5 | Pulsar | 100.00  |
+----+--------+---------+
5 rows in set (0.00 sec)

结论

不可重复读的重点在于 update 和 delete, 而幻读着重强调 insert 操作, 当用户第一次读取时,存在结果集 a, b, c,第二次读取时结果集为 a, b, c, d 即多出来的一条数据 d 表示发送幻读现象。

不可重复读的现象是用户第一次读取,返回结果集 a, 而第二次读取返回结果 a’, 即数据发生了变更. 而 a’ 数据确实由一个已经 committed 的事务修改. 不可重复读现象需要隔离级别为 Repeatable Read 来规避. 在 InnoDB 中使用 MVCC 机制实现。

通常来说, MVCC 的多版本控制并不能保证在 RR 隔离级别下完全避免幻读, 但 InnoDB 通过 MVCC + Next key lock 的方式来保证在 RR 隔离级别下避免部分场景下幻读。

MVCC(Multi-Version Concurrency Control)是一种用于解决并发事务中幻读的机制。在MVCC中,每个事务都有一个唯一的事务ID和时间戳。当一个事务开始时,它会创建一个事务快照,该快照包含了在该事务开始之前的数据库状态。如果其他事务在这个事务快照创建后进行了修改,那么这些修改将在事务快照中不可见。 具体来说,MVCC通过保存数据的旧版本来实现。当一个事务开始时,它只能看到在该事务开始之前已经提交的其他事务的修改。如果在事务的执行过程中有新的事务提交了修改,那么这些新的修改对于当前事务是不可见的。 因此,MVCC可以解决幻读问题。幻读是指在同一个事务中,当多次执行相同的查询时,可能会得到不同的结果集,因为其他事务已经插入或删除了符合查询条件的数据。通过MVCC的机制,事务在开始时就创建了一个快照,这个快照包含了在事务开始之前的数据库状态。因此,在整个事务的执行过程中,事务只能看到在事务开始之前已经提交的其他事务的修改,而无法看到其他事务的插入或删除操作。 总之,MVCC通过保存旧版本的数据来防止幻读的发生。每个事务在开始时都会创建一个事务快照,该快照包含了在事务开始之前已经提交的其他事务的修改。这样,事务在执行过程中只能看到在事务开始之前已经提交的数据,从而避免了幻读的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [mysql MVCC产生幻读原因及解决方式](https://blog.csdn.net/weixin_44145478/article/details/119910868)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [详解MySQL是如何解决幻读的](https://download.csdn.net/download/weixin_38730767/13699659)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [一文搞懂MySQLMVCC原理及如何解决幻读](https://blog.csdn.net/weixin_43477531/article/details/121963884)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值