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 隔离级别下避免部分场景下幻读。

相关推荐
ProtonBase10 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
云和数据.ChenGuang5 小时前
Django 应用安装脚本 – 如何将应用添加到 INSTALLED_APPS 设置中 原创
数据库·django·sqlite
woshilys6 小时前
sql server 查询对象的修改时间
运维·数据库·sqlserver
Hacker_LaoYi6 小时前
SQL注入的那些面试题总结
数据库·sql
建投数据7 小时前
建投数据与腾讯云数据库TDSQL完成产品兼容性互认证
数据库·腾讯云
Hacker_LaoYi8 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
岁月变迁呀8 小时前
Redis梳理
数据库·redis·缓存
独行soc8 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
你的微笑,乱了夏天8 小时前
linux centos 7 安装 mongodb7
数据库·mongodb
工业甲酰苯胺9 小时前
分布式系统架构:服务容错
数据库·架构