MVCC及其原理

时间:2024-03-05 16:57:56

1. MVCC概述及其原理

多版本并发控制(MVCC,Multi-Version Concurrency Control)是一种数据库管理技术,用于提高数据库系统在多用户环境中的并发性能,同时保证事务的隔离性,避免了不必要的锁定。MVCC允许在不同的事务中读取数据的早期版本,从而使读操作不会阻塞写操作,反之亦然。这种机制在很多数据库系统中都有实现,包括PostgreSQL和MySQL的InnoDB存储引擎。

在这里插入图片描述

MVCC原理

MVCC的核心思想是为数据库中的每一行数据保持不同版本的记录。这意味着当用户对数据库进行写操作(如更新或删除)时,系统会创建一行数据的新版本,而不是直接在原有数据上修改。每个版本的数据都有一个时间戳(或其他形式的版本标识),标识它被创建或修改的时间点。

当一个事务请求读取数据时,MVCC系统会返回该事务开始时刻可见的数据版本。具体来说,系统会根据以下规则来确定哪个版本的数据对当前事务是“可见”的:

  1. 创建版本号:每个数据版本在创建时都会被赋予一个唯一的版本号,这通常是事务的ID。这个版本号标识了数据被创建或修改的逻辑时间点。

  2. 删除版本号:当数据被另一个事务更新或删除时,原有版本的数据会被赋予一个删除版本号,也是事务的ID。这个版本号标识了数据停止被可见的逻辑时间点。

  3. 版本可见性规则:给定一个事务,如果某个数据版本的创建版本号小于或等于该事务的ID,且该数据版本没有被删除(即没有删除版本号)或其删除版本号大于该事务的ID,那么这个数据版本对该事务是可见的。

在这里插入图片描述

MVCC的优势

  • 提高并发性:读操作不会阻塞写操作,写操作也不会阻塞读操作,这大大提高了数据库的并发性能。
  • 减少锁的需求:由于读操作可以访问数据的早期版本,因此减少了对读写锁的需求,进一步提高并发性。
  • 事务隔离级别的支持:MVCC可以非常灵活地支持不同的事务隔离级别,包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

MVCC的实现

不同的数据库系统实现MVCC的具体机制可能有所不同,但基本原理相似。以MySQL的InnoDB存储引擎为例,它使用以下机制来实现MVCC:

  • Undo日志:当数据被修改时,原始数据会被存储在Undo日志中。这允许系统在需要时构造出数据的早期版本。
  • Read View:当事务开始时,InnoDB会为该事务创建一个“读视图”,决定哪些版本的数据对该事务可见。
  • 隐藏的系统列:InnoDB在每行数据中存储两个隐藏的系统列,记录了行的创建版本和删除版本,用于支持MVCC。

通过这种机制,MVCC能够在保证事务隔离性的同时,提高数据库的并发访问性能。

2. MySQL中的MVCC

在MySQL中,多版本并发控制(MVCC)主要通过InnoDB存储引擎实现。InnoDB使用一系列内部机制来支持MVCC,允许数据库在保持高并发的同时,确保数据的一致性和事务的隔离级别。这里是InnoDB实现MVCC的几个关键组成部分:

1. 隐藏列

InnoDB对每一行数据添加了三个隐藏的列来支持MVCC:

  • DB_TRX_ID:每当一行数据被修改时,InnoDB都会在这个隐藏列中存储修改该行的事务ID。
  • DB_ROLL_PTR:这个指针指向undo log记录,如果这行数据被多次修改,这些undo log记录形成一个链表。通过这个链表,InnoDB可以找到某个特定版本的行数据。
  • DB_ROW_ID:如果表没有定义主键,InnoDB会使用这个隐藏的行ID作为主键。

2. Undo日志

当事务更新数据时,InnoDB会将原始数据的副本存储在undo log中。这允许InnoDB在需要时回滚事务或重建旧的数据版本。对于MVCC来说,undo log使得读取事务能够看到事务开始之前的数据状态,即使这些数据后来被其他事务修改了。

3. Read View

当事务读取数据时,InnoDB会为该事务创建一个read view,这个视图定义了事务可以“看到”哪些行的版本。read view基于以下几个列表来判断数据行的可见性:

  • 活跃事务列表:在read view创建时,系统中所有活跃事务的ID列表。任何具有更高事务ID的数据修改都对当前事务不可见。
  • 上一个事务ID:创建read view时的最大事务ID。这帮助确定哪些数据版本是由尚未提交的事务创建的,因此对当前事务不可见。

4. 数据行的多个版本

通过以上机制,InnoDB能够为每个事务维护数据行的多个版本。当事务需要读取数据时,InnoDB会使用read view来决定哪个版本的数据对该事务是可见的。这取决于数据版本的创建事务ID与read view中活跃事务列表的比较。

如何工作

  • 当事务A更新一行数据时,InnoDB会将该行的当前版本写入undo log,并更新该行的DB_TRX_ID。
  • 如果此时事务B要读取相同的数据,InnoDB会检查事务B的read view。如果事务A的ID不在事务B的活跃事务列表中,这意味着事务A在事务B开始之前就已经提交,因此事务B可以看到事务A对该行所做的更改。
  • 如果事务A还未提交,事务B将看不到这些更改。InnoDB会使用undo log来为事务B提供该行数据的一个早期版本。

通过这种方式,InnoDB的MVCC机制支持了读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)等不同的事务隔离级别,并且允许并发事务高效安全地访问数据库,而无需对读操作加锁。

3. 例子

让我们通过一个简单的例子来说明InnoDB是如何实现MVCC的。假设我们有一个简单的银行账户表accounts,其中包含两列:account_id(账户ID)和balance(余额)。现在,我们有两个事务同时操作这个表:事务A(Tx A)要更新一个账户的余额,而事务B(Tx B)想要读取一些账户的余额信息。

初始状态

  • accounts表中有一条记录:account_id = 1, balance = 100

事务A的操作

  1. 开始事务A:事务A开始,并打算将account_id = 1的账户余额更新为200。
  2. 更新操作:InnoDB在更新余额之前,会将这行数据当前的版本(balance = 100)存储到undo log中,并将这行数据的DB_TRX_ID更新为事务A的事务ID。
  3. 余额更新account_id = 1的账户余额现在变为200。

事务B的操作

  1. 开始事务B:事务B开始,想要读取所有账户的余额信息。
  2. 构建Read View:为事务B创建一个read view,这个read view包含了事务开始时系统中所有未完成的事务ID。由于事务A尚未提交,它的ID也在这个列表中。

读取操作

  1. 事务B读取account_id = 1的余额:当事务B尝试读取这个账户的余额时,InnoDB检查这行数据的DB_TRX_ID
  2. 使用Read View判断数据版本的可见性:由于事务A的ID在事务B的read view活跃事务列表中,这意味着事务B不能看到事务A所做的更改。
  3. 通过Undo Log访问旧数据:InnoDB使用undo log中的信息,提供给事务B这行数据的旧版本(balance = 100),即使当前表中的数据已经被更新为200。

事务提交

  1. 事务A提交:事务A完成更新操作后提交。此时,InnoDB会更新这行数据的DB_TRX_ID,表示这个版本的数据是由事务A创建的。
  2. 事务B读取操作之后:即使事务A已经提交,由于事务B的read view是在事务B开始时创建的,事务B仍然只能看到旧版本的数据。

结论

通过这个例子,我们可以看到InnoDB的MVCC是如何工作的:

  • Undo Log:保留数据的旧版本,使得即使数据被更新,旧的事务也可以看到更新前的数据。
  • Read View:定义了一个事务可以看到哪些数据版本,确保事务的隔离性。
  • DB_TRX_ID:标识了数据版本是由哪个事务创建的,用于决定数据的可见性。

这种机制允许事务B在事务A更新数据的同时读取数据,而不会看到未提交的更改,从而实现了非锁定读取和高并发性。