MVCC实现原理

发布于:2025-06-07 ⋅ 阅读:(18) ⋅ 点赞:(0)

​MVCC机制简明指南​

MVCC(Multi-Version Concurrency Control,多版本并发控制)是数据库实现高并发的核心技术,​​核心思想​​是通过保存数据的多个版本,让读写操作互不阻塞。以下是其核心原理和实现逻辑:


​一、MVCC解决什么问题?​

​1. 传统锁机制的痛点​

  • ​读-写冲突​​:读数据时加锁,会阻塞写操作(反之亦然)。
  • ​并发度低​​:悲观锁(如SELECT FOR UPDATE)导致大量线程等待。

​2. MVCC的解决方案​

  • ​读操作​​:读取数据的​​历史快照​​(无需加锁)。
  • ​写操作​​:创建新版本数据,不影响正在读取旧版本的事务。

​二、MVCC的实现原理(以InnoDB为例)​

​1. 隐藏字段​

每行记录包含两个隐藏字段:

  • DB_TRX_ID​:最后一次修改该行的事务ID。
  • DB_ROLL_PTR​:指向Undo Log中旧版本数据的指针(构成版本链)。
id name age DB_TRX_ID DB_ROLL_PTR
1 Alice 25 101 0x123456

​2. Undo Log(回滚日志)​

  • 存储数据被修改前的值,用于:
    • ​事务回滚​​:恢复到修改前的状态。
    • ​构建版本链​​:支持MVCC读取历史版本。

​3. ReadView(读视图)​

事务每在读取数据时生成一个快照,决定能看到哪些版本的数据:

  • m_ids​:当前活跃(未提交)的事务ID列表,是一个集合
  • min_trx_id​:所有正在并发的活跃事务中最小ID。
  • max_trx_id​:系统预分配的下一个事务ID。
  • creator_trx_id​:创建该ReadView的事务ID。

​三、MVCC的工作流程​

​1. 读操作(SELECT)​

  • m_ids​:当前活跃(未提交)的事务ID列表,是一个集合
  • min_trx_id​:所有正在并发的活跃事务(​m_ids)中最小ID。
  • max_trx_id​:系统预分配的下一个事务ID
  • creator_trx_id​:创建该ReadView的事务ID(查询事务)。
  • DB_TRX_ID​:最后一次修改该行的事务ID。最新修改的事务ID。

判断redo log版本链中,到底那一条数据可以被当前事务看见:

  1. 检查行数据的DB_TRX_ID
    • 如果DB_TRX_ID < min_trx_id:说明该版本已提交,​​可见​​。
    • 如果DB_TRX_ID > max_trx_id:说明该版本是未来事务修改的,​​不可见​​。
    • 如果min_trx_id ≤ DB_TRX_ID ≤ max_trx_id
      • DB_TRX_IDm_ids中:说明事务未提交,​​不可见​​。
      • 否则:​​可见​​。
  2. 若不可见,则通过DB_ROLL_PTR找到Undo Log中的旧版本,重复判断。

​2. 写操作(UPDATE/INSERT/DELETE)​

  • ​UPDATE​​:拷贝当前行到Undo Log,修改当前行并更新DB_TRX_ID
  • ​DELETE​​:标记删除(逻辑删除),通过DB_ROLL_PTR保留旧版本。
  • ​INSERT​​:直接插入新行,分配新DB_TRX_ID

​四、MVCC如何保证隔离级别?​

​隔离级别​ ​MVCC的实现方式​
​读未提交(RU)​ 直接读最新数据,忽略版本链
​读已提交(RC)​ 每次SELECT生成新ReadView,能看到其他事务已提交的修改
​可重复读(RR)​ 事务内第一次SELECT生成ReadView,后续复用该视图(MySQL默认,避免不可重复读和幻读)
​串行化(S)​ 退化为悲观锁(如加表锁)

​五、MVCC的优缺点​

​优点​

  • ​读不阻塞写,写不阻塞读​​:大幅提升并发性能。
  • ​避免脏读和不可重复读​​:通过版本链和ReadView机制。

​缺点​

  • ​存储开销​​:需额外保存历史版本(Undo Log占用空间)。
  • ​清理成本​​:需要定期清理不再需要的旧版本(Purge线程)。

​六、示例说明​

假设事务A(ID=100)和事务B(ID=101)并发操作:

  1. ​初始数据​​:行X的DB_TRX_ID=90(已提交)。
  2. ​事务B修改行X​​:
    • 将旧值存入Undo Log,更新行X的DB_TRX_ID=101
  3. ​事务A读取行X​​:
    • 生成ReadView:m_ids=[101](事务B未提交),min_trx_id=101
    • 发现行X的DB_TRX_ID=101m_ids中,不可见 → 通过DB_ROLL_PTR读取Undo Log中的旧版本(DB_TRX_ID=90),返回该值。

​总结​

MVCC通过​​版本链+读视图​​的机制,在保证事务隔离性的同时,最大化并发性能。它是MySQL高并发能力的基石,理解其原理对优化SQL和排查并发问题至关重要。

​通俗易懂版解释:MVCC的可见性规则​

你可以把MVCC的可见性判断想象成一个"时间线游戏",通过比较事务ID的大小关系,决定当前事务能看到哪个版本的数据。下面用最直白的语言和例子解释:


​1. 关键角色说明​

  • DB_TRX_ID​:每行数据上贴的"最后修改者身份证号"(事务ID)。
  • min_trx_id​:当前系统中"最老未提交事务的身份证号"。
  • max_trx_id​:系统即将分配的下一个事务ID(未来事务的起点号)。
  • m_ids​:当前所有"未提交事务的身份证号列表"。

​2. 判断规则拆解​

​情况1:DB_TRX_ID < min_trx_id(已提交的旧数据)​
  • ​比喻​​:你(当前事务)在查资料时,发现这份资料的修改者(DB_TRX_ID=50)比教室里最老的学生(min_trx_id=100)还早,说明他早就交卷离开(已提交)。
  • ​结论​​:这份资料是​​可见的​​。
​情况2:DB_TRX_ID > max_trx_id(未来的数据)​
  • ​比喻​​:资料上写着修改者ID是200,但系统现在最大只分配到150,这个ID属于"未来人"。
  • ​结论​​:这份资料是​​不可见的​​(可能是系统异常)。
​情况3:min_trx_id ≤ DB_TRX_ID ≤ max_trx_id(可能是未提交的数据)​
  • ​子情况3.1:DB_TRX_IDm_ids列表中​

    • ​比喻​​:资料修改者(DB_TRX_ID=120)还在教室里考试(未提交事务列表中有他)。
    • ​结论​​:​​不可见​​(不能看他未提交的答案)。
  • ​子情况3.2:DB_TRX_ID不在m_ids列表中​

    • ​比喻​​:资料修改者ID是130,但教室里没这个人(说明他已交卷离开)。
    • ​结论​​:​​可见​​。
​如果不可见怎么办?​
  • ​动作​​:顺着DB_ROLL_PTR指针(类似"上一版本链接")找到旧版本数据,重新判断。
  • ​比喻​​:当前资料不可见,就去翻看它的历史修订版,直到找到符合条件的版本。

​3. 实际例子​

假设当前系统状态:

  • 活跃事务列表m_ids = [100, 110](事务100和110未提交)
  • min_trx_id = 100, max_trx_id = 120

某行数据的版本链:

版本3: DB_TRX_ID=110 (通过ROLL_PTR指向版本2)
版本2: DB_TRX_ID=100 (通过ROLL_PTR指向版本1) 
版本1: DB_TRX_ID=80

​事务A(ID=105)读取该行时的判断过程​​:

  1. 先看最新版本(版本3):
    • DB_TRX_ID=110min_trx_id=100 → 属于情况3
    • 110m_ids=[100,110]中 → 不可见
  2. 通过ROLL_PTR找到版本2:
    • DB_TRX_ID=100min_trx_id=100 → 属于情况3
    • 100m_ids中 → 不可见
  3. 继续找版本1:
    • DB_TRX_ID=8080 < min_trx_id=100 → 属于情况1
    • ​可见!​​ 最终返回版本1的数据。

​4. 为什么这样设计?​

  • ​读已提交(RC)​​:每次查询都重新生成ReadView,能看到其他事务最新提交的数据。
  • ​可重复读(RR)​​:事务内第一次查询生成ReadView后固定不变,保证多次读取结果一致。

​总结​

MVCC的可见性规则本质是:
​通过比较事务ID,判断数据版本是否由已提交的事务创建​​。

  • 比"最老未提交事务"更早的 → 安全可见
  • 属于"未提交事务"的 → 不可见
  • 属于"已提交但不算太老"的 → 也可见

就像考试时,你只能参考已交卷同学的答案,不能看还在答题的同学的试卷!


网站公告

今日签到

点亮在社区的每一天
去签到