小白也能懂的MVCC详解

MVCC 的全称是 Multi-Version Concurrency Control,即多版本并发控制。它是数据库中用于解决并发读写冲突的核心技术,目的是让 读写操作互不阻塞,提升数据库的并发性能,同时还能保证事务的隔离性。

一、为什么需要MVCC?先看并发下的“坑”

如果没有MVCC,数据库只能靠来处理并发,会出现很多问题:

  • 读锁阻塞写:一个事务读数据时,会加读锁,其他事务想修改这条数据就会被阻塞。
  • 写锁阻塞读:一个事务修改数据时,会加写锁,其他事务想读这条数据也会被阻塞。
  • 隔离性问题:并发操作时,容易出现脏读、不可重复读、幻读(这三个是事务隔离性的三大问题)。

而MVCC的出现,就是为了在不加锁的情况下,让读写操作并行执行,同时避免上述隔离性问题。

核心思路很简单:给每行数据保存多个历史版本,不同事务看到的数据版本不同

二、MVCC的核心概念:3个隐藏字段 + 2个关键日志

要实现多版本,数据库会在每行数据的背后偷偷做手脚,同时依赖两类日志,我们以 MySQL InnoDB 引擎为例(主流数据库的MVCC实现逻辑类似)。

1. 每行数据的3个隐藏字段

InnoDB 会为每一行记录自动添加3个隐藏字段,它们是实现MVCC的基础:

隐藏字段 作用
DB_TRX_ID 事务ID:记录最后一次修改(插入/更新)这条数据的事务ID
DB_ROLL_PTR 回滚指针:指向这条数据的上一个历史版本(存放在 undo log 中)
DB_ROW_ID 行ID:如果表没有主键,InnoDB会自动生成这个字段作为聚簇索引,和MVCC关联不大

2. 两个关键日志

(1)undo log:数据的“历史版本仓库”

undo log回滚日志,它的核心作用是:

  • 当事务执行更新/删除操作时,会先把数据的旧版本保存到 undo log 中。
  • 事务回滚时,可以用 undo log 恢复数据。
  • 给MVCC提供历史版本:通过 DB_ROLL_PTR 指针,能串联起一行数据的所有历史版本,形成一个版本链

举个例子:一行数据被3个事务依次修改,它的版本链长这样:

最新版本(当前数据页) ← DB_ROLL_PTR ← 版本2(undo log) ← DB_ROLL_PTR ← 版本1(undo log)

(2)redo log:保证数据不丢(辅助作用)

redo log重做日志,主要用于保证数据库崩溃后的数据恢复,和MVCC的核心逻辑无关,这里只需要知道它是InnoDB的“保障”即可。

三、MVCC的核心:Read View(一致性视图)

当事务执行读操作SELECT)时,InnoDB 会为这个事务生成一个 Read View(一致性视图),这个视图是判断数据版本是否可见的“规则手册”。

1. Read View 包含的4个关键参数

参数 含义
m_ids 当前活跃的事务ID列表(即还没提交的事务ID)
min_trx_id m_ids 中的最小事务ID
max_trx_id 系统下一个要分配的事务ID(大于当前所有已分配的事务ID)
creator_trx_id 生成这个 Read View 的事务ID

2. 版本可见性判断规则

拿到 Read View 后,事务会去对比当前数据版本的 DB_TRX_ID(最后修改这个版本的事务ID)和 Read View 的参数,判断这个版本是否可见。规则如下(重点):

  1. 如果 DB_TRX_ID == creator_trx_id:这个版本是当前事务自己修改的,可见
  2. 如果 DB_TRX_ID < min_trx_id:修改这个版本的事务已经提交了,可见
  3. 如果 DB_TRX_ID >= max_trx_id:修改这个版本的事务是在 Read View 生成后才开启的,不可见
  4. 如果 min_trx_id <= DB_TRX_ID < max_trx_id
    • DB_TRX_IDm_ids 中:事务还没提交,不可见
    • DB_TRX_ID 不在 m_ids 中:事务已经提交,可见

如果当前版本不可见,就通过 DB_ROLL_PTRundo log上一个历史版本,重复上述判断,直到找到可见的版本,或者版本链结束(返回空)。

四、举个例子:彻底看懂MVCC的执行过程

假设数据库里有一张 user 表,初始数据如下:

id name DB_TRX_ID DB_ROLL_PTR
1 张三 100 null

现在有3个事务并发执行,事务ID分别是 200201202,执行顺序如下:

时间 事务200(未提交) 事务201(读) 事务202(已提交)
T1 执行 UPDATE user SET name='李四' WHERE id=1 - -
T2 - 执行 SELECT * FROM user WHERE id=1 -
T3 - - 执行 UPDATE user SET name='王五' WHERE id=1提交事务
T4 - 再次执行 SELECT * FROM user WHERE id=1 -

步骤1:T1 事务200修改数据

事务200更新数据时,InnoDB 会:

  • 把原数据(name=张三,DB_TRX_ID=100)保存到 undo log
  • 更新当前数据页:name=李四,DB_TRX_ID=200DB_ROLL_PTR 指向 undo log 里的旧版本。
  • 此时事务200未提交,属于活跃事务。

步骤2:T2 事务201第一次查询

事务201执行查询,生成 Read View

  • m_ids = [200](活跃事务只有200)
  • min_trx_id = 200
  • max_trx_id = 203(下一个要分配的事务ID)
  • creator_trx_id = 201

然后判断当前数据版本(DB_TRX_ID=200):

  • 200 在 m_ids 中 → 不可见。
  • 通过 DB_ROLL_PTR 找到 undo log 里的旧版本(DB_TRX_ID=100)。
  • 100 < min_trx_id(200) → 可见。
  • 所以事务201查到的 name 是 张三

步骤3:T3 事务202修改并提交

事务202更新数据时:

  • 把当前数据(name=李四,DB_TRX_ID=200)保存到 undo log
  • 更新数据页:name=王五,DB_TRX_ID=202DB_ROLL_PTR 指向 undo log 里的李四版本。
  • 事务202提交,从活跃事务列表中移除。

步骤4:T4 事务201第二次查询

事务201是可重复读隔离级别,整个事务只会生成一次 Read View(关键!),所以还是用 T2 生成的 Read View(m_ids=[200])。

判断当前数据版本(DB_TRX_ID=202):

  • 202 >= max_trx_id(203)?不,202 < 203。
  • 202 在 m_ids([200])里?不。
  • 但 202 > min_trx_id(200),且 Read View 生成时,事务202还没开启(max_trx_id是203,202是有效的,但不在活跃列表)→ 按照规则,是否可见?
    → 注意:可重复读隔离级别下,Read View 是事务启动时生成的,此时事务202还没修改数据,所以事务201看不到事务202的修改结果。
  • 继续找历史版本,直到找到 DB_TRX_ID=100 的版本 → 可见。
  • 所以第二次查询结果还是 张三 → 这就是可重复读的实现!

五、MVCC和事务隔离级别的关系

MVCC 主要在 MySQL 的两个隔离级别下工作:

  1. 读已提交(Read Committed)

    • 每次执行 SELECT 都会重新生成 Read View
    • 能看到已提交的事务修改结果 → 解决脏读,但可能出现不可重复读。
  2. 可重复读(Repeatable Read)

    • 整个事务只在第一次 SELECT 时生成一次 Read View
    • 不管其他事务怎么修改,只要没提交,当前事务看到的都是同一个版本 → 解决脏读、不可重复读,InnoDB 还通过间隙锁解决了幻读。

提示:读未提交(Read Uncommitted)直接读最新数据,串行化(Serializable)直接用锁,都不依赖 MVCC。

六、MVCC的优点和缺点

优点

  1. 读写不阻塞:读操作不用加锁,写操作也不会阻塞读操作 → 极大提升并发性能。
  2. 保证隔离性:在可重复读/读已提交级别下,避免脏读、不可重复读等问题。
  3. 数据一致性:每个事务看到的都是符合自己视图的一致数据。

缺点

  1. 存储开销:需要保存数据的多个历史版本,undo log 会占用额外的磁盘空间。
  2. 清理开销:数据库需要定期清理过期的 undo log(比如事务都提交了,旧版本没用了),这会带来一定的性能消耗。

七、总结

MVCC 的本质就是 “版本链 + Read View”

  • 版本链保存数据的历史修改记录。
  • Read View 判断当前事务能看到哪个版本的数据。
  • 最终实现 读写并行,同时保证事务隔离性。

它是数据库并发控制的“神器”,也是理解 InnoDB 事务机制的关键。


我可以帮你整理一份MVCC与锁机制的对比清单,方便你更清晰地理解两者的适用场景,需要吗?

posted @ 2026-01-20 19:57  先弓  阅读(3)  评论(0)    收藏  举报