如何理解数据库的几种事务隔离级别?以及如何理解脏读、不可重复度、幻读?
在多用户并发访问数据库时,数据库系统需要通过事务隔离级别来控制不同事务之间的相互影响。不同的隔离级别可以避免或减少在并发环境下可能出现的数据不一致或冲突。常见的事务隔离级别有四种:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable)。与之对应,数据库在并发环境下可能会出现三种常见的问题:脏读(Dirty Read)、不可重复读(Non-Repeatable Read)和幻读(Phantom Read)。
下面,我们先介绍几种常见的事务隔离级别,然后再说明“脏读”、“不可重复读”、“幻读”这几种并发读问题是如何产生的以及在哪些隔离级别下会被避免。
一、数据库事务隔离级别
1. 读未提交(Read Uncommitted)
- 概念:允许一个事务读到另一个事务尚未提交的数据。
- 可能导致的问题:脏读、不可重复读、幻读都有可能发生。
- 特点:这是最低的隔离级别,几乎没有任何并发控制。
场景举例
- 事务A正在更新某条记录,更新尚未提交;事务B此时读到事务A尚未提交的数据。若事务A随后回滚,那么B读到的数据就成为了无效的数据,这就是脏读。
2. 读已提交(Read Committed)
- 概念:只能读到其他事务已经提交的数据,即禁止读到未提交的数据。
- 可能导致的问题:不可重复读、幻读仍可能发生。
- 特点:很多数据库(如 Oracle、默认的 SQL Server 等)采用该隔离级别,能避免脏读,但无法解决不可重复读或幻读。
场景举例
- 在事务B中,先执行一次查询得到某行记录为“值X”。随后,事务A对这条记录进行了更新并提交。再回到事务B中执行同样的查询,发现该记录已变成“值Y”,与先前读到的不一致,这就是不可重复读。
3. 可重复读(Repeatable Read)
- 概念:在一个事务中多次读取同一条记录时,结果始终保持一致,除非该事务自己修改数据。
- 可能导致的问题:幻读依然可能存在。
- 特点:MySQL InnoDB 存储引擎在默认情况下使用该隔离级别,可以避免不可重复读的问题,但对幻读需要额外处理(MySQL 在可重复读隔离级别下通过间隙锁(Gap Lock)等机制,通常也能避免幻读。但在理论上,该级别仍可能出现幻读的情况)。
场景举例
- 在事务B中,多次读取同一条记录,读到的值不会改变(如果没有被本事务修改),避免了不可重复读。
- 但如果在事务B进行“range查询”时,事务A插入了新的记录,可能导致事务B前后两次范围查询的结果条数不一样,出现幻读(如果没有进一步的锁机制来阻止)。
4. 可串行化(Serializable)
- 概念:最高的隔离级别,相当于对同一时间执行的事务进行排队串行化处理,几乎可以避免所有并发问题,包括脏读、不可重复读和幻读。
- 特点:并发性能最差,通常只有在对数据一致性要求极高、并发量相对较低的场景中使用。
场景举例
- 数据库会对相关的表、记录或范围上加锁,确保所有并发事务都像是串行执行的一样,从而避免脏读、不可重复读、幻读。不过,这种强隔离也付出了降低并发性能的代价。
二、脏读、不可重复读和幻读
1. 脏读(Dirty Read)
- 含义:一个事务读到了另一个事务还未提交(甚至还可能回滚)的数据。
- 发生场景:隔离级别为“读未提交”时(Read Uncommitted)。
- 示例:
- 事务A:将某个记录的值从10更新为20,但是暂未提交。
- 事务B:读取该记录时发现值为20。
- 如果事务A随后回滚,撤销更新,值又变回10,那么B就读到了一个从未真正“生效”过的数据,这就是脏读。
2. 不可重复读(Non-Repeatable Read)
- 含义:同一个事务中,多次读取同一条记录却得到不同的结果。
- 发生场景:在“读已提交”级别下会出现不可重复读;“可重复读”隔离级别能够避免不可重复读。
- 示例:
- 事务B(“读已提交”级别)第一次查询记录R,得到的值为10。
- 事务A更新记录R为20并提交。
- 事务B再次查询相同的记录R,却变成了20。前后两次读取到的数据不同,就产生了不可重复读。
3. 幻读(Phantom Read)
- 含义:同一个事务中,多次执行“范围查询”或“聚合操作”时,因其他事务插入或删除了符合范围条件的新数据,导致返回的数据行数或统计结果不一致。
- 发生场景:在“读已提交”和“可重复读”级别下,都有机会发生幻读。只有在“可串行化”级别(或者 MySQL InnoDB 结合间隙锁实现的 MVCC 机制)才能彻底避免幻读。
- 示例:
- 事务B第一次执行:
SELECT * FROM student WHERE score > 60;
得到 10 条记录。 - 事务A向
student
表中插入了一条score=70
的新记录并提交。 - 事务B第二次执行相同查询:
SELECT * FROM student WHERE score > 60;
发现有 11 条记录,与第一次查询的结果不一致。 - 这种新记录“突然出现”的现象称为幻读。
- 事务B第一次执行:
三、隔离级别与并发问题对照表
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 (Read Uncommitted) | 可能 | 可能 | 可能 |
读已提交 (Read Committed) | 避免 | 可能 | 可能 |
可重复读 (Repeatable Read) | 避免 | 避免 | 可能(理论上) / 实际上 MySQL 可避免 |
可串行化 (Serializable) | 避免 | 避免 | 避免 |
- 在实际应用中,读已提交 和 可重复读 是最常见的两个隔离级别:
- Oracle、SQL Server 的默认级别通常是 “读已提交”。
- MySQL InnoDB 的默认级别是 “可重复读”,并在实现中通过间隙锁、Next-Key Lock等机制,实际上也能避免幻读。
四、总结
-
读未提交(Read Uncommitted)
- 容易出现脏读、不可重复读、幻读等问题,隔离级别最低。
-
读已提交(Read Committed)
- 解决了脏读问题,但仍有不可重复读和幻读。
-
可重复读(Repeatable Read)
- 进一步解决不可重复读问题,在理论上仍有幻读可能,但 MySQL InnoDB 实际上也通过机制避免了大多数幻读场景。
-
可串行化(Serializable)
- 隔离级别最高,可以避免所有并发问题,但是会严重影响并发性能。
- 脏读:读取到其他事务未提交的数据。
- 不可重复读:同一个事务中多次读取同一条记录,读取结果不一致。
- 幻读:同一个事务中多次执行范围查询,结果集不一致(新增或删除的“幻影”记录出现或消失)。
选择适合的隔离级别需要在性能与数据一致性之间做权衡。一般而言,“读已提交”或“可重复读”是常见的折中选择。在一些对数据一致性要求极高的场景下,可能会使用“可串行化”级别,但必须承受其带来的性能代价。