多版本并发控制MVCC

发布于:2025-09-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

 MVCC(Multi-Version Concurrency Control,多版本并发控制)。是一个在数据库管理系统中用于处理并发控制的核心技术。理解它对于深入掌握数据库(尤其是 InnoDB、PostgreSQL 等)的工作原理至关重要。

1. 什么是 MVCC?

MVCC 的全称是 多版本并发控制

核心思想:在数据库中,同一份数据可以保留多个历史版本。当事务需要读取数据时,它会根据一定的规则(比如事务的开始时间)看到一个特定的、一致性的“快照”(Snapshot),而不是直接读取最新的、可能还未提交的数据。写操作则会创建一个新版本的数据。

你可以把它想象成一个高效的版本控制系统(如 Git):

  • 读操作:就像 git checkout 到某个特定的 commit 版本,你看到的是那个时间点的完整项目状态,即使之后有新的提交,你的视图也不会变。
  • 写操作:就像创建一个新的 commit,它不会覆盖旧的 commit,而是在旧版本的基础上生成一个新版本。

通过这种方式,读操作和写操作可以不再互相阻塞,从而极大地提高了数据库的并发性能。


2. 为什么需要 MVCC?

在传统的数据库并发控制中,主要使用两种机制:

  1. 锁机制

    • 读-写冲突:当一个事务在读取一行数据时,会给它加上共享锁(S锁)。另一个事务如果想修改这行数据,需要加排他锁(X锁),但 S 锁和 X 锁互斥,所以写事务必须等待读事务完成。反之亦然,写事务会阻塞读事务。
    • 问题:并发度低。读和写操作串行化,性能很差。
  2. 基于时间戳的排序

    • 所有操作按时间戳排序执行,如果操作冲突,则回滚其中一个事务。
    • 问题:事务冲突率高,回滚频繁,性能同样不理想。

MVCC 的出现就是为了解决这些问题,它提供了一种“乐观”的并发控制方式:

  • 读写不冲突:读数据(快照读)不会阻塞写数据,写数据也不会阻塞读数据。这是 MVCC 最大的优势。
  • 非锁定读:大多数情况下,普通的 SELECT 查询不需要加锁,避免了锁的开销和死锁的风险。
  • 实现事务隔离:MVCC 是实现数据库事务隔离级别(特别是 READ COMMITTED 和 REPEATABLE READ)的基础。

3. MVCC 是如何工作的?

MVCC 的实现依赖于三个关键组件:隐藏列Undo Log 和 Read View。我们以最经典的 MySQL InnoDB 存储引擎为例来讲解。

a. 隐藏列

InnoDB 会为每一行数据额外添加三个隐藏的字段:

  • DB_TRX_ID (6字节): 最后修改该行的事务ID。记录了最后一次对这行记录进行 INSERT 或 UPDATE 的事务ID。每次事务修改一行,这个字段都会被更新。
  • DB_ROLL_PTR (7字节): 回滚指针。它指向该行上一个版本的数据在 Undo Log 中的位置。通过这个指针,可以形成一个“版本链”,把一个数据行的所有历史版本串联起来。
  • DB_ROW_ID (6字节): 隐藏的行ID。一个单调递增的ID,当表没有显式主键时,InnoDB会用它来生成一个聚集索引。

版本链示例
假设一行数据被事务 10、事务 20 依次修改。

  1. 初始状态:事务 10 插入一行数据。
    • DB_TRX_ID = 10
    • DB_ROLL_PTR = null (因为是第一个版本)
  2. 事务 20 修改:事务 20 更新了这行数据。
    • InnoDB 不会直接覆盖旧数据,而是:
      1. 将旧版本的数据(DB_TRX_ID=10 的版本)复制到 Undo Log 中。
      2. 在原位置创建一个新版本的数据行。
      3. 更新新版本的字段:DB_TRX_ID = 20
      4. 更新新版本的 DB_ROLL_PTR,让它指向 Undo Log 中旧版本的位置。
    • 现在,通过新版本的 DB_ROLL_PTR,我们可以找到旧版本,形成一条 版本链最新版本(20) -> 旧版本(10)
b. Undo Log

Undo Log 主要有两个作用:

  1. 事务回滚:当一个事务需要回滚时,可以利用 Undo Log 中记录的旧版本数据,将数据恢复到修改之前的状态。
  2. 构建版本链:如上所述,它存储了数据行的历史版本,是 MVCC 实现多版本的关键。当需要读取某个历史版本时,就可以从这里获取。
c. Read View(读视图)

Read View 是事务在执行快照读(普通的 SELECT)时,动态生成的一个“可见性判断”标准。它决定了当前事务能看到版本链上的哪个版本。

Read View 主要包含以下几个重要属性:

  • creator_trx_id: 创建该 Read View 的事务的 ID。
  • trx_ids: 创建 Read View 时,当前系统中所有活跃的(未提交的)读写事务的 ID 列表。
  • up_limit_idtrx_ids 列表中事务 ID 的最小值。如果版本链上某个版本的 DB_TRX_ID 小于 up_limit_id,则表示这个版本在创建 Read View 之前已经提交,所以对当前事务是可见的
  • low_limit_id: 创建 Read View 时,系统应该分配给下一个事务的 ID。如果版本链上某个版本的 DB_TRX_ID 大于或等于 low_limit_id,则表示这个版本是在创建 Read View 之后才开启的事务中修改的,所以对当前事务是不可见的

4. MVCC 如何解决并发问题?

现在,我们把这三个组件结合起来,看看一个 SELECT 语句是如何利用 MVCC 找到它应该看到的数据版本的。我们以 InnoDB 的 REPEATABLE READ(可重复读)隔离级别为例。

核心判断流程
当一个事务(假设 ID 为 T1)执行 SELECT 时,它会获取一个 Read View。然后,它会从版本链的最新版本开始,逐个版本地应用以下规则,直到找到一个可见的版本:

  1. 检查 DB_TRX_ID 是否是自己创建的?

    • 如果 DB_TRX_ID == creator_trx_id,说明这行数据是本事务自己修改的,可见
  2. 检查 DB_TRX_ID 是否小于 up_limit_id

    • 如果 DB_TRX_ID < up_limit_id,说明修改这个版本的事务在当前事务开始前就已经提交了,可见
  3. 检查 DB_TRX_ID 是否大于或等于 low_limit_id

    • 如果 DB_TRX_ID >= low_limit_id,说明修改这个版本的事务是在当前事务开始之后才启动的,不可见。需要根据 DB_ROLL_PTR 去 Undo Log 中查找上一个版本,然后重复整个判断流程。
  4. 检查 DB_TRX_ID 是否在 trx_ids 列表中?

    • 如果 up_limit_id <= DB_TRX_ID < low_limit_id,则需要判断 DB_TRX_ID 是否在活跃事务列表 trx_ids 中。
    • 如果在:说明修改这个版本的事务在当前事务创建 Read View 时还未提交,不可见。需要去 Undo Log 中找上一个版本。
    • 如果不在:说明修改这个版本的事务在当前事务创建 Read View 时已经提交了,可见

最终:如果遍历完整个版本链都找不到可见的版本,说明这行数据对当前事务是不可见的(比如被其他事务删除了)。

REPEATABLE READ vs READ COMMITTED 的关键区别
  • REPEATABLE READ (可重复读)

    • 事务中第一次执行 SELECT 时,会创建一个 Read View,之后该事务内的所有 SELECT 都复用这个 Read View
    • 效果:确保了在同一个事务中,多次读取同一数据的结果是一致的,因为判断可见性的标准(Read View)从未改变。这就是“可重复读”的由来。
  • READ COMMITTED (读已提交)

    • 事务中每次执行 SELECT 时,都会重新创建一个新的 Read View
    • 效果:每次读取都能看到其他已提交事务所做的最新修改。因为每次的 Read View 都是最新的,up_limit_id 和 trx_ids 都会更新,所以之前不可见的版本可能就变得可见了。

5. MVCC 的优缺点

优点
  1. 高并发性:读写操作不阻塞,极大地提高了数据库的并发读写性能。
  2. 非锁定读:避免了读操作加锁带来的开销和死锁风险。
  3. 实现一致性读:为不同隔离级别提供了基础,保证了事务的隔离性。
缺点
  1. 存储空间开销:需要维护多个版本的数据,Undo Log 会占用额外的存储空间。对于长事务或更新频繁的表,Undo Log 可能会变得非常大。
  2. 管理开销:需要额外的逻辑来管理版本链、创建和判断 Read View,增加了数据库的复杂性。
  3. 行版本清理:需要后台线程(如 InnoDB 的 Purge 线程)定期清理已经不再需要的旧版本数据(即没有事务再需要访问它们),否则 Undo Log 会无限增长。这个清理过程本身也消耗资源。
  4. 并非万能:MVCC 主要解决的是 SELECT 的并发问题。对于 UPDATEDELETE 之间的冲突,仍然需要使用(比如行锁、间隙锁、Next-Key Locks)来保证数据的一致性和防止幻读。

6. MVCC 与隔离级别的关系

隔离级别 MVCC 如何工作 能解决的问题
READ UNCOMMITTED (读未提交) 基本不使用 MVCC。直接读取最新的数据,即使它未提交。
READ COMMITTED (读已提交) 每次 SELECT 都创建新的 Read View。只能读到已提交的数据。 解决脏读
REPEATABLE READ (可重复读) 事务中第一次 SELECT 创建 Read View,后续复用。保证同一事务内多次读取结果一致。 解决脏读、不可重复读
(在 InnoDB 中,结合 Next-Key Locks 还能解决幻读)
SERIALIZABLE (可串行化) 基本不使用 MVCC 的快照读。所有 SELECT 语句都会隐式地转换为 SELECT ... LOCK IN SHARE MODE,即加共享锁。读写操作都互相阻塞。 解决所有并发问题(脏读、不可重复读、幻读)

7. 总结

MVCC 是一种优雅而强大的并发控制技术,其精髓在于“用空间换时间,用版本换锁”。

  • 核心:通过为数据维护多个版本,让读操作访问历史快照,写操作创建新版本。
  • 关键组件:隐藏列(DB_TRX_IDDB_ROLL_PTR)构建版本链,Undo Log 存储历史版本,Read View 定义可见性规则。
  • 目的:实现读写不阻塞,提高并发性能,并作为实现数据库事务隔离级别的基础。
  • 应用:广泛应用于现代主流数据库,如 MySQL InnoDBPostgreSQLOracle 等。