什么是幻读

幻读,即某个事务在执行两次相同的 select 操作中,另一个事务插入了一条数据,导致两次 select 读取出的数据不同,会多出另一个事务插入的数据,违反了事务的隔离性。

举个例子,下图是两个事务,第 3、5 步执行了相同的 sql 查询语句,但是后一次查询比前一次查询,多出了 age = 10 的这一条数据。

image-20200815172545713

MySQL 的 InnoDB 存储引擎在 RR 隔离级别就避免了幻读。

相关基本概念

快照读

通过 MVCC 的方式来读取数据库中的数据。如果读取的行正在进行修改操作,则会读取该行的快照数据。

在 RR 隔离级别下,会读取当前事务对应的行数据版本;而在 RC 隔离级别下,则会读取最新的行快照数据。

例如,普通的 select 就是快照读。

select * from t where ?;

当前读

在某些情况下,我们需要对行数据加锁,以保证数据的一致性。读取的行加锁后,其他事务修改该行,会被阻塞。

对应的 SQL 语句如下:

select ··· for update;

select ··· lock in share mode;

MVCC

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。

对于每个事务,都有一个版本号。每开始一个新的业务,事务版本号会递增。

MVCC 在每行数据后面,新增了隐藏的两列,创建版本号、删除版本号。

对于 select、insert、delete、update 四种操作,MVCC 操作逻辑如下:

SELECT

InnoDB会根据以下条件检查每一行记录:

  1. InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在的,要么是事务自身插入或者修改过的,在事务开始之后才插入的行,事务不会看到。

  2. 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除,在事务开始之前就已经过期的数据行,该事务也不会看到。只有符合上述两个条件的才会被查询出来

INSERT

将当前系统版本号作为数据行快照的创建版本号。

DELETE

将当前系统版本号作为数据行快照的删除版本号。

UPDATE

将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。

行锁

行锁有三种:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  • Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

如何解决幻读问题

对于快照读,即 select * from t where ? 类型的 SQL 语句,读取数据时会比较行数据的版本,根据 MVCC 机制返回数据。

对于当前读,读取时会相应地加行锁,以保证读取的行数据是最新的,并且其他事物不能修改相应的行数据。

参考资料

MySQL 事务隔离级别和锁

MySQL 三万字精华总结

《高性能 MySQL》

《MySQL 技术内幕-InnoDB 存储引擎》