Techoc`s

Techoc`s

事务的隔离级别与MVCC

2026-03-03

事务隔离级别与 MVCC 的详细讲解

1. 事务并发执行遇到的问题

在多个事务并发执行时,如果不加控制,可能会产生以下问题(按严重性排序):

  • 脏写(Dirty Write):如果一个事务修改了另一个未提交事务修改过的数据。这是最严重的问题,任何隔离级别都不允许发生。

  • 脏读(Dirty Read):如果一个事务读到了另一个未提交事务修改过的数据。

  • 不可重复读(Non-Repeatable Read):如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。

  • 幻读(Phantom):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。

    • 注:幻读重点强调读取到了之前读取没有获取到的记录。

2. SQL 标准与 MySQL 中的隔离级别

SQL 标准设立了 4 个隔离级别,针对不同级别,并发事务可能发生的问题如下:

隔离级别

脏读

不可重复读

幻读

READ UNCOMMITTED (未提交读)

可能

可能

可能

READ COMMITTED (已提交读)

不可能

可能

可能

REPEATABLE READ (可重复读)

不可能

不可能

可能

SERIALIZABLE (可串行化)

不可能

不可能

不可能

MySQL 的特殊情况:

  • MySQL 支持上述 4 种隔离级别。

  • 默认隔离级别REPEATABLE READ

  • 与 SQL 标准不同,MySQL 在 REPEATABLE READ 隔离级别下,是可以禁止幻读问题的发生的

设置隔离级别:
可以通过以下语句修改事务隔离级别:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

level 可选值:REPEATABLE READ, READ COMMITTED, READ UNCOMMITTED, SERIALIZABLE

  • GLOBAL:只对执行完该语句之后产生的会话起作用。

  • SESSION:对当前会话的所有后续事务有效。

  • 都不加:只对当前会话中下一个即将开启的事务有效。

3. MVCC(多版本并发控制)原理

MVCC 是指在使用 READ COMMITTEDREPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程。这使得不同事务的读 - 写、写 - 读操作并发执行,从而提升系统性能。

3.1 隐藏列与版本链

对于使用 InnoDB 存储引擎的表,聚簇索引记录中包含两个必要的隐藏列:

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列。

  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,这个隐藏列相当于一个指针,可以通过它找到该记录修改前的信息。

每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性)。可以将这些 undo 日志连起来,串成一个链表,称之为版本链。版本链的头节点就是当前记录最新的值。

3.2 ReadView

为了判断版本链中的哪个版本是当前事务可见的,InnoDB 提出了 ReadView 的概念。ReadView 主要包含 4 个重要内容:

  • m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。

  • min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id。

  • max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。

  • creator_trx_id:表示生成该 ReadView 的事务的事务 id。

可见性判断规则:
在访问某条记录时,按照以下步骤判断记录的某个版本是否可见:

  1. 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,该版本可见

  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,该版本可见

  3. 如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,该版本不可见

  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_idmax_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中:

    • 如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可见

    • 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可见

如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,继续判断,直到版本链中的最后一个版本。

3.3 READ COMMITTED 与 REPEATABLE READ 的区别

这两个隔离级别的核心区别在于生成 ReadView 的时机不同

  • READ COMMITTED (RC)每次读取数据前都生成一个 ReadView

    • 在执行 SELECT 语句时会先生成一个 ReadView。

    • 这意味着每次查询都能读到其他事务最新提交的数据。

  • REPEATABLE READ (RR)在第一次读取数据时生成一个 ReadView

    • 只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了,而是复用之前的 ReadView。

    • 这意味着在整个事务期间,读取到的数据保持一致,避免了不可重复读。

4. 幻读问题的解决

在 MySQL 中,REPEATABLE READ 隔离级别下幻读问题的解决分为两种情况:

  1. 快照读(普通 SELECT)

    • 利用 MVCC 解决。由于 RR 级别下 ReadView 只在第一次生成后复用,后续查询看不到其他事务新插入并提交的数据(因为新事务的 trx_id 大于 ReadView 的 max_trx_id 或在 m_ids 中),从而避免了幻读。

  2. 当前读(锁定读或写操作)

    • 利用 锁(Locking Reads) 解决。

    • 对于 SELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATE 以及 UPDATEDELETE 等操作,InnoDB 会使用 Gap Locks(间隙锁)Next-Key Locks

    • Gap Locks:锁住记录前的间隙,不允许别的事务在该间隙插入新记录。

    • Next-Key Locks:记录锁 + 间隙锁的合体,既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。

5. 一致性读与锁定读

  • 一致性读(Consistent Reads)

    • 事务利用 MVCC 进行的读取操作,也称为快照读。

    • 所有普通的 SELECT 语句在 READ COMMITTEDREPEATABLE READ 隔离级别下都算是一致性读。

    • 一致性读并不会对表中的任何记录做加锁操作。

  • 锁定读(Locking Reads)

    • 读操作也采用加锁的方式。

    • SELECT ... LOCK IN SHARE MODE:为读取到的记录加 S 锁(共享锁)

    • SELECT ... FOR UPDATE:为读取到的记录加 X 锁(独占锁)

    • 这种读取方式读取的是记录的最新版本,而不是历史版本。

6. Purge 机制

  • insert undo:在事务提交之后就可以被释放掉。

  • update undo:由于还需要支持 MVCC,不能立即删除掉。

  • 为了支持 MVCC,对于 delete mark 操作来说,仅仅是在记录上打一个删除标记,并没有真正将它删除掉。

  • 随着系统的运行,在确定系统中包含最早产生的那个 ReadView 的事务不会再访问某些 update undo 日志以及被打上删除标记的记录后,有一个后台运行的 purge 线程 会把它们真正的删除掉。

总结

  • 事务隔离级别决定了并发事务之间可见性的程度,MySQL 默认使用 REPEATABLE READ

  • MVCC 通过隐藏列(trx_id, roll_pointer)、版本链和 ReadView 机制,实现了非阻塞的读操作,解决了脏读和不可重复读问题,并在快照读场景下解决了幻读问题。

  • RC 与 RR 的核心区别在于 ReadView 的生成时机(每次查询 vs 第一次查询)。

  • 对于当前读(锁定读)场景下的幻读问题,InnoDB 通过 间隙锁(Gap Lock)Next-Key 锁 来解决。