欢迎光临
我们一直在努力

MySql中MVCC的底层原理

MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL InnoDB引擎实现高性能并发的关键机制。简单来说,它让数据库在读写并发的场景下,读操作不再需要等待写操作,写操作也不会阻塞读操作,显著提升了系统的吞吐量。

它通过在每行记录上保存多个版本,并为不同事务创建一致性快照(Read View),让事务看到自己“应该看到”的数据版本(如同一事务内始终读同一个快照,实现“可重复读”)。

⚙️ MVCC的三大核心组件

MVCC的实现依赖于以下三个在InnoDB表隐式存在的结构:

组件定义与作用
三个隐藏字段 InnoDB为每行记录自动添加了三个隐藏字段。
Undo Log 版本链 记录了数据行的所有历史版本,通过回滚指针串联形成一个单向链表。
Read View 读视图 事务执行快照读时生成的一致性快照,用于判断版本链中哪个版本对当前事务可见。

这三个组件共同协作,构成了MVCC的基石。

1. 三个隐藏字段

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

  • DB_TRX_ID (6字节):记录最后修改该行的事务ID,是进行可见性判断的核心依据。
  • DB_ROLL_PTR (7字节):回滚指针,指向该行在Undo Log中的上一个版本,用于构建版本链。
  • DB_ROW_ID (6字节):单调递增的行ID。当表没有定义主键时,InnoDB会用它生成隐藏的聚簇索引。
2. Undo Log 版本链

当一行数据被修改时,InnoDB不会直接覆盖旧值,而是执行以下操作:

  • 将被修改行的旧数据写入Undo Log。
  • 将该行当前的 DB_TRX_ID 更新为当前修改事务的ID。
  • 将当前行的 DB_ROLL_PTR 更新为指向刚写入的Undo Log的指针。
  • 通过这种方式,每一次修改都会在Undo Log中形成一个“旧版本”,并被 DB_ROLL_PTR 串联起来,形成一个按时间顺序从新到旧排列的版本链。
📌 示例场景
  • 事务 100 插入一行:id=1, name='A', age=20
  • 事务 101 更新该行:name='B', age=21
  • 事务 102 更新该行:name='C', age=22
  • 假设每个事务修改后都立即提交(为了简化,实际未提交时也会产生版本链)。


    🧬 详细版本链结构图

    【当前版本】聚簇索引中的记录(事务102修改后)
    ┌───────────────────────────────────────────────┐
    │ 实际数据: id=1, name='C', age=22 │
    │ DB_TRX_ID = 102 │
    │ DB_ROLL_PTR ──────────────────────────────┐ │
    └───────────────────────────────────────────┼────┘
    │
    ▼
    ┌────────────────────────────────────────────────┐
    │ Undo Log 记录(版本 V2) │
    │ 变更前数据: id=1, name='B', age=21 │
    │ 该版本的 DB_TRX_ID = 101 │
    │ 该版本的 DB_ROLL_PTR ──────────────────────┐ │
    │ 额外元数据:表空间ID、页号、记录偏移 等 │
    └───────────────────────────────────────────┼─────┘
    │
    ▼
    ┌─────────────────────────────────────────────────┐
    │ Undo Log 记录(版本 V1) │
    │ 变更前数据: id=1, name='A', age=20 │
    │ 该版本的 DB_TRX_ID = 100 │
    │ 该版本的 DB_ROLL_PTR = NULL (无更旧版本) │
    │ 额外元数据:表空间ID、页号、记录偏移 等 │
    └──────────────────────────────────────────────────┘

    3. Read View 读视图

    当事务执行快照读(普通的SELECT查询)时,InnoDB会为这次查询生成一个Read View(快照)。这个视图记录了在该时刻数据库的系统状态,主要包含四个核心属性:

    属性含义
    creator_trx_id 创建当前Read View的事务ID。
    m_ids 在生成Read View时,系统中所有活跃且未提交的事务ID列表。
    min_trx_id m_ids列表中的最小值,即当前活跃事务中最老的事务ID。
    max_trx_id 系统即将分配给下一个事务的事务ID(max_trx_id = 当前最大事务ID + 1)。

    注意:不同隔离级别下,Read View的生成策略不同。在READ COMMITTED级别,每次查询都会生成新的Read View;在REPEATABLE READ级别,整个事务只使用第一个查询生成的Read View。

    🎯 可见性判断算法 (Read View如何工作)

    当事务发起快照读时,InnoDB会沿着版本链,从最新的版本开始,为每个版本的DB_TRX_ID执行以下判断,直到找到第一个符合条件的版本:

  • 版本是否被当前事务更改:如果 DB_TRX_ID == creator_trx_id,说明该版本是当前事务自己修改的,可见,直接返回。
  • 版本是否早已提交:如果 DB_TRX_ID < min_trx_id,说明生成此版本的事务在Read View生成时已经提交,可见,返回该版本。
  • 版本是否来自未来的事务:如果 DB_TRX_ID >= max_trx_id,说明修改该版本的事务是在Read View生成之后才开始的,不可见,继续回溯上一版本。
  • 版本是否来自其他活跃事务:如果 min_trx_id <= DB_TRX_ID < max_trx_id,且 DB_TRX_ID 在 m_ids 列表中,说明生成该版本的事务在Read View生成时尚未提交,不可见,继续回溯上一版本。
  • 版本来自已提交且在当前Read View开启后启动的事务:如果 min_trx_id <= DB_TRX_ID < max_trx_id,但 DB_TRX_ID 不在 m_ids 列表中,说明事务在Read View生成时已经提交,可见,返回该版本。
  • 通过这套严谨的算法,每个事务都能获得一个与自己隔离级别相符、一致的数据快照。

    🔍 快照读 vs. 当前读

    MVCC并非适用于所有SQL语句,它主要作用于快照读。理解快照读与当前读的区别至关重要:

    类型定义操作语句锁机制
    快照读 读取数据的历史快照版本,不加锁,非阻塞。 普通的SELECT语句。 不加锁
    当前读 读取数据的最新提交版本,并对其加锁,防止并发修改。 SELECT … FOR UPDATESELECT … LOCK IN SHARE MODEUPDATE …DELETE …INSERT … 行锁、间隙锁、Next-Key Lock等

    实践提醒:当你需要基于可重复读的快照进行更新操作时,需要特别小心。例如,在一个事务中先用快照读查询了数据,然后再执行更新操作(当前读),可能会出现"更新丢失"的问题。建议在RR级别下,如果后续要更新数据,应使用SELECT … FOR UPDATE进行当前读,确保读到最新值。

    🔬 RC vs. RR:MVCC如何实现不同隔离级别

    MVCC是**READ COMMITTED(读已提交,RC)和REPEATABLE READ(可重复读,RR)隔离级别的底层实现基础。二者实现的核心区别在于Read View的生成策略**:

    隔离级别Read View 生成策略行为表现
    READ COMMITTED (RC) 语句级快照:事务中每条SELECT语句执行时,都会生成一个全新的Read View。 每次查询都获取最新的快照,因此可能读到其他事务已提交的新数据,导致同一事务内多次读取结果可能不一致,即“不可重复读”。
    REPEATABLE READ (RR) 事务级快照:事务中只在第一次执行SELECT(快照读)时生成一个Read View,后续的所有SELECT都复用它。 整个事务的查询都基于最初的那个快照,因此多次读取同一数据的结果始终一致,避免了“不可重复读”问题。

    注意:关于“幻读”,MVCC的 Read View 机制只解决了快照读场景下的幻读问题。但当发生当前读时,MySQL InnoDB 引擎还需要借助 Next-Key Lock(记录锁+间隙锁) 来阻止其他事务插入新记录,以避免幻读的发生。

    ※RR 隔离级别下,没有当前读时,如何解决幻读?

    首先明确“幻读”的定义

    幻读是指:在同一事务内,两次相同的范围查询(例如 SELECT * FROM t WHERE id > 10)返回的记录条数不一致,因为另一个事务插入了符合条件的新记录。

    注意:幻读强调的是 插入 操作导致的“多出几行”,与不可重复读(针对已存在行的更新)不同。

    RR 隔离级别下,快照读(普通 SELECT)避免幻读的原理

    在 RR 级别,事务内第一次执行快照读时,会生成一个 Read View(即一致性视图),之后该事务的所有快照读都复用这个 Read View,不再重新生成。

    这个 Read View 记录了“当时”数据库中所有活跃事务的 ID 列表,以及已提交事务的最大 ID 等信息。
    当后续查询再次扫描同一个范围时,依然使用同一个 Read View,因此:

    • 即使其他事务在两次查询之间插入了新行并提交了,这些新行的 DB_TRX_ID 一定大于等于生成 Read View 时的 max_trx_id(因为它们是后来才提交的事务)。
    • 根据 MVCC 可见性算法,DB_TRX_ID >= max_trx_id 的行对当前 Read View 是不可见的(属于“未来的事务”)。
    • 所以,第二次快照读的结果集与第一次完全一样,看不见新插入的行,幻读因此被避免。
    举例说明

    — 事务 A(RR 隔离级别)
    START TRANSACTION;
    — 第一次快照读,生成 Read View
    SELECT * FROM user WHERE age > 18; — 返回 2 行

    — 事务 B(并发执行)
    START TRANSACTION;
    INSERT INTO user (age) VALUES (25); — 插入新行
    COMMIT;

    — 事务 A 再次快照读(复用之前的 Read View)
    SELECT * FROM user WHERE age > 18; — 依然返回原来的 2 行,没有幻读
    COMMIT;

    重要补充说明
    • 以上机制只对快照读有效。如果事务 A 在第二次查询之前执行了 当前读(如 SELECT … FOR UPDATE 或 UPDATE …),那么当前读会看到其他事务已提交的最新数据(因为它读取的是最新版,并加锁),此时可能产生幻读。
    • 若要彻底消除幻读(包括当前读场景),InnoDB 需要配合 间隙锁(Gap Lock) 或 Next-Key Lock,但这超出了“没有当前读”的范畴。
    结论
    • 在 RR 隔离级别下,只要事务中的所有查询都是快照读(普通 SELECT),没有穿插任何当前读,那么 MVCC 就能完美解决幻读问题。
    • 这是由于 Read View 的事务级复用,使得事务始终看到与第一次查询时一致的数据快照,新插入的行被自动过滤。

    ⚖️ MVCC的优缺点

    优点:

    • 读写不阻塞:提升高并发读写场景下的吞吐量。
    • 解决常见隔离性问题:在RC和RR级别下,避免脏读,并在RR级别下避免不可重复读和快照读幻读。
    • 高并发读性能:显著提升读操作性能。

    缺点:

    • 存储空间开销:Undo Log占用额外空间,频繁更新会导致其膨胀。
    • 写冲突风险:不解决“写-写”冲突,仍需依赖锁机制。
    • 历史版本维护成本:如果存在长事务,会阻止Undo Log清理,导致版本链过长,影响查询性能。
    • 查询性能开销:需要遍历版本链进行可见性判断,增加查询成本。

    💎 总结

    MVCC 让 InnoDB 在大部分情况下,可以无锁地实现高效、一致的快照读,同时区分了 RC 和 RR 两种隔离级别。

    在实际的开发中,掌握 MVCC 可以帮助做更合理的数据库设计:

    • 选择合适的隔离级别:在大多数读多写少的业务场景下,充分利用 RR 级别默认带来的强大一致性保证;在对一致性要求不那么严格但对并发敏感的场景,可酌情考虑切换至 RC 级别以降低锁开销。
    • 规避长事务:长事务会阻止 Undo Log 的回收,导致磁盘空间膨胀和历史版本链过长,严重影响数据库性能。务必确保生产环境中的事务代码在完成操作后立即提交。
    • 理解快照读与当前读的差异:如上所述,这是正确编写并发程序的关键。
    赞(0)
    未经允许不得转载:171主机测评 » MySql中MVCC的底层原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址