深入理解 MySQL事务 MVCC原理:读写不阻塞的并发控制机制

在高并发数据库系统中,如何让多个事务同时读写数据而不互相干扰,是一个核心难题。传统方案依赖锁机制,但锁会带来阻塞、死锁和性能瓶颈。为了解决这一问题,MySQL 的InnoDB 引擎引入了 MVCC(Multi-Version Concurrency Control,多版本并发控制) ——一种基于“快照”思想的无锁并发控制机制。

一、为什么需要 MVCC?

在没有MVCC 的年代,数据库主要靠行锁/表锁来保证事务隔离性。例如:

-- 事务 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id =1; -- 获取写锁

-- 事务B(被阻塞)
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 等待读锁释放

这种模式存在明显问题:

  • 读写互斥:写操作会阻塞所有读操作;
  • 并发度低:大量 SELECT 请求排队等待;
  • 死锁风险:多个事务交叉加锁容易形成环路。

而 MVCC 的核心思想是:不修改原数据,而是保留多个历史版本,让读操作看到“合适”的版本,从而避免加锁

MVCC 的最大优势:读不加锁,读写不冲突!

二、MVCC 只在哪些隔离级别下生效?

MySQL 支持四种事务隔离级别,但 MVCC 仅在以下两种级别中工作

隔离级别 是否使用 MVCC 说明
READ UNCOMMITTED 直接读最新数据(可能未提交),无视版本
READ COMMITTED (RC) 每次 SELECT 都生成新 ReadView
REPEATABLE READ(RR) 事务首次SELECT 时生成ReadView,后续复用
SERIALIZABLE 所有读都加共享锁,退化为锁机制

📌 注意:InnoDB 默认隔离级别是 REPEATABLE READ,这也是 MVCC 发挥最大价值的场景。

三、MVCC 的三大核心组件

InnoDB 实现 MVCC依赖三个关键技术:

1.隐藏字段(每行数据自带)

InnoDB 为每一行记录自动添加以下隐藏列:

  • DB_TRX_ID(6 字节):最后一次修改该行的事务 ID
  • DB_ROLL_PTR(7 字节):回滚指针,指向 undo log中的上一个版本
  • DB_ROW_ID(7 字节):无主键时用于生成聚簇索引(非 MVCC 核心)

💡 事务ID 是全局递增的整数,由InnoDB 内部维护。

2. Undo Log +版本链(VersionChain)

当执行 UPDATEDELETE 时:

  • 原始数据不会被覆盖,而是拷贝到 undolog 中;
  • 新版本写入数据页,并更新 DB_TRX_IDDB_ROLL_PTR
  • DB_ROLL_PTR指向 undolog 中的旧版本,形成一条 版本链

例如:

当前行 (trx_id=102) → undolog (trx_id=101) → undo log(trx_id=99) → ...

这样,通过链式结构可以回溯任意历史版本。

3. ReadView(读视图)

ReadView 是 MVCC 判断“哪个版本可见”的关键。它在事务执行第一个快照读(如普通 SELECT)时创建,包含以下信息:

  • m_ids:当前活跃(未提交)的事务 ID 列表
  • min_trx_idm_ids 中最小的事务 ID
  • max_trx_id:下一个将分配的事务 ID(即当前最大 ID+ 1)
  • creator_trx_id:当前事务自己的ID

可见性判断规则(对某行数据的 trx_id):

  1. 如果 trx_id == creator_trx_id自己修改的,可见
  2. 如果 trx_id < min_trx_id在 ReadView 创建前已提交,可见
  3. 如果 trx_id >= max_trx_id未来事务创建的,不可见
  4. 如果 trx_id ∈ m_ids其他活跃事务修改的,不可见
  5. 否则(trx_id < max_trx_id 且不在 m_ids 中)→ 已提交,可见

若当前版本不可见,则沿 DB_ROLL_PTR向上查找历史版本,直到找到可见版本或链结束。

四、RC与 RR 的 MVCC 行为差异

特性 READ COMMITTED (RC) REPEATABLE READ (RR)
ReadView 创建时机 每次 SELECT 都新建 事务首次SELECT 时创建,后续复用
是否可重复读 ❌(可能看到其他事务新提交的数据) ✅(始终看到同一快照)
幻读问题 存在(范围查询可能新增行) 快照读无幻读(但当前读仍可能有)

🔍 举例:事务 A 在 RR下两次 SELECT同一行,即使事务B 中间 UPDATE并 COMMIT,A仍看到第一次的结果。

五、Purge 线程:清理无用版本

Undolog 不会无限增长。InnoDB 有后台 purge线程,定期清理:

  • 已被标记删除(deleted_bit=true
  • 且对所有活跃事务都不可见的历史版本

这确保了存储空间的有效回收。

六、总结:MVCC 的本质

MVCC 本质上是一种 基于时间戳(事务ID)的乐观并发控制。它通过:

  • 保留多版本数据(undo log +版本链)
  • 动态判断可见性(ReadView)
  • 避免读写加锁

从而在保证隔离性的同时,极大提升了并发性能,特别适合 读多写少 的 OLTP场景。

⚠️但注意:MVCC 只解决快照读(普通 SELECT)的并发问题。对于SELECT ... FOR UPDATEUPDATEDELETE当前读(Current Read),仍需加锁!

延伸思考

  • 为什么 SERIALIZABLE 不用MVCC?→ 因为它要求完全串行,必须加锁。
  • Undo log 是逻辑日志还是物理日志?→ 逻辑日志(记录反向 SQL操作)。
  • MVCC 能解决幻读吗?→ 在 RR 下,快照读可以;但当前读仍需间隙锁配合

本站简介

聚焦于全栈技术和量化技术的技术博客,分享软件架构、前后端技术、量化技术、人工智能、大模型等相关文章总结。