深入解析:数据库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 就是借助给数据行“拍快照”现代数据库完成高并发的重要技术。就是的方式,让每个事务都能看到一份一致的数据视图,从而巧妙地避免了不必要的锁竞争,
浙公网安备 33010602011771号