大数跨境
0
0

深度好文:MySQL InnoDB 事务隔离与 MVCC、版本链与 ReadView 原理详解

深度好文:MySQL InnoDB 事务隔离与 MVCC、版本链与 ReadView 原理详解 搜狐技术产品
2025-12-03
1

余弦:码哥,上一章说道 MySQL 的全局锁、表锁、意向锁、间隙锁(Gap Lock)、记录锁(Record Lock)、临键锁(Next-Key Lock)以及死锁的形成和优化。

我去面试的时候滔滔不绝,感觉胜利在握,可是面试官忽然到:“什么是 MVCC,MySQL 有了各种锁?为什么还要射界 MVCC?”

这是个好问题!

MVCC(Multi-Version Concurrency Control)中文叫做多版本并发控制协议,是 MySQL InnoDB 引擎用于控制数据并发访问的协议。

今天我就带你从 MVCC 基本原理说起,并且教你鬼狐一般隔离级别、版本连、Read View 的作用。

为什么需要 MVCC

在 MVCC 出现之前,数据库主要依靠锁机制来解决并发冲突。但锁机制存在明显的瓶颈:

锁的代价:

  • 阻塞等待:写锁会阻塞读锁,读锁会阻塞写锁
  • 死锁风险:多个事务相互等待对方释放锁
  • 并发度低:悲观锁机制限制了系统吞吐量
-- 传统锁机制下的并发问题示例
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 获取写锁

-- 事务2(被阻塞)
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 等待读锁,直到事务1提交

试想一下,如果一个线程准备执行 UPDATE 一行数据,如果这时候阻塞住了所有的 SELECT 语句,那么这个性能你能接受吗?

余弦:“那肯定不行,所以 MVCC 的核心思想是为每个数据项维护多个版本,读写操作可以并发进行而不相互阻塞?”

聪明,MVCC 的核心思想是为每一行数据维护多个版本(通常是两个),通过某个时间点的“快照”(Snapshot)来读取数据,从而避免加锁带来的性能损耗,实现非阻塞的读操作:

  • 非阻塞读:读操作不需要等待写操作完成
  • 非阻塞写:写操作不需要等待读操作完成(在合理隔离级别下)。
  • 高并发:大幅提升系统吞吐量。

隔离级别

在深入理解 MVCC 之前,我们现在还需要进步一了解和 MVCC 紧密关联的概念,隔离级别

MySQL 的事务隔离级别有以下四种:

  1. 读未提交(Read Uncommitted):

    1. 是指一个事务可以看到另外一个事务尚未提交的修改。
    2. 设置SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    3. 实现机制不加任何锁,直接读取最新的数据页,无论其是否已提交。性能最好,但数据一致性最差,生产环境极少使用。
  2. 读已提交(Read Committed,简写 RC):

    1. 如何避免脏读:因为每次读都取已提交的最新快照,所以不会读到未提交的数据。
    2. 为何有不可重复读和幻读:因为每次查询的 ReadView 都最新,其他事务的提交会立刻被当前事务看到。
    1. 是指一个事务只能看到已经提交的事务的修改,如果在事务执行过程中有别的事务提交了,那么事务还是能够看到别的事务最新提交的修改。
    2. 设置SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(RC)
    3. 实现机制基于 MVCC,每次 SELECT 语句都会生成一个独立的、最新的 ReadView
  3. 可重复读(Repeatable Read,简写 RR):

    • 如何避免不可重复读:因为整个事务看到的都是同一个“历史快照”,其他事务的提交对当前事务不可见。
    • InnoDB 如何解决幻读:这是 MySQL InnoDB 的精髓!通过 Next-Key Lock(临键锁) 实现。它结合了记录锁(锁住索引项)和间隙锁(锁住索引项之间的间隙)。当执行范围查询时,InnoDB 会锁住整个范围,防止其他事务在这个范围内插入新数据,从而避免了幻读。
    1. 是指在这一个事务内部读同一个数据多次,读到的结果都是同一个。这意味着即便在事务执行过程中有别的事务提交,这个事务依旧看不到别的事务提交的修改。这是 MySQL 默认的隔离级别。

    2. 设置SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;(RR)

    3. 实现机制基于 MVCC,但一个事务中只有第一次 SELECT 会生成 ReadView,后续所有读操作都复用这个 ReadView。

      -- 事务A
      BEGIN;
      SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE-- 会锁住age=20到30这个区间,甚至包括两边的“间隙”

      -- 此时事务B的插入会被阻塞
      INSERT INTO users (name, age) VALUES ('新用户'25); -- 阻塞,直到事务A提交
  4. 串行化(Serializable)是指事务对数据的读写都是串行化的。

    1. 设置SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    2. 实现机制完全摒弃 MVCC,退化为基于锁的并发控制。所有读操作都会加上共享锁,读写操作会相互阻塞。一致性最好,但并发性能最差,像单线程执行一样。

余弦:为什么要隔离?

在并发环境下,如果不进行任何隔离控制,并发事务会引发哪些问题?隔离级别本质上就是为了解决这些问题而存在的。

脏读 (Dirty Read)

一个事务读到了另一个未提交事务修改的数据。如果另一个事务中途回滚,那么第一个事务读到的数据就是“脏”的、无效的。

示例

  • 事务 A 将账户余额从 100 元修改为 200 元(但未提交)。
  • 此时事务 B 读取余额,得到了 200 元这个结果。
  • 事务 A 因某种原因回滚,余额恢复为 100 元。
  • 事务 B 之后的操作都是基于错误的“200 元”余额进行的。

不可重复读 (Non-Repeatable Read)

一个事务内,两次读取同一个数据项,得到了不同的结果。重点在于另一个已提交事务对数据进行了修改(UPDATE)

示例

  • 事务 A 第一次读取账户余额为 100 元。
  • 此时事务 B 提交了修改,将余额更新为 150 元。
  • 事务 A 再次读取余额,得到了 150 元。两次读取结果不一致。

幻读 (Phantom Read)

一个事务内,两次执行同一个查询,返回的结果集行数不同。重点在于另一个已提交事务对数据进行了增删(INSERT/DELETE),像产生了幻觉一样。

示例

  • 事务 A 第一次查询年龄小于 30 岁的员工,返回了 10 条记录。
  • 此时事务 B 提交了一个新操作,插入了一名 25 岁的员工记录。
  • 事务 A 再次执行相同的查询,返回了 11 条记录。

不可重复读 vs 幻读

  • 不可重复读针对的是某一行数据的值被修改(UPDATE)。
  • 幻读针对的是结果集的行数发生变化(INSERT/DELETE)。

SQL 标准下的四种事务隔离级别

为了解决上述问题,SQL 标准定义了四种隔离级别,严格程度从低到高。级别越高,能解决的问题越多,但并发性能通常越低。

隔离级别
脏读
不可重复读
幻读
读未提交 (Read Uncommitted)
❌ 可能
❌ 可能
❌ 可能
读已提交 (Read Committed)
✅ 避免
❌ 可能
❌ 可能
可重复读 (Repeatable Read)
✅ 避免
✅ 避免
❌ 可能
串行化 (Serializable)
✅ 避免
✅ 避免
✅ 避免

注意: 在 MySQL 的 InnoDB 引擎中,通过 Next-Key Locking 技术,在可重复读(Repeatable Read) 隔离级别下就已经可以避免绝大部分的幻读现象。

这是 MySQL 对标准隔离级别的增强,也是其默认使用该级别的重要原因。

另外,还有两个概念你需要掌握:

  • 快照读:快照读就是在事务开始的时候创建了一个数据的快照,在整个事务过程中都读这个快照;
  • 当前读,则是每次都去读最新数据。MySQL 在可重复读这个隔离级别下,查询的执行效果和快照读非常接近。

在这里先介绍下我根据 10 年工作经验和面试经验亲手打造的面试专栏 -》《互联网大厂面试高手心法 58 讲》专栏,原价 89元, 现在已经涨价到 20元满 400 人立马涨价,不要再错过。

我向官方申请了 20 张 8 折优惠券,前 20 名还能再优惠下单,不要错过!!!

8 折优惠码

图片

MVCC 架构

MVCC 的实现依赖于三个核心组件:隐藏字段Undo Log 版本链和ReadView

让我们通过架构图来理解它们的关系:

隐藏字段

为了实现 MVCC,InnoDB 引擎给每一行都加了三个额外的字段 trx_id 和 roll_ptr。

  • trx_id:事务 ID,也叫做事务版本号。MVCC 里面的 V 指的就是这个数字。每一个事务在开始的时候就会获得一个 ID,然后这个事务内操作的行的事务 ID,都会被修改为这个事务的 ID。
  • roll_ptr:回滚指针。InnoDB 通过 roll_ptr 把每一行的历史版本串联在一起。
  • row_id:如果你没有设置任何主键,那么这个列就会被当成主键来使用。但是它其实和 MVCC 没太大的关系,所以你不需要关注。

数据行的物理结构:

+------------+-------------+---------------+----------+-----------+
| 字段头信息 | DB_TRX_ID   | DB_ROLL_PTR   | 列1数据  | 列2数据   |
+------------+-------------+---------------+----------+-----------+
|  5字节     |  6字节      |  7字节        | 变长     | 变长      |
+------------+-------------+---------------+----------+-----------+

Undo Log 版本链

Undo Log 是 MVCC 的基石,它记录了数据变更的历史版本。每次数据修改时,InnoDB 都会在 Undo Log 中保存修改前的数据副本。

Undo Log 的类型:

  • INSERT Undo Log:记录插入操作,事务回滚时直接删除
  • UPDATE Undo Log:记录更新/删除操作,用于回滚和 MVCC。

版本链的构建过程:

余弦:现在问题来了,假如这个时候我有一个新的事务 400,那么我该读取 trx_id 为几的数据呢?

这就涉及到了另外一个和 MVCC 紧密相关的概念:Read View。

Read View

Read View 你可以理解成是一种可见性规则。前面你已经知道了 undolog 里面存放着历史版本的数据,当事务内部要读取数据的时候,Read View 就被用来控制这个事务应该读取哪个版本的数据。

class ReadView {
private:
    trx_id_t m_low_limit_id;    // 高水位:大于等于此值的事务不可见
    trx_id_t m_up_limit_id;     // 低水位:小于此值的事务可见
    trx_id_t m_creator_trx_id;  // 创建该ReadView的事务ID
    ids_t m_ids;                // 活跃事务ID列表
    trx_id_t m_low_limit_no;    // 用于Purge的事务号
};

Read View 最关键的字段叫做 m_ids,它代表的是当前已经开始,但是还没有结束的事务的 ID,也叫做活跃事务 ID。

Read View 只用于已提交读和可重复读两个隔离级别,它用于这两个隔离级别的不同点就在于什么时候生成 Read View。

  • 已提交读:事务每次发起查询的时候,都会重新创建一个新的 Read View。
  • 可重复读:事务开始的时候,创建出 Read View。

可见性判断是 MVCC 最精妙的部分,它遵循一套严格的算法规则:

def check_visibility(trx_id, read_view):
    # 规则1: 当前事务修改的数据始终可见
    if trx_id == read_view.creator_trx_id:
        returnTrue

    # 规则2: 事务ID小于低水位,说明在ReadView创建前已提交
    if trx_id < read_view.up_limit_id:
        returnTrue

    # 规则3: 事务ID大于等于高水位,在ReadView之后开始
    if trx_id >= read_view.low_limit_id:
        returnFalse

    # 规则4: 检查事务是否活跃
    if trx_id in read_view.ids:  # 活跃事务列表
        returnFalse
    else:
        returnTrue# 已提交事务

# 对版本链进行判断
def find_visible_version(version_chain, read_view):
    for version in version_chain:
        if check_visibility(version.trx_id, read_view):
            return version
    returnNone# 没有可见版本

MVCC 在不同隔离级别下的行为有显著差异,这主要通过 ReadView 的生成时机来控制

读已提交

特点: 每次 SELECT 都会生成新的 ReadView。

-- 事务1
BEGIN;
SELECTnameFROMusersWHEREid = 1-- 生成ReadView1,看到事务100的版本

-- 事务2修改并提交
UPDATEusersSETname = 'Charlie'WHEREid = 1;
COMMIT;

-- 事务1再次查询
SELECTnameFROMusersWHEREid = 1-- 生成ReadView2,看到事务200的新版本

可重复读

只在第一次 SELECT 时生成 ReadView,后续查询复用。

-- 事务1
BEGIN;
SELECTnameFROMusersWHEREid = 1-- 生成ReadView,看到事务100的版本

-- 事务2修改并提交
UPDATEusersSETname = 'Charlie'WHEREid = 1;
COMMIT;

-- 事务1再次查询(可重复读)
SELECTnameFROMusersWHEREid = 1-- 使用相同的ReadView,仍看到事务100的版本

总结

面试官在问 MVCC 的时候,都是直接问你这几个问题。

  • 你是否了解 MVCC?
  • MVCC 是什么?
  • MySQL 的 InnoDB 引擎是怎么控制数据并发访问的?
  • 当一个线程在修改数据的时候,另外一个线程还能不能读到数据?
  • 为什么要调整为已提交读?主要原因有两个:一是因为业务用不上,二是为了提升性能。

这时候你就要简明扼要地把原理解释清楚。按照基本定义、实现机制、隔离级别的逻辑顺序来回答。

MVCC 是 MySQL InnoDB 引擎用于控制数据并发访问的协议。

MVCC 主要是借助于版本链来实现的。在 InnoDB 引擎里面,每一行都有两个额外的列,一个是 trx_id,代表的是修改这一行数据的事务 ID。

另外一个是 roll_ptr,代表的是回滚指针。

InnoDB 引擎通过回滚指针,将数据的不同版本串联在一起,也就是版本链。

这些串联起来的历史版本,被放到了 undolog 里面。

当某一个事务发起查询的时候,MVCC 会根据事务的隔离级别来生成不同的 Read View,从而控制事务查询最终得到的结果。

余弦:MySQL 的默认隔离级别是可重复读,实际上互联网的很多应用都调整过这个隔离级别,降低为已提交读。

可重复读比已提交读更加容易引起死锁的问题,比如说我们之前就出现过一个因为临键锁引发的死锁问题。

而且已提交读的性能要比可重复读更好。

所以综合之下,我就推动公司去调整隔离级别,将数据库的默认隔离级别降低为已提交读。

在调整了事务级别之后,万一需要可重复读的特性了,你怎么办?”

首先你要分析使用可重复读的场景:

  • 需要在事务中发起两次同样的查询,并且你希望两次得到的结果是一样的。
  • 需要避开幻读,也就是事务开始之后,即便有别的事务插入了数据并且提交了,你也不希望读到这个新数据。

实际上这种场景真的存在吗?大多数可重复读的需求都是代码没写好!!

比如想要两次查询是同样的数据,那我们可以在第一次读取后的数据缓存。

此外,幻读一般不会认为是一个问题。事务提交往往意味着业务已经结束,所以读到一个已经提交的事务的数据,不会损害业务的正确性。

最后介绍我人生的第一本书《Redis 高手心法》本书基于 Redis 7.0 版本,复杂的概念与实际案例相结合,以简洁、诙谐、幽默的方式揭示了Redis的精髓。

本书完美契合你对一个具体技术学习的期望: Redis 核心原理、关键细节、应用场景以及如何取舍......

从 Redis 的第一人称视角出发,拟人故事化方式和诙谐幽默的言语与各路“神仙”对话,配合 158 张图,由浅入深循序渐进的讲解 Redis 的数据结构实现原理、开发技巧、运维技术和高阶使用,让人轻松愉快地学习。

如下图所示,上市后得到了许多读者的较好口碑评价,而且上过京东榜单!其中还有一些业界大佬、公司 CTO 的推荐。

图片
图片



往期推荐



「深度好文」MySQL 事务 ACID 隔离级别、MVCC和Next-Key Lock详解,架构师必备高并发下的知识图谱

深度解析 InnoDB 如何用MVCC和Next-Key Lock实现RR隔离?看完顿悟!

「深度好文」MySQL 全局锁、表锁、行锁、间隙锁、临键锁详解,架构师必备高并发下的锁优化策略

2w 字 MySQL面试真题(图文并茂版):事务、索引、锁、主从同步、日志、性能优化、分库分表

【声明】内容源于网络
0
0
搜狐技术产品
这里是搜狐技术产品的知识分享平台。作为中国领先的互联网品牌,在拥有媒体、视频、搜索、游戏四大业务平台和超7亿用户的背后,搜狐始终致力于技术的创新与实践,更多实战干货和技术资讯将在此与您分享。
内容 1173
粉丝 0
搜狐技术产品 这里是搜狐技术产品的知识分享平台。作为中国领先的互联网品牌,在拥有媒体、视频、搜索、游戏四大业务平台和超7亿用户的背后,搜狐始终致力于技术的创新与实践,更多实战干货和技术资讯将在此与您分享。
总阅读149
粉丝0
内容1.2k