[转] MySQL InnoDB MVCC 实现原理详解
本文转载自掘金技术社区,原文作者:小假是真的 原文链接:https://juejin.cn/post/7565516607302074419
# 前言
MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。它是一种为了提高数据库并发性能而提出的技术,使得在并发读写数据库时,读操作不会阻塞写操作,写操作也不会阻塞读操作,解决了传统锁机制带来的性能瓶颈问题。
在 MySQL 中,InnoDB 存储引擎实现了 MVCC。
# 一、MVCC 要解决的核心问题
在没有 MVCC 的情况下,如果要保证事务的隔离性(例如可重复读级别),通常会用锁来实现:
- 当一个事务正在读取某些数据时,其他事务不能修改这些数据(共享锁),会导致读 - 写冲突;
- 当一个事务在修改数据时(排他锁),其他事务不能读取,会导致写 - 读冲突。
MVCC 通过创建数据的历史版本来优雅地解决这个问题。
核心思想:为每行数据维护多个历史版本。当一个事务需要读取数据时,它会看到在它开始之前就已经提交的某个一致性数据快照,而不管当前这些数据被其他事务修改成了什么样子。
# 二、MVCC 的实现基石
MVCC 的实现依赖于三个核心组件:
- 隐藏字段
- Undo Log(回滚日志)
- Read View(读视图)
# 1. 隐藏字段
InnoDB 为每一行数据(记录)都添加了三个系统隐藏字段,此外还有一个删除标记字段,用于标记该行是否被删除。
表格
| 隐藏字段 | 字节数 | 作用说明 |
|---|---|---|
DB_TRX_ID | 6 字节 | 事务 ID。表示最后一次插入或更新该行的事务 ID;删除操作在内部也被视为更新,会设置特殊的删除标记。 |
DB_ROLL_PTR | 7 字节 | 回滚指针。指向该行数据的上一个历史版本,存储在 Undo Log 中。 |
DB_ROW_ID | 6 字节 | 行 ID。随着新行插入而单调递增的行 ID;如果表没有定义主键,InnoDB 会基于这个字段生成聚簇索引。 |
# 2. Undo Log
Undo Log(回滚日志)主要有两个作用:
- 事务回滚时,用于恢复数据;
- 实现 MVCC 的核心载体,存储数据行的历史版本。
# 工作原理
当一个事务对某行数据进行修改(INSERT、UPDATE、DELETE)时:
- UPDATE / DELETE:会先将该行数据的当前版本(修改前)复制到 Undo Log 中,副本保留原有的
DB_TRX_ID和DB_ROLL_PTR;随后将新的DB_ROLL_PTR指向这个刚存入 Undo Log 的旧版本,最后在表中修改该行数据,写入新的DB_TRX_ID和新的DB_ROLL_PTR。 - INSERT:新插入的数据对之前的事务不可见,它的 Undo Log 仅在事务回滚时生效,在 MVCC 中作用不大。
通过 DB_ROLL_PTR 指针,一行数据的所有历史版本(快照)被串联成一个链表,这个链表就存放在 Undo Log 中,称为版本链。链表的头节点是当前的最新记录,尾节点是最早的历史版本。
# 3. Read View(读视图)
Read View 是事务在进行快照读操作时产生的,它定义了当前事务在执行期间,能看到哪些版本的数据。
Read View 主要包含以下几个关键属性:
m_ids:生成 Read View 时,系统中活跃的(未提交的)读写事务的事务 ID 列表。min_trx_id:m_ids中的最小值。max_trx_id:生成 Read View 时,系统应该分配给下一个事务的 ID(注意:不是m_ids的最大值,而是已创建的最大事务 ID + 1)。creator_trx_id:创建该 Read View 的事务 ID。
# 三、可见性算法:如何判断一个版本是否可见
当一个事务执行一条 SELECT 语句(快照读)时,它需要遍历数据行的版本链,并利用自己的 Read View,通过一套算法来决定哪个版本对它来说是可见的。
对于版本链中的某个版本,假设其对应的事务 ID 为 trx_id,判断规则如下:
如果
trx_id == creator_trx_id:说明当前事务自己修改了这行数据,这个版本是可见的。如果
trx_id < min_trx_id:说明这个版本是在当前 Read View 创建之前就已经提交的,这个版本是可见的。如果
trx_id >= max_trx_id:说明这个版本是在当前 Read View 创建之后才开启的事务修改的,这个版本不可见。如果
min_trx_id <= trx_id < max_trx_id:需要检查
trx_id1是否在
m_ids1(活跃事务列表)中:
- 如果在,说明创建 Read View 时,修改这个版本的事务还未提交,该版本不可见;
- 如果不在,说明创建 Read View 时,修改这个版本的事务已经提交了,该版本可见。
如果当前版本不可见,就顺着版本链找下一个旧版本,重复上述判断,直到找到可见的版本;如果遍历完所有版本都不可见,则返回空。
# 四、在不同隔离级别下的表现
MVCC 主要在 READ COMMITTED(读已提交) 和 REPEATABLE READ(可重复读) 这两个隔离级别下工作,二者的核心差异在于 Read View 的生成时机不同。
# 1. REPEATABLE READ(可重复读 - MySQL 默认级别)
- 核心特性:在同一个事务中,第一次执行快照读时会创建一个 Read View,之后在这个事务中的所有快照读都复用这个相同的 Read View。
- 效果:因为 Read View 是静态的,所以无论之后其他事务如何提交修改,这个事务看到的数据快照始终和它第一次看到的一样,完美实现了 “可重复读”。
# 2. READ COMMITTED(读已提交)
- 核心特性:在同一个事务中,每次执行快照读都会生成一个新的、独立的 Read View。
- 效果:因为每次读都会重新获取一次当前系统的活跃事务列表,所以每次都能看到在本次查询开始之前已经提交的所有事务的修改,实现了 “读已提交”,即能读到其他事务最新提交的内容。
# 五、工作流程总结
MVCC 的完整工作流程可以总结为:
- 每个数据行都带有隐藏的
DB_TRX_ID和DB_ROLL_PTR字段; - 数据的修改操作会在 Undo Log 中创建历史版本,形成版本链;
- 事务执行快照读时生成 Read View(RC 级别每次生成,RR 级别仅第一次生成);
- 通过可见性算法遍历版本链,找到对当前事务可见的那个数据版本。
# 六、补充说明
# 快照读 vs 当前读
- 快照读:普通的
SELECT语句,基于 MVCC 和 Read View 读取历史版本,不加锁。 - 当前读:特殊的
SELECT语句(如SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE)以及INSERT、UPDATE、DELETE操作。当前读读取的是记录的最新版本,并且会通过加锁(Next-Key Lock)来保证数据一致性。
# Purge 操作
随着时间推移,Undo Log 中旧版本数据会越来越多。系统会有一个后台的 Purge 线程,来清理那些不再被任何事务的 Read View 需要的旧版本数据,从而释放存储空间。