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` `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中对表执行当前读 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 隔离级别下避免部分场景下幻读。

相关推荐
打鱼又晒网17 分钟前
【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
数据库·mysql
大白要努力!23 分钟前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
tatasix1 小时前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。1 小时前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度1 小时前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮1 小时前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9992 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️2 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98763 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发