MVCC(Multi-Version Concurrency Control,多版本并发控制)。是一个在数据库管理系统中用于处理并发控制的核心技术。理解它对于深入掌握数据库(尤其是 InnoDB、PostgreSQL 等)的工作原理至关重要。
1. 什么是 MVCC?
MVCC 的全称是 多版本并发控制。
核心思想:在数据库中,同一份数据可以保留多个历史版本。当事务需要读取数据时,它会根据一定的规则(比如事务的开始时间)看到一个特定的、一致性的“快照”(Snapshot),而不是直接读取最新的、可能还未提交的数据。写操作则会创建一个新版本的数据。
你可以把它想象成一个高效的版本控制系统(如 Git):
- 读操作:就像
git checkout
到某个特定的 commit 版本,你看到的是那个时间点的完整项目状态,即使之后有新的提交,你的视图也不会变。 - 写操作:就像创建一个新的
commit
,它不会覆盖旧的commit
,而是在旧版本的基础上生成一个新版本。
通过这种方式,读操作和写操作可以不再互相阻塞,从而极大地提高了数据库的并发性能。
2. 为什么需要 MVCC?
在传统的数据库并发控制中,主要使用两种机制:
锁机制:
- 读-写冲突:当一个事务在读取一行数据时,会给它加上共享锁(S锁)。另一个事务如果想修改这行数据,需要加排他锁(X锁),但 S 锁和 X 锁互斥,所以写事务必须等待读事务完成。反之亦然,写事务会阻塞读事务。
- 问题:并发度低。读和写操作串行化,性能很差。
基于时间戳的排序:
- 所有操作按时间戳排序执行,如果操作冲突,则回滚其中一个事务。
- 问题:事务冲突率高,回滚频繁,性能同样不理想。
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 依次修改。
- 初始状态:事务 10 插入一行数据。
DB_TRX_ID = 10
DB_ROLL_PTR = null
(因为是第一个版本)
- 事务 20 修改:事务 20 更新了这行数据。
- InnoDB 不会直接覆盖旧数据,而是:
- 将旧版本的数据(
DB_TRX_ID=10
的版本)复制到 Undo Log 中。 - 在原位置创建一个新版本的数据行。
- 更新新版本的字段:
DB_TRX_ID = 20
。 - 更新新版本的
DB_ROLL_PTR
,让它指向 Undo Log 中旧版本的位置。
- 将旧版本的数据(
- 现在,通过新版本的
DB_ROLL_PTR
,我们可以找到旧版本,形成一条版本链
:最新版本(20) -> 旧版本(10)
。
- InnoDB 不会直接覆盖旧数据,而是:
b. Undo Log
Undo Log 主要有两个作用:
- 事务回滚:当一个事务需要回滚时,可以利用 Undo Log 中记录的旧版本数据,将数据恢复到修改之前的状态。
- 构建版本链:如上所述,它存储了数据行的历史版本,是 MVCC 实现多版本的关键。当需要读取某个历史版本时,就可以从这里获取。
c. Read View(读视图)
Read View 是事务在执行快照读(普通的 SELECT
)时,动态生成的一个“可见性判断”标准。它决定了当前事务能看到版本链上的哪个版本。
Read View 主要包含以下几个重要属性:
creator_trx_id
: 创建该 Read View 的事务的 ID。trx_ids
: 创建 Read View 时,当前系统中所有活跃的(未提交的)读写事务的 ID 列表。up_limit_id
:trx_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。然后,它会从版本链的最新版本开始,逐个版本地应用以下规则,直到找到一个可见的版本:
检查
DB_TRX_ID
是否是自己创建的?- 如果
DB_TRX_ID == creator_trx_id
,说明这行数据是本事务自己修改的,可见。
- 如果
检查
DB_TRX_ID
是否小于up_limit_id
?- 如果
DB_TRX_ID < up_limit_id
,说明修改这个版本的事务在当前事务开始前就已经提交了,可见。
- 如果
检查
DB_TRX_ID
是否大于或等于low_limit_id
?- 如果
DB_TRX_ID >= low_limit_id
,说明修改这个版本的事务是在当前事务开始之后才启动的,不可见。需要根据DB_ROLL_PTR
去 Undo Log 中查找上一个版本,然后重复整个判断流程。
- 如果
检查
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 的优缺点
优点
- 高并发性:读写操作不阻塞,极大地提高了数据库的并发读写性能。
- 非锁定读:避免了读操作加锁带来的开销和死锁风险。
- 实现一致性读:为不同隔离级别提供了基础,保证了事务的隔离性。
缺点
- 存储空间开销:需要维护多个版本的数据,Undo Log 会占用额外的存储空间。对于长事务或更新频繁的表,Undo Log 可能会变得非常大。
- 管理开销:需要额外的逻辑来管理版本链、创建和判断 Read View,增加了数据库的复杂性。
- 行版本清理:需要后台线程(如 InnoDB 的 Purge 线程)定期清理已经不再需要的旧版本数据(即没有事务再需要访问它们),否则 Undo Log 会无限增长。这个清理过程本身也消耗资源。
- 并非万能:MVCC 主要解决的是
SELECT
的并发问题。对于UPDATE
、DELETE
之间的冲突,仍然需要使用锁(比如行锁、间隙锁、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_ID
,DB_ROLL_PTR
)构建版本链,Undo Log 存储历史版本,Read View 定义可见性规则。 - 目的:实现读写不阻塞,提高并发性能,并作为实现数据库事务隔离级别的基础。
- 应用:广泛应用于现代主流数据库,如 MySQL InnoDB、PostgreSQL、Oracle 等。