多版本并发控制(MVCC)机制原理详解

摘要

多版本并发控制(MVCC)是现代数据库系统中一项至关重要的并发控制技术,旨在提高数据库的并发性能,同时确保数据的一致性和隔离性。本文将深入探讨 MVCC 的核心概念、关键实现机制(以 MySQL InnoDB 为例)、可见性判断规则,并简要介绍其在不同数据库系统中的应用,以期提供对 MVCC 机制的全面理解。

1. 引言

在多用户并发访问数据库的场景中,如何有效地管理并发操作,避免数据冲突,同时保证数据的一致性和隔离性,是数据库系统面临的核心挑战。传统的锁机制虽然能够保证数据的一致性,但在高并发环境下,锁竞争可能导致性能瓶颈。多版本并发控制(MVCC)技术应运而生,它通过维护数据的多个版本,允许读操作和写操作在不相互阻塞的情况下并发执行,从而显著提升了数据库的并发性能。

2. MVCC 的核心概念

MVCC 的核心思想是为每个事务提供一个数据的“快照视图”(Snapshot),使得事务在读取数据时,能够看到一个在事务开始时(或语句开始时,取决于隔离级别)的数据一致性状态,而不会被其他并发事务所做的修改所影响。当一个事务修改数据时,数据库不会直接覆盖旧数据,而是创建一个新的数据版本,并保留旧版本,从而允许多个版本的数据同时存在。

MVCC 的主要优势包括:

  • 读写不阻塞:读操作无需等待写操作释放锁,写操作也无需等待读操作完成,极大地减少了锁竞争,提升了并发度。
  • 提升并发性能:在高并发读写场景下,MVCC 能够有效避免锁竞争带来的性能下降。
  • 简化应用开发:开发者无需手动处理复杂的锁机制,降低了并发编程的难度。
  • 实现事务隔离:MVCC 是实现数据库事务隔离级别(如“读已提交”Read Committed 和“可重复读”Repeatable Read)的关键技术。

3. MVCC 的关键实现机制

MVCC 的实现依赖于数据库存储引擎提供的特定数据结构和机制。以下将以 MySQL 的 InnoDB 存储引擎为例,详细阐述其关键实现机制:

3.1 数据行的隐藏字段

在 InnoDB 中,每一行数据除了用户可见的列之外,还包含一些隐藏的系统字段,这些字段对于 MVCC 的实现至关重要:

  • trx_id:记录了最后一次修改该行数据的事务的 ID。每次事务对数据进行修改时,都会将自己的事务 ID 记录到该字段中。
  • roll_pointer:一个指向 undo log 中该行数据上一个版本的指针。通过这个指针,可以追溯到该行数据的历史版本,形成一个版本链(Version Chain)。
  • DB_ROW_ID:一个隐藏的行 ID,当表没有显式定义主键时,InnoDB 会自动生成一个 DB_ROW_ID 作为聚簇索引。这个 ID 在 MVCC 中也可能被用于唯一标识行。

3.2 Undo Log(回滚日志)

undo log 是 InnoDB 存储引擎中非常重要的一部分,它主要有两个作用:

  1. 事务回滚:当事务需要回滚时,可以通过 undo log 中的记录将数据恢复到修改之前的状态。
  2. MVCC 实现undo log 存储了数据修改前的旧版本。当一个事务修改一行数据时,会先将该行数据的旧版本写入 undo log,然后更新当前行数据,并将当前行的 roll_pointer 指向 undo log 中的旧版本。这样,所有历史版本的数据都通过 roll_pointer 链接起来,形成一个版本链。读事务可以通过这个版本链找到它需要读取的可见版本。

3.3 ReadView(一致性视图)

ReadView 是 MVCC 实现可见性判断的核心。当一个事务启动时(在“读已提交”隔离级别下是每个语句开始时,在“可重复读”隔离级别下是事务开始时),会生成一个 ReadViewReadView 记录了当前系统中所有活跃(即未提交)的事务 ID 列表,以及一些辅助信息,用于判断当前事务能够看到哪个版本的数据。

ReadView 主要包含以下信息:

  • m_ids:一个列表,记录了在生成 ReadView 时,当前系统中所有活跃(未提交)的事务 ID。
  • min_trx_idm_ids 列表中最小的事务 ID。如果 m_ids 为空,则 min_trx_idmax_trx_id
  • max_trx_id:在生成 ReadView 时,系统下一个将要分配的事务 ID。所有大于等于 max_trx_id 的事务 ID 都是在 ReadView 生成之后才启动的。
  • creator_trx_id:创建该 ReadView 的事务本身的 ID。这个字段用于判断当前事务是否能看到自己修改的数据。

4. 可见性判断规则

当一个事务尝试读取一行数据时,数据库会根据当前事务的 ReadView 和数据行的 trx_id,遵循以下规则来判断该行数据的哪个版本是可见的:

  1. 情况一:trx_idcreator_trx_id 相同
    如果被访问版本的 trx_id 等于 ReadViewcreator_trx_id,这意味着该版本是由当前事务自己修改的。在这种情况下,该版本对当前事务是可见的。

  2. 情况二:trx_id 小于 min_trx_id
    如果被访问版本的 trx_id 小于 ReadViewmin_trx_id,这表明修改该行的事务在当前事务创建 ReadView 之前就已经提交了。因此,该版本对当前事务是可见的。

  3. 情况三:trx_id 大于或等于 max_trx_id
    如果被访问版本的 trx_id 大于或等于 ReadViewmax_trx_id,这说明修改该行的事务是在当前事务创建 ReadView 之后才启动的。因此,该版本对当前事务是不可见的。此时,需要沿着 roll_pointer 链查找该行的上一个版本,并重复上述判断过程。

  4. 情况四:trx_idmin_trx_idmax_trx_id 之间
    如果被访问版本的 trx_id 介于 min_trx_idmax_trx_id 之间,则需要进一步判断 trx_id 是否存在于 ReadViewm_ids 列表中:

    • 如果 trx_idm_ids 列表中:这表示修改该行的事务在当前事务创建 ReadView 时仍然处于活跃状态(未提交)。因此,该版本对当前事务是不可见的。此时,需要沿着 roll_pointer 链查找该行的上一个版本,并重复上述判断过程。
    • 如果 trx_id 不在 m_ids 列表中:这说明修改该行的事务在当前事务创建 ReadView 时已经提交。因此,该版本对当前事务是可见的。

通过上述规则,每个事务都能够看到一个与其隔离级别要求相符的数据快照,从而实现了读写并发和事务隔离。

5. MVCC 在不同数据库中的应用

MVCC 机制在主流的关系型数据库和分布式数据库中得到了广泛应用,但具体实现细节可能有所不同:

  • MySQL (InnoDB):如前所述,InnoDB 是 MVCC 的典型实现者,其依赖 trx_idroll_pointerundo logReadView 来实现多版本并发控制。
  • PostgreSQL:PostgreSQL 也采用了 MVCC 机制。它使用 xminxmax 两个字段来标记每个元组(Tuple)的创建事务 ID 和删除事务 ID,并有专门的 VACUUM 进程来清理不再需要的旧版本数据,以回收存储空间。
  • TiDB:作为一款分布式关系型数据库,TiDB 在其存储引擎 TiKV 中实现了 MVCC。TiDB 使用全局统一的时间戳(Timestamp Oracle, TSO)作为版本号,确保分布式事务的全局一致性。TiDB 的 MVCC 同样需要进行垃圾回收(GC)来清理过期的版本数据,以避免存储空间的无限增长。
  • CockroachDB:与 TiDB 类似,CockroachDB 也是一个分布式数据库,它高度依赖 MVCC 来处理并发请求并保证强一致性。CockroachDB 将 MVCC 时间戳编码到每个键中,从而支持多版本数据的存储和访问。

6. 总结

MVCC 机制是现代数据库系统实现高并发和强一致性的基石。通过维护数据的多个版本,并结合 undo logReadView 等机制,MVCC 使得读写操作能够并行不悖,极大地提升了数据库的整体性能和用户体验。尽管不同数据库在实现细节上有所差异,但其核心思想都是为了在并发环境下提供一致性的数据视图,从而满足复杂业务场景的需求。

1 个赞

学习数据库感觉MVCC 机制是一个难点,分享的很详细。

学习了

mvcc在事物层下面还是上面

根据事物隔离级别,读到不同版本,脏读,一致读,幻读,可重复读

快照技术、undo和redo技术以及一致性机制构建了MVCC,现在只要是事务型数据库都离不开这个MVCC.

mysql里面的mvcc和事务是通过一些共同的机制去完成的,不能简单的理解成分层结构。

楼主,这个mysql的mvcc描述的很详尽,tidb的mvcc机制也说说,期待

很多分布式数据库都在使用MVCC,只有oracle通过undo实现

这是大佬

是的,mvcc是最基础的,都得支持才行

我以前写过一篇关于“脏读,一致读,幻读,可重复读”的文章,奈何表妹觉得没价值- -||