MySQL架构与底层原理之:`InnoDB`存储引擎的`MVCC`(多版本并发控制):`undo log`与`read view`的工作原理。

MySQL InnoDB MVCC深度剖析:Undo Log与Read View

各位同学,大家好!今天我们来深入探讨MySQL InnoDB存储引擎中一个非常核心的概念——MVCC(多版本并发控制)。MVCC是InnoDB实现高并发的关键技术之一,它允许事务并发地读写数据库,而无需加锁,从而显著提高系统的性能。

我们今天主要聚焦于MVCC中两个关键组件:Undo Log和Read View,彻底搞清楚它们是如何协同工作,来实现数据的一致性读取。

1. 什么是MVCC?

MVCC(Multi-Version Concurrency Control)即多版本并发控制。简单来说,它为每一行数据维护多个版本,每个版本对应一个事务对该数据的修改。当一个事务需要读取数据时,它会根据一定的规则读取特定版本的数据,而不是直接读取最新的数据。 这样,不同的事务可以同时读取同一行数据的不同版本,而无需互相阻塞。

2. MVCC解决的问题

MVCC主要解决以下问题:

  • 读写阻塞问题: 传统的锁机制会导致读写操作相互阻塞,降低并发性能。MVCC允许读操作读取旧版本的数据,而无需等待写操作完成。
  • 脏读问题: 事务可以读取未提交的修改,可能导致数据不一致。MVCC通过读取已提交的旧版本数据,避免了脏读。
  • 不可重复读问题: 在同一个事务中,多次读取同一行数据可能得到不同的结果。MVCC通过读取事务开始时的数据版本,保证了可重复读。

3. Undo Log:数据的时光机

Undo Log,顾名思义,就是用于撤销操作的日志。它记录了事务在修改数据之前的原始状态,以便在事务回滚或需要读取旧版本数据时使用。

  • Undo Log的类型:

    Undo Log主要分为两种类型:

    • Insert Undo Log: 用于记录INSERT操作产生的Undo Log。由于INSERT操作的数据只对当前事务可见,因此Insert Undo Log只需要在事务回滚时使用,可以直接丢弃。
    • Update Undo Log: 用于记录UPDATE和DELETE操作产生的Undo Log。Update Undo Log不仅在事务回滚时使用,还需要在MVCC中提供旧版本数据,因此不能立即丢弃。
  • Undo Log的存储:

    Undo Log存储在特殊的段(Segment)中,称为Undo Segment。Undo Segment是循环使用的,当Undo Log不再需要时,其空间可以被重用。

  • Undo Log的结构:

    Undo Log 包含必要的信息来重构旧版本的数据。 例如,对于UPDATE操作,Undo Log会包含被修改字段的原始值。对于DELETE操作,Undo Log会包含被删除行的完整数据。

  • 示例代码:

    虽然无法直接查看InnoDB内部的Undo Log,但我们可以通过模拟的方式来理解它的工作原理。

    class UndoLog:
        def __init__(self, log_type, table_name, row_id, old_values=None):
            self.log_type = log_type  # "INSERT", "UPDATE", "DELETE"
            self.table_name = table_name
            self.row_id = row_id
            self.old_values = old_values  # 字典,存储修改前的字段值
    
        def __repr__(self):
            return f"UndoLog(type={self.log_type}, table={self.table_name}, id={self.row_id}, old_values={self.old_values})"
    
    # 模拟一个UPDATE操作的Undo Log
    undo_log = UndoLog(
        log_type="UPDATE",
        table_name="users",
        row_id=123,
        old_values={"name": "Alice", "age": 30}
    )
    
    print(undo_log)

    这段代码模拟了一个UPDATE操作的Undo Log,它记录了users表中row_id为123的行的原始数据。 如果发生回滚,系统就可以使用这个Undo Log将数据恢复到原始状态。

4. Read View:事务的快照

Read View是MVCC实现的关键,它定义了事务能够看到哪些版本的数据。 每个事务在启动时都会创建一个Read View,它包含了当前系统中活跃事务的信息。

  • Read View的结构:

    Read View主要包含以下几个关键属性:

    • m_ids: 当前系统中所有活跃的读写事务ID列表。
    • min_trx_id: m_ids 列表中最小的事务ID。
    • max_trx_id: 下一个将要分配的事务ID,表示系统中最大的事务ID。
    • creator_trx_id: 创建该Read View的事务ID。
  • 版本可见性判断规则:

    当事务需要读取一行数据时,InnoDB会根据Read View来判断该行数据的哪个版本是可见的。判断规则如下:

    1. 如果数据的版本号(trx_id)小于min_trx_id,则说明该版本是在创建Read View之前已经提交的事务创建的,对当前事务可见。
    2. 如果数据的版本号(trx_id)大于等于max_trx_id,则说明该版本是在创建Read View之后创建的,对当前事务不可见。
    3. 如果数据的版本号(trx_id)在min_trx_idmax_trx_id之间,则需要判断该版本号是否在m_ids列表中:
      • 如果在m_ids列表中,则说明该版本是由当前活跃的事务创建的,对当前事务不可见(除非版本号等于creator_trx_id,表示是当前事务自己修改的)。
      • 如果不在m_ids列表中,则说明该版本是在创建Read View之前已经提交的事务创建的,对当前事务可见。
  • 示例代码:

    class ReadView:
        def __init__(self, creator_trx_id, m_ids, min_trx_id, max_trx_id):
            self.creator_trx_id = creator_trx_id
            self.m_ids = m_ids  # List of active transaction IDs
            self.min_trx_id = min_trx_id
            self.max_trx_id = max_trx_id
    
        def is_visible(self, trx_id):
            """判断事务ID是否对当前ReadView可见"""
            if trx_id < self.min_trx_id:
                return True  # Version created before ReadView, visible
            elif trx_id >= self.max_trx_id:
                return False # Version created after ReadView, not visible
            elif trx_id in self.m_ids:
                return trx_id == self.creator_trx_id  # Visible only if it's the creator's own transaction
            else:
                return True  # Version created before ReadView and committed, visible
    
    # 模拟一个ReadView
    read_view = ReadView(
        creator_trx_id=100,
        m_ids=[100, 101, 102],
        min_trx_id=100,
        max_trx_id=105
    )
    
    # 测试不同的事务ID的可见性
    print(f"Transaction 99 is visible: {read_view.is_visible(99)}")   # True
    print(f"Transaction 100 is visible: {read_view.is_visible(100)}")  # True (creator)
    print(f"Transaction 101 is visible: {read_view.is_visible(101)}")  # False
    print(f"Transaction 103 is visible: {read_view.is_visible(103)}")  # True
    print(f"Transaction 105 is visible: {read_view.is_visible(105)}")  # False
    

    这段代码模拟了一个Read View,并根据版本可见性判断规则,判断了不同事务ID的可见性。

5. Undo Log和Read View的协同工作

Undo Log和Read View协同工作,实现了MVCC的核心功能。 当一个事务需要读取一行数据时,InnoDB会执行以下步骤:

  1. 获取Read View: 事务在启动时,会创建一个Read View,记录当前系统中活跃事务的信息。
  2. 查找最新版本: InnoDB首先查找该行数据的最新版本。
  3. 版本可见性判断: InnoDB使用Read View判断该版本是否对当前事务可见。
    • 如果可见,则直接读取该版本的数据。
    • 如果不可见,则通过Undo Log回溯到更早的版本,直到找到一个可见的版本。
  4. 返回数据: InnoDB将可见版本的数据返回给事务。

6. 示例演示

为了更直观地理解Undo Log和Read View的工作原理,我们来模拟一个简单的场景:

  • 初始状态: users表中id=1的行的name字段值为'Alice'trx_id=10
  • 事务A(trx_id=100)启动: 创建Read View A,m_ids=[100], min_trx_id=100, max_trx_id=101
  • 事务B(trx_id=101)启动: 创建Read View B,m_ids=[100,101], min_trx_id=100, max_trx_id=102
  • 事务A更新数据: 事务A将id=1的行的name字段更新为'Bob',生成Undo Log,trx_id=100
  • 事务B读取数据: 事务B读取id=1的行的name字段。
    1. InnoDB找到最新版本的数据,name='Bob', trx_id=100
    2. 使用Read View B判断该版本是否可见。由于trx_id=100m_ids=[100, 101]中,且不等于creator_trx_id=101,因此该版本不可见。
    3. InnoDB通过Undo Log回溯到上一个版本,name='Alice', trx_id=10
    4. 使用Read View B判断该版本是否可见。由于trx_id=10小于min_trx_id=100,因此该版本可见。
    5. 事务B读取到的数据为name='Alice'
  • 事务A提交: 事务A提交,trx_id=100的数据对后续事务可见。
  • 事务C(trx_id=102)启动: 创建Read View C,m_ids=[102], min_trx_id=102, max_trx_id=103
  • 事务C读取数据: 事务C读取id=1的行的name字段。
    1. InnoDB找到最新版本的数据,name='Bob', trx_id=100
    2. 使用Read View C判断该版本是否可见。由于trx_id=100小于min_trx_id=102,因此该版本可见。
    3. 事务C读取到的数据为name='Bob'

通过这个例子,我们可以看到Undo Log和Read View如何协同工作,保证了不同事务读取到一致的数据版本。

7. RC和RR隔离级别下的MVCC

InnoDB支持四种事务隔离级别,分别是READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。 MVCC在READ COMMITTED(RC)和REPEATABLE READ(RR)隔离级别下发挥作用。

  • READ COMMITTED (RC):

    在RC隔离级别下,每次读取数据都会创建一个新的Read View。 这意味着,在同一个事务中,每次读取同一行数据都可能读取到不同的版本,从而导致不可重复读。

  • REPEATABLE READ (RR):

    在RR隔离级别下,事务在第一次读取数据时创建一个Read View,之后所有的读取操作都使用同一个Read View。 这保证了在同一个事务中,多次读取同一行数据都会得到相同的结果,从而避免了不可重复读。这是InnoDB默认的隔离级别。

8. 总结: Undo Log与Read View保障数据读取的一致性

Undo Log记录了数据的历史版本,Read View定义了事务能够看到的数据版本。通过Undo Log和Read View的协同工作,InnoDB实现了MVCC,允许事务并发地读写数据库,同时保证数据的一致性。在RC隔离级别下,每次读取都创建新的Read View,可能导致不可重复读;在RR隔离级别下,事务只创建一次Read View,保证了可重复读。

希望今天的讲解能够帮助大家更深入地理解MySQL InnoDB的MVCC机制。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注