1 背景
最近在复习MySQL相关知识,忽然想到MVCC真的能完全解决不可重复读、幻读问题吗?于是做了下述测试。
2 准备环境
MySQL版本:
➜ ~ mysql --version
mysql Ver 8.0.31 for macos12 on x86_64 (MySQL Community Server - GPL)
MySQL隔离级别:
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
建立测试表:
CREATE TABLE `test_01` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
`age` int DEFAULT NULL,
`msg` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
2 具体测试
2.1 能否出现“不可重复读”问题
- 清表并初始化数据
mysql> truncate test_01;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test_01 values (1,'name-a',11,'msg-a'),(2,'name-b',12,'msg-b');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select * from test_01;
+----+--------+------+-------+
| id | name | age | msg |
+----+--------+------+-------+
| 1 | name-a | 11 | msg-a |
| 2 | name-b | 12 | msg-b |
+----+--------+------+-------+
2 rows in set (0.00 sec)
- 开启两个事务进行测试
3. 测试结果分析
此测试中的步骤1-6均符合预期,但是在步骤7更新了id=2这条记录的msg字段后,查询时发现此记录的name字段也变成了最新的值,而之前我以为会是原始值。
假设事务A的trx_id=1,事务B的trx_id=2,由于无法查到DB_TRX_ID的值,只能猜测原因:
(1)步骤5完成之后,数据如下。
id | name | age | msg | DB_TRX_ID |
---|---|---|---|---|
1 | modify_by_a | 11 | msg-a | 1 |
2 | modify_by_a | 12 | msg-b | 1 |
(2)步骤6由于是快照读,但是快照发生在事务A提交之前,所以此时查数据发现id=2、id=3两条记录的DB_TRX_ID均是1后会去找历史版本并返回,所以查不到事务A的更新。
(3)步骤7的update实际执行的时候是“查询-设置字段的新值-写入”顺序,这里的查询时当前读,所以更新id=2的记录后,id=2的记录的name字段仍然是modify_by_a,而msg变为了modify_by_a,且DB_TRX_ID变更为2,如下:
id | name | age | msg | DB_TRX_ID |
---|---|---|---|---|
1 | modify_by_a | 11 | msg-a | 1 |
2 | modify_by_a | 12 | modify_by_b | 2 |
(4)步骤8查询时仍然是快照读,由于id=1的记录的DB_TRX_ID=1,在事务B的Read View中,事务A(trx_id=1)尚未提交,所以与步骤6中的处理逻辑一样,依然回去找此记录的历史版本。但是由于id=2的记录的DB_TRX_ID与当前事务ID相同,所以会读取这条最新的记录,所以会将事务A修改的name=modify_by_a返回。
(5)这里需要看对“不可重复读”问题是如何理解的。如果“不可重复读”是针对一条记录中的某个字段,那么RR级别下的MVCC可以解决此问题;如果“不可重复读”是针对同一SQL返回的全部字段,那么这个测试说明在事务中同时出现当前读和快照读时,可能出现此问题。
2.1 能否出现“幻读”问题
- 清表并初始化数据
mysql> truncate test_01;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test_01 values (1,'name-a',11,'msg-a'),(2,'name-b',12,'msg-b');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select * from test_01;
+----+--------+------+-------+
| id | name | age | msg |
+----+--------+------+-------+
| 1 | name-a | 11 | msg-a |
| 2 | name-b | 12 | msg-b |
+----+--------+------+-------+
2 rows in set (0.00 sec)
- 开启两个事务进行测试
3. 测试结果分析
此测试中的步骤1-6均符合预期,但是在步骤7加锁后的是当前读,所以获取到了事务A新增的数据,此时出现了幻读。
3 总结
两次测试中出现的问题,均是由于事务中同时出现了快照读和当前读导致的,在事务中要特别注意。