要说幻读,首先要了解MVCC,MVCC叫做多版本并发控制,实际上就是保存了数据在某个时间节点的快照。
我们每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。
还是拿上面的user表举例子,假设我们插入两条数据,他们实际上应该长这样。
| id | name | create_version | delete_version |
|---|---|---|---|
| 1 | 张三 | 1 | |
| 2 | 李四 | 2 |
这时候假设小明去执行查询,此时current_version=3select * from user where id<=3;<br />
同时,小红在这时候开启事务去修改id=1的记录,current_version=4update user set name='张三三' where id=1;<br />
执行成功后的结果是这样的
| id | name | create_version | delete_version |
|---|---|---|---|
| 1 | 张三 | 1 | |
| 2 | 李四 | 2 | |
| 1 | 张三三 | 4 |
如果这时候还有小黑在删除id=2的数据,current_version=5,执行后结果是这样的。
| id | name | create_version | delete_version |
|---|---|---|---|
| 1 | 张三 | 1 | |
| 2 | 李四 | 2 | 5 |
| 1 | 张三三 | 4 |
由于MVCC的原理是查找创建版本小于或等于当前事务版本,删除版本为空或者大于当前事务版本,小明的真实的查询应该是这样select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);<br />
所以小明最后查询到的id=1的名字还是’张三’,并且id=2的记录也能查询到。这样做是为了保证事务读取的数据是在事务开始前就已经存在的,要么是事务自己插入或者修改的。
明白MVCC原理,我们来说什么是幻读就简单多了。举一个常见的场景,用户注册时,我们先查询用户名是否存在,不存在就插入,假定用户名是唯一索引。
- 小明开启事务current_version=6查询名字为’王五’的记录,发现不存在。
小红开启事务current_version=7插入一条数据,结果是这样:
| id | Name | create_version | delete_version | | —- | —- | —- | —- | | 1 | 张三 | 1 |
| | 2 | 李四 | 2 |
| | 3 | 王五 | 7 |
|小明执行插入名字’王五’的记录,发现唯一索引冲突,无法插入,这就是幻读。
