数据库MVCC
MVCC
一、MVCC 是什么?
MVCC 的全称是 Multi-Version Concurrency Control,即多版本并发控制。
它是一种数据库管理技术,用于提高数据库在高并发场景下的性能。MVCC 通过在同一时刻保留数据行的多个版本,使得读操作(SELECT
)不会阻塞写操作(UPDATE
, DELETE
),写操作也不会阻塞读操作。它完美解决了读写之间不必要的阻塞问题,极大地提升了并发能力。
MySQL 中 InnoDB 存储引擎的核心特性之一就是实现了 MVCC。
二、为什么需要 MVCC?—— 解决读写冲突
在没有 MVCC 的情况下,数据库通常通过锁来实现事务的隔离性。
- 如果使用写锁(排他锁,X Lock),一个事务在写数据时,会阻塞其他事务的读和写。
- 如果使用读锁(共享锁,S Lock),一个事务在读数据时,会阻塞其他事务的写,但允许其他事务读。
这种“锁”的方式虽然能保证数据的一致性,但并发性能很差,因为读和写是互斥的。
MVCC 提供了一种非锁的读(Consistent Nonlocking Read)方式。它让读操作去读一个快照版本,而写操作去创建一个新的版本。这样读和写操作的对象是不同的数据版本,因此可以并发执行,互不阻塞。
三、MVCC 的核心工作原理
MVCC 的实现依赖于三个核心概念和一个关键机制:
- 三个隐藏字段
- Undo Log(回滚日志)
- Read View(读视图)
1. 三个隐藏字段
InnoDB 为每一行数据都额外添加了三个用户看不到的隐藏字段:
DB_TRX_ID
(6字节):最后修改该数据行的事务ID。记录是哪个事务插入或修改了这行数据。DB_ROLL_PTR
(7字节):回滚指针。指向该行数据在Undo Log
中的上一个历史版本。它将所有版本的数据串联成一个版本链。DB_ROW_ID
(6字节):行标识(隐藏主键)。如果表没有定义主键,InnoDB 会自动生成这个字段作为聚簇索引的键。
2. Undo Log (回滚日志)
Undo Log
保存了数据被修改前的旧版本数据。当执行 UPDATE
或 DELETE
操作时,旧版本的数据并不会被立刻删除或覆盖,而是会被拷贝到 Undo Log
中。
- 每修改一次,就会在
Undo Log
中生成一条记录。 - 通过
DB_ROLL_PTR
回滚指针,可以将当前记录的所有历史版本串联起来,形成一个版本链。链头是最新的记录,链尾是最老的记录。
3. Read View (读视图) - MVCC 的“快照”灵魂
Read View
是事务在进行快照读(普通SELECT)时产生的,它定义了当前事务能看到哪个版本的数据。
Read View
本质上是一个数据结构,主要包含以下关键信息:
m_ids
:生成Read View
时,系统中活跃的(未提交的)读写事务ID的集合。min_trx_id
:m_ids
集合中的最小值。max_trx_id
:生成Read View
时,系统应该分配给下一个事务的ID。creator_trx_id
:创建这个Read View
的当前事务的ID。
数据可见性规则:
当访问某一行数据时,MVCC 会从最新的版本开始,顺着版本链依次判断每个版本是否对当前事务可见。判断规则如下:
- 如果被访问版本的
DB_TRX_ID
小于min_trx_id
,说明该版本在Read View
创建前就已提交,对当前事务可见。 - 如果被访问版本的
DB_TRX_ID
大于等于max_trx_id
,说明该版本在Read View
创建后才生成,对当前事务不可见。需要顺着版本链继续找更老的版本。 - 如果被访问版本的
DB_TRX_ID
在min_trx_id
和max_trx_id
之间(min_trx_id <= trx_id < max_trx_id
):- 若
DB_TRX_ID
在m_ids
(活跃事务集合)中,说明创建该版本的事务当时还未提交,该版本不可见。 - 若
DB_TRX_ID
不在m_ids
中,说明创建该版本的事务当时已经提交,该版本可见。
- 若
- 如果当前记录版本的
DB_TRX_ID
等于creator_trx_id
,说明是这个事务自己修改的记录,对自己总是可见的。
一旦找到第一个对当前事务可见的版本,就返回这个版本的数据。
简单解释: Read View 就是 InnoDB 给每个快照读开的“时间戳发票”。
事务拿到这张发票后,整个事务期间都按这张发票上的规则,判断到底能看哪个“历史版本”的行记录。
发票一旦打印,内容就不会再变,所以 repeatable read 才能做到“可重复”。
1.发票长什么样(4 个字段)
字段名 | 含义 | 类比 |
---|---|---|
m_ids | 开票瞬间,还没提交的所有事务编号 | 黑名单 |
min_trx_id | 黑名单里最小的那个编号 | 最早“坏人” |
max_trx_id | 系统下一个要分配的事务编号 | “未来人”起点 |
creator_trx_id | 开票人自己的事务编号 | 我自己 |
- 拿到发票后怎么“验货”
对每条记录的每个版本(顺着 undo 链从头走到尾):
- 版本太老(
DB_TRX_ID < min_trx_id
)
→ 坏人名单里都没它,说明早就提交了,可见。 - 版本太新(
DB_TRX_ID ≥ max_trx_id
)
→ 这是我开票之后才冒出来的,不可见,继续往 older 版本找。 - 版本在中间(
min_trx_id ≤ DB_TRX_ID < max_trx_id
)- 如果编号在黑名单
m_ids
里 → 开票时它还没提交,不可见。 - 如果编号不在黑名单 → 开票时它已提交,可见。
- 如果编号在黑名单
- 版本是我自己改的(
DB_TRX_ID == creator_trx_id
)
→ 自己写的东西当然能看到,可见。
一旦找到第一个“可见”版本就停下来返回,后面的 older 版本不再看。
- 一张图秒懂
时间轴: ...[min_trx_id) ...[m_ids 黑名单]... [max_trx_id)...↑ ↑ ↑太老,可见 在黑名单→不可见 太新,不可见不在黑名单→可见
- 举个数字例子
- 当前系统里活着的事务:88,90,93
- 于是 Read View:
m_ids = {88,90,93}
min_trx_id = 88
max_trx_id = 95(系统下一个号)
creator_trx_id = 91(我自己)
来一条记录版本链:
版本号(DB_TRX_ID) | 判断 | 可见? |
---|---|---|
96 | ≥ max_trx_id | 否 |
94 | 88≤94<95 且 94∉m_ids | 可见 (返回) |
- 一句话总结
Read View 把“并发世界”瞬间拍成一张静态照片,之后事务无论读多少次,都只看这张照片允许的“历史镜像”,从而
- 挡住未提交的脏数据(脏读)
- 挡住已提交的后续改动(不可重复读)
- 配合间隙锁还能挡住新插入的幻影行(幻读)
这就是 MVCC 里“快照”真正的灵魂。
四、MVCC 如何实现不同隔离级别?
MVCC 主要作用于 READ COMMITTED (RC,提交读) 和 REPEATABLE READ (RR,可重复读) 这两个隔离级别。
- READ COMMITTED (RC):
- 核心:每次执行快照读(SELECT)时,都会生成一个新的 Read View。
- 效果:每次读都能看到最新已经提交的事务所做的修改。所以会出现“不可重复读”现象(同一个事务内两次读取同一数据,结果可能不同)。
- REPEATABLE READ (RR):
- 核心:只在第一次执行快照读时生成一个 Read View,后续所有的读操作都复用这个 Read View。
- 效果:在整个事务期间,每次读到的数据都是一致的,就像是在事务开始时拍了一个快照一样。因此解决了“不可重复读”问题。(这也是 InnoDB 在 RR 级别下能防止幻读的手段之一)。
五、一个简单的例子
假设:
- 事务A (id=10) 开启,查询一条记录。
- 事务B (id=20) 修改了这条记录并提交。
- 事务A 再次查询。
在 RC 级别下:
- 事务A第一次查询,生成
Read View1
,m_ids
包含 [10](假设只有自己活跃)。它读到的是原始版本。 - 事务B修改并提交。
- 事务A第二次查询,生成一个新的
Read View2
,此时m_ids
只包含 [10](20已提交)。根据规则,它能看到事务B提交的版本。所以两次查询结果不同(不可重复读)。
在 RR 级别下:
- 事务A第一次查询,生成
Read View1
,m_ids
包含 [10]。 - 事务B修改并提交。
- 事务A第二次查询,复用之前的
Read View1
。根据规则,Read View1
生成时,事务B (20) 还未开始或处于活跃状态(取决于时机),所以事务B修改的版本对Read View1
不可见。事务A只能看到和第一次一样的原始版本。所以两次查询结果相同(可重复读)。
六、总结
特性 | 说明 |
---|---|
目的 | 提高并发性能,实现读写不阻塞。 |
实现基础 | 隐藏字段 (DB_TRX_ID , DB_ROLL_PTR ) + Undo Log (版本链) + Read View (可见性判断)。 |
核心思想 | 为每个事务提供一个数据快照,读操作读历史版本,写操作创建新版本。 |
适用操作 | 快照读(普通 SELECT ... 不加锁)。当前读(SELECT ... FOR UPDATE , UPDATE , DELETE , INSERT 会加锁)不适用。 |
与隔离级别关系 | 是 RC 和 RR 隔离级别实现的基础。RC 每次读生成新 Read View ;RR 第一次读生成 Read View 并复用。 |
优点 | 读不加锁,读写不冲突,并发性能高。 |
缺点 | 需要维护多版本数据,会占用更多的存储空间;需要复杂的垃圾回收机制来清理不再需要的旧版本数据。 |
简单来说,MVCC 就是通过给数据行“拍快照” 的方式,让每个事务都能看到一份一致的数据视图,从而巧妙地避免了不必要的锁竞争,是现代数据库实现高并发的重要技术。