mysql的MVVC版本控制原理

47 阅读9分钟

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 存储引擎实现高并发读写的核心技术,核心目标是:在不加锁的情况下,让读事务(快照读)和写事务并发执行,避免读写冲突,同时保障事务隔离性(解决脏读、不可重复读等问题)。

简单说:MVCC 让 “读” 不用等 “写”,“写” 不用等 “读”,大幅提升数据库并发效率,是 InnoDB 区别于其他存储引擎(如 MyISAM)的关键特性之一。

一、MVCC 的核心目标:解决 “读写冲突”

在没有 MVCC 时,数据库并发控制依赖锁机制,但会出现严重问题:

  • 读锁(S 锁)和写锁(X 锁)互斥:一个事务在读数据时,其他事务不能写;一个事务在写数据时,其他事务不能读 —— 导致 “读写阻塞”,并发效率极低。

MVCC 的优化思路:为数据维护多个版本,读事务读取 “历史版本”,写事务修改 “新版本”,两者互不干扰。具体来说:

  • 写事务执行时,不会覆盖原始数据,而是生成一个新的数据版本;
  • 读事务根据自身的 “可见性规则”,选择合适的历史版本读取,无需等待写事务提交;
  • 旧版本数据不会立即删除,而是通过 “版本链” 管理,当不再被任何读事务引用时,由后台线程清理。

二、MVCC 的核心组成:隐藏列 + 版本链 + Read View

InnoDB 实现 MVCC 依赖三大核心组件,缺一不可:

1. 行数据的隐藏列(版本元数据)

InnoDB 表的每一行数据,除了用户定义的字段,还隐含 3 个核心隐藏列(用于维护版本信息):

隐藏列名作用说明
DB_TRX_ID最近一次修改该数据的事务 ID(唯一标识一个事务,自增分配)
DB_ROLL_PTR回滚指针,指向该数据的上一个版本(存储在 undo log 中),形成版本链
DB_ROW_ID隐含主键(若表无主键 / 唯一索引,InnoDB 自动生成),用于标识行唯一性

示例:假设表 user 有 id=1, name='张三', age=20,其实际存储结构(简化):

id(用户列)name(用户列)age(用户列)DB_TRX_ID(隐藏)DB_ROLL_PTR(隐藏)DB_ROW_ID(隐藏)
1张三20100指向 undo log 中版本 110001

2. 版本链(基于 undo log 的历史版本链表)

每当事务修改数据时,InnoDB 不会直接覆盖原始数据,而是执行以下步骤:

  1. 将数据的 “原始版本” 复制到 undo log 中(作为历史版本);
  2. 修改当前行数据,更新 DB_TRX_ID 为当前事务 ID,更新 DB_ROLL_PTR 指向 undo log 中的原始版本;
  3. 多次修改后,undo log 中的历史版本通过 DB_ROLL_PTR 串联,形成 “版本链”(最新版本在数据表中,旧版本在 undo log 中)。
版本链示例(3 次事务修改)

假设事务 100、200、300 依次修改 id=1 的数据:

  1. 事务 100(新增):INSERT INTO user VALUES (1, '张三', 20) → DB_TRX_ID=100DB_ROLL_PTR=NULL(无历史版本);
  2. 事务 200(修改):UPDATE user SET age=21 WHERE id=1 → 原始版本(age=20)存入 undo log,当前行 DB_TRX_ID=200DB_ROLL_PTR 指向 undo log 中事务 100 的版本;
  3. 事务 300(修改):UPDATE user SET age=22 WHERE id=1 → 原始版本(age=21)存入 undo log,当前行 DB_TRX_ID=300DB_ROLL_PTR 指向 undo log 中事务 200 的版本。

最终版本链结构(从新到旧):

当前行(age=22, TRX_ID=300) → undo log(age=21, TRX_ID=200) → undo log(age=20, TRX_ID=100)

3. Read View(读视图:判断版本可见性的规则)

版本链中存在多个历史版本,读事务需要知道 “哪个版本对自己可见”—— 这就是 Read View 的作用:事务启动时生成的 “快照”,包含当前活跃事务的 ID 范围,定义了版本可见性规则

Read View 的核心属性

Read View 包含 4 个关键参数,用于判断版本是否可见:

属性名作用说明
m_low_limit_id下一个将要分配的事务 ID(所有大于等于该 ID 的事务,对当前视图不可见)
m_up_limit_id活跃事务中最小的事务 ID(所有小于该 ID 的事务,均已提交,对当前视图可见)
m_ids生成 Read View 时,当前所有活跃(未提交)的事务 ID 集合
m_creator_trx_id生成 Read View 的事务自身的 ID(自身事务修改的版本,对自己可见)
版本可见性判断规则(核心逻辑)

读事务在版本链中遍历(从最新版本到旧版本),对每个版本的 DB_TRX_ID(修改该版本的事务 ID)执行以下判断:

  1. 若 DB_TRX_ID < m_up_limit_id:该版本由已提交事务修改,可见,直接读取;

  2. 若 DB_TRX_ID >= m_low_limit_id:该版本由未来事务修改,不可见,继续遍历旧版本;

  3. 若 m_up_limit_id <= DB_TRX_ID < m_low_limit_id

    • 若 DB_TRX_ID 不在 m_ids 中(该事务已提交),可见
    • 若 DB_TRX_ID 在 m_ids 中(该事务仍活跃),不可见,继续遍历旧版本;
  4. 若遍历到版本链末尾仍无可见版本,返回空(或报错)。

关键结论

Read View 的生成时机,直接决定了事务的隔离级别(RC/RR)—— 这是 MVCC 与事务隔离性的核心关联。

三、MVCC 与事务隔离级别的关联(RC vs RR)

InnoDB 的 RC(读已提交)  和 RR(可重复读)  隔离级别,均基于 MVCC 实现,但核心差异在于:Read View 的生成时机不同

1. RC 级别:每次查询都生成新的 Read View

  • 规则:事务内每次执行 SELECT 语句时,都会重新生成一个 Read View;
  • 效果:只能看到 “查询瞬间已提交” 的事务版本,避免脏读,但可能出现 “不可重复读”。
示例(RC 级别下的不可重复读)
时间顺序事务 A(读事务)事务 B(写事务)
T1BEGIN; 开启事务-
T2SELECT age FROM user WHERE id=1 → 读 View1,age=20(可见事务 100 的版本)-
T3-BEGIN; 开启事务,UPDATE user SET age=21 WHERE id=1DB_TRX_ID=200
T4SELECT age FROM user WHERE id=1 → 读 View2,age=20(事务 B 未提交,200 在 m_ids 中,不可见)-
T5-COMMIT; 提交事务 B
T6SELECT age FROM user WHERE id=1 → 读 View3,age=21(事务 B 已提交,200 不在 m_ids 中,可见)-
  • 结果:事务 A 两次查询同一数据,结果不一致(20→21),即 “不可重复读”—— 这是 RC 级别的特性。

2. RR 级别:事务启动时生成一次 Read View,全程复用

  • 规则:事务启动(BEGIN)后,第一次执行 SELECT 语句时生成 Read View,后续所有 SELECT 复用该 View;
  • 效果:事务内多次查询同一数据,始终看到 “第一次查询时的快照”,避免不可重复读和幻读(InnoDB 对 RR 的优化)。
示例(RR 级别下的可重复读)
时间顺序事务 A(读事务)事务 B(写事务)
T1BEGIN; 开启事务-
T2SELECT age FROM user WHERE id=1 → 生成 View1,age=20(可见事务 100 的版本)-
T3-BEGIN; 开启事务,UPDATE user SET age=21 WHERE id=1DB_TRX_ID=200
T4SELECT age FROM user WHERE id=1 → 复用 View1,age=20200 在 m_ids 中,不可见)-
T5-COMMIT; 提交事务 B
T6SELECT age FROM user WHERE id=1 → 复用 View1,age=20200 仍在 View1 的 m_ids 中,不可见)-
T7COMMIT; 提交事务 A,新事务查询 → age=21-
  • 结果:事务 A 全程复用同一个 Read View,即使事务 B 提交了修改,仍看不到新版本 —— 这就是 “可重复读”。

3. 隔离级别与 MVCC 总结

隔离级别Read View 生成时机MVCC 效果解决的并发问题
RC(读已提交)每次 SELECT 都生成新 View读已提交,允许不可重复读、幻读禁止脏读
RR(可重复读)事务内第一次 SELECT 生成,全程复用可重复读,禁止幻读(InnoDB 优化)禁止脏读、不可重复读
读未提交不生成 Read View,直接读最新版本允许脏读、不可重复读、幻读
串行化不用 MVCC,直接加表锁串行执行完全隔离,无并发问题所有并发问题

四、MVCC 的适用场景与限制

1. 适用场景(快照读)

MVCC 仅对 快照读(非锁定读)  生效,即:普通 SELECT 语句(不加锁):

SELECT * FROM user WHERE id=1;  -- 快照读,走 MVCC,不加锁

2. 不适用场景(锁定读)

以下操作属于 锁定读,不走 MVCC,而是通过锁机制控制并发(避免修改冲突):

  • SELECT ... FOR SHARE(加 S 锁,共享读锁);
  • SELECT ... FOR UPDATE(加 X 锁,排他写锁);
  • UPDATEDELETEINSERT(写操作,默认加 X 锁)。

3. 版本清理机制(purge 线程)

版本链中的旧版本(undo log 中的历史数据)不会一直存在,否则会占用大量磁盘空间。InnoDB 有一个后台线程 purge 线程,负责:

  • 定期扫描 undo log,判断哪些历史版本 “不再被任何 Read View 引用”(即没有读事务需要读取该版本);
  • 将这些无用的旧版本删除,释放磁盘空间。

4. 限制

  • 仅 InnoDB 支持 MVCC(MyISAM 不支持事务,也不支持 MVCC);
  • 长事务会导致版本链过长(undo log 膨胀):因为长事务的 Read View 会一直引用旧版本,purge 线程无法清理,可能导致磁盘空间占用过高、崩溃恢复时间变长。

五、MVCC 与锁机制的区别(核心对比)

很多人会混淆 MVCC 和锁机制,两者的核心目标和适用场景完全不同:

对比维度MVCC(多版本并发控制)锁机制(行锁 / 表锁)
核心目标解决 “读写冲突”(读不阻塞写,写不阻塞读)解决 “写写冲突”(防止多个事务同时修改同一数据)
适用场景快照读(普通 SELECT)锁定读 / 写操作(FOR UPDATE、UPDATE、DELETE 等)
并发效率高(无锁,非阻塞)中 / 低(加锁,可能阻塞)
数据一致性基于快照,保证隔离级别(RC/RR)基于锁竞争,保证修改原子性

六、总结

MVCC 是 InnoDB 实现高并发读写的核心技术,其本质是:通过 “隐藏列维护版本元数据”+“undo log 构建版本链”+“Read View 控制可见性”,让读事务读取历史版本,写事务生成新版本,从而实现读写不阻塞

关键要点:

  1. MVCC 的核心是 “多版本”,通过版本链隔离读写操作;
  2. Read View 的生成时机决定隔离级别:RC 每次查询生成,RR 事务内复用;
  3. MVCC 仅对普通 SELECT(快照读)生效,写操作仍依赖锁机制;
  4. 避免长事务,防止版本链过长导致的性能问题。