MySQL的MVCC是否解决幻读(最新推荐)
作者:huchao_lingo
MySQL的MVCC是否解决幻读
MySQL事务隔离级别
✓读未提交(ReadUncommitted):最低的隔离级别,会读取到其他事务还未提交的内容,存在脏读。
✓读已提交(ReadCommitted):读取到的内容都是已经提交的,可以解决脏读,但是存在不可重复读。
✓可重复读(RepeatableRead):在一个事务中多次读取时看到相同的内容,可以解决不可重复读,但是存在幻读。但是在InnoDB中不存在幻读问题,对于快照读,InnoDB使用MVCC解决幻读,对于当前读,InnoDB通过gaplocks或next-keylocks解决幻读。
✓串行化(Serializable):最高的隔离级别,串行的执行事务,没有并发事务问题。
MySQL默认的事务隔离级别是可重复读(REPEATABLEREAD)
简单总结一下,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个:READUNCOMMITTED、READCOMMITTED、REPEATABLEREAD、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的数据就是插入不进去,就像某种幻觉一样。幻读演示的执行流程如下:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(READ UNCOMMITTED) | √ | √ | √ |
读已提交(READ COMMITTED) | × | √ | √ |
可重复读(REPEATABLE READ) | × | × | √ |
串行化(SERIALIZABLE) | × | × | × |
在窗口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 matched: 1 Changed: 1 Warnings: 0
第五步,在窗口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中对表执行当前读forupdate可以查询到id为X的记录。
执行步骤 | 客户端1(窗口1) | 客户端2(窗口2) | 说明 |
第 1 步 | set session transaction isolationlevel 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执行当前读forupdate,查询出了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的事务修改.不可重复读现象需要隔离级别为RepeatableRead来规避.在InnoDB中使用MVCC机制实现。
通常来说,MVCC的多版本控制并不能保证在RR隔离级别下完全避免幻读,但InnoDB通过MVCC+Nextkeylock的方式来保证在RR隔离级别下避免部分场景下幻读。
到此这篇关于MySQL的MVCC是否解决幻读的文章就介绍到这了,更多相关MySQL的MVCC解决幻读内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!