什么是脏读、不可重复读和幻读

2,359 阅读4分钟

我们知道,事务是一种保障数据原子性、一致性、隔离性和持久性(ACID)的重要机制。
但是用了事务就是安全的吗?用了事务就能100%正确的读写数据了吗?
如果你没用搞懂数据库的隔离机制就盲目使用事务,最终会导致无法确保数据一致性又或者无法对应高并发场景下的数据读写等等问题。

脏读、不可重复读、幻读是如何发生的

让我们来看一些例子,在隔离等级为Read uncommitted(我们后面再说)的情况下执行以下事务操作。 因为没有选择适当的隔离方法,这些操作最终都会发生期待结果和实际结果不一样的情况。

脏读

T1读取了T2未提交的数据。

不可重复读

T1虽然读取到了T2提交的数据,但却不是T1事务开启时的数据。

幻读

T1虽然读取到了T2提交的数据,但这个数据是T1事务开启时不存在的数据。

如果这是一个个人网站,而且用户数不多,每个用户的请求都在不同时间发生,那么上述的情况基本上是不会发生的。 但是如果这是一个高并发场景,而且读写的都是热数据,那么读错或者写错数据的可能性就会大大增加。

如何防止脏读、不可重复读、幻读

有人可能要问了,那么这些情况要怎么避免呢?

主流的关系型数据库都提供了隔离等级,以MySQL为例:

MySQL的隔离等级

MySQL提供了4种隔离等级,等级越高越能保持读写的一致性。 但为了保持一致性也付出了一定的成本。比如说串行化(Serializable),它要求事务序列化执行,比如一个事务在写的时候其他事务不能读。在高并发场景下对系统的负载是比较高的。

隔离等级:READ COMMITTED

隔离等级:REPEATABLE READ

不同的隔离等级的实现方法

重点来了,讲了这么多概念和现象,如果面试官问
为什么READ COMMITTED可以避免脏读?
为什么REPEATABLE READ可以避免不可重复读?
你能答上来吗?

多版本并发控制(Multiversion Concurrency Control)

在MySQL的表中除了用户定义的列以外还会有隐藏列。

列名 长度(字节) 作用
DB_TRX_ID 6 插入或更新行的最后一个事务的事务标识符。(删除视为更新,将其标记为已删除)
DB_ROLL_PTR 7 写入回滚段的撤消日志记录(若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息)
DB_ROW_ID 6 行标识(隐藏单调自增id)

以READ COMMITTED为例,

每当事务对数据进行修改的时候都会将旧的数据会被备份到undo日志中, 然后更新数据库, 再将新数据的DB_ROLL_PTR指向undo日志中被复制的那一条数据。

每当同一条数据被不同事务修改时会重复上面的操作, 最终会形成一个通过DB_ROLL_PTR不断指向旧数据的版本链。

而T1之所以能够读到Empty是因为它读取的不是数据库的最新内容,而是版本链中和TRX_ID == 1的最新状态的数据。

下面这篇文章对MVCC做了详细的介绍,这里就不做赘述了。
推荐阅读:MySQL事务隔离级别和MVCC

记忆方法

脏读

“脏”是相对于“干净”而言的。“干净”的数据就是已经提交了的数据,而“脏”数据是提交之前的数据。读到了提交之前的数据就叫脏读。

不可重复读

重复说的是每一次读取某一条数据结果都是一样的。 不可重复读就是说在一次事务中,两次读取的结果不同,说明在两次读取的过程中有别的事务修改了这条记录。

幻读

幻即幻觉,就是读到了原本不存在的数据。 那么为什么会读到不存在的数据呢,因为在事务过程中别的事务插入了一条记录导致读到了原本不存在的数据。

参考文章

脏读、幻读与不可重复读
MySQL事务隔离级别和MVCC
MySQL中InnoDB的多版本并发控制(MVCC)