【华为云MySQL技术专栏】MySQL Mini-Transaction (MTR) 作用及源码解析

举报
GaussDB 数据库 发表于 2025/08/15 08:52:32 2025/08/15
【摘要】 1、背景介绍在InnoDB存储引擎中,Mini-Transaction (MTR) 并非是面向用户的“事务”,需要用户显式地执行begin、commit来开启和提交,而是引擎用来保证对物理页面修改的原子性、一致性、隔离性和持久性的内部事务。例如,对表插入一条数据时会发生页面分裂,此时会涉及到分配一个新页面,将部分数据拷贝到新页面中,更新父节点指针,Undo Log的写入等操作会产生数十条Re...

1、背景介绍

InnoDB存储引擎中,Mini-Transaction (MTR) 并非是面向用户的“事务”,需要用户显式地执行begin、commit来开启和提交,而是引擎用来保证对物理页面修改的原子性、一致性、隔离性和持久性的内部事务。

例如,对表插入一条数据时会发生页面分裂,此时会涉及到分配一个新页面,将部分数据拷贝到新页面中,更新父节点指针,Undo Log的写入等操作会产生数十条Redo Log。为了防止只有部分Redo Log被刷新到了磁盘中导致在崩溃恢复时恢复出错误的B+树,我们需要将这些不可分割的Redo Log以组的形式来记录,来保证对B+树修改时的原子性。

对此,InnoDB引入了MTR的概念,主要负责对多条Redo log的管理和写入,并对数据页面的并发访问进行控制,以保证数据最终的一致性。本文将基于MySQL 8.0.41,从源码角度剖析MTR的设计、用途、及其使用方式。

2、核心源码结构

MTR相关的结构体和函数主要定义在storage/innobase/mtr目录下的mtr0mtr.ccmtr0log.cc中,其中部分直接定义在了storage/innobase/include目录下以mtr0开头的.h.ic的文件中。

InnoDB引擎中的每一个MTR都是通过mtr_t类型的内存结构体来维护的,其中主要的成员变量存储在mtr_t::Impl类中:

1.pngmtr_t::Impl::m_memomtr_t::Impl::m_log是其中最重要的两个成员变量。他们通过dyn_buf_t类型的动态数组进行维护。

  • m_memo 

其中,m_memo是用来记录本次 MTR 中涉及的资源和其对应锁的类型。数组的主要存储的对象为mtr_memo_slot_t

struct mtr_memo_slot_t {
  void *object; //指向关联的对象,通常是维护页面信息的buf_block_t和维护锁相关的rw_lock_t
  // 关联对象的类型,当对象是页面相关信息时,对应的类型为MTR_MEMO_PAGE_S_FIX,MTR_MEMO_PAGE_X_FIX,MTR_MEMO_PAGE_SX_FIX, MTR_MEMO_BUF_FIX表示给对应页面加了S,X,SX,或未加锁
  //当object的对象是锁相关的信息,对应的类型为MTR_MEMO_S_LOCK, MTR_MEMO_X_LOCK, MTR_MEMO_SX_LOCK,表示给对应读写锁加了S,X或SX锁。
  ulint type; 

  /** Check if the object stored in this slot is a lock (rw_lock_t).
  @return true if it is a lock object, false otherwise. */
  bool is_lock() const {
    return type == MTR_MEMO_S_LOCK || type == MTR_MEMO_X_LOCK ||
           type == MTR_MEMO_SX_LOCK;
  }

  // 打印此结构体的方法
  std::ostream &print(std::ostream &out) const;
};

其中成员变量object主要是类型为buf_block_t的数据页内存结构体或类型为rw_lock_t的读写锁。我们可以通过调用mtr_memo_push()函数将当前线程需要访问到的资源以及访问时对其添加的闩锁(Latch)的类型(SXSX锁)添加到MTR中。由于Latch锁的存在,我们可以保证buffer pool中被访问的页面不会被其他线程修改或淘汰。

这些资源最后会在MTR 提交时释放。

  • m_log 

功能用来临时存储本次MTR生成的Redo Log记录

结构本质是一个动态增长的字节数组 (dyn_array_t 或内部指针管理)。其中每512个字节为一个block,按block维度去管理内存的申请和释放。

操作在需要写入Redo Log时,我们可以通过mlog_open()函数获取m_log中可以写入的block的位点,或者在block剩余空间不足时创建新的block来提供Redo Log所需的内存空间,然后可以调用例如mlog_write_initial_log_record_low来写入一条Redo记录的头部信息,再然后可以mach_write_to_2()等系统函数继续写入Redo记录的具体内容。最后需要使用mlog_close()更新动态数组中已使用的字节等相关信息。

提交MTR提交时,即 mtr_commit() 调用时,m_log 中的内容会被复制到全局的 Log Buffer (log_t::buf中。 

3、MTR 在代码中的使用方式

我们以一个需要修改页面内容为例,介绍 MTR的使用流程:

void some_innodb_function() {  
mtr_t mtr;             // 1. 在栈上声明一个 mtr_t 对象
  
mtr_start(&mtr);     // 2. 初始化 MTR (设置状态为 ACTIVE, 初始化 m_log, m_memo等成员变量)

  // 3. 执行页面修改操作 (核心!)  
buf_block_t* block1 = btr_block_get(..., &mtr, MTR_MEMO_PAGE_X_FIX); // 获取页面并加X锁,记录到m_memo
  
// ... 修改 block1 的数据 ...
  mlog_write_ulint(block1->frame + OFFSET, new_value, MLOG_4BYTES, &mtr); // 记录redo到m_log
  buf_block_t* block2 = btr_block_get(..., &mtr, MTR_MEMO_PAGE_X_FIX);  // ... 修改 block2 的数据 (可能涉及多个页面) ...  
mlog_write_string(block2->frame + OFFSET, data, len, &mtr);

  // ... 可能还会操作其他页面 ...
  // 4. 提交 MTR  
mtr_commit(&mtr);      // 关键步骤!见下方分解
}

1. 声明mtr_t对象,并调用mtr_start()初始化mtr_t的成员变量。

void mtr_t::start(bool sync) {
  ...
  new (&m_impl.m_log) mtr_buf_t();
  new (&m_impl.m_memo) mtr_buf_t();
 
  m_impl.m_mtr = this;
  m_impl.m_log_mode = MTR_LOG_ALL;
  m_impl.m_inside_ibuf = false;
  m_impl.m_modifications = false;
  m_impl.m_n_log_recs = 0;
  m_impl.m_state = MTR_STATE_ACTIVE;
  m_impl.m_flush_observer = nullptr;
  m_impl.m_marked_nolog = false;
  ...
}

2.通过btr_block_get()获取页面的控制块buf_block_t。在获取页面时会将对应的block添加到mtrmemo动态数组中。

btr_block_get() //会传入要获取的页面的page id,加锁类型等信息
  btr_block_get_func()
    buf_page_get_gen()
      Buf_fetch<T>::single_page()
        mtr_add_page(block)
          rw_lock_..._lock_gen() // 加S,X或SX锁
          mtr_memo_push() // 将block加入到mtr::impl::memo中

3. 页面修改及Redo写入。

InnoDB有几十种Redo Log的类型,在枚举类mlog_id_t中记录。不同类型的Redo Log可能会有不同的结构。我们以mlog_write_ulint为例。它会先根据类型,修改对应页面的内存数据为指定的值,然后写入一条类型为MLOG_1BYTE, MLOG_2BYTE或MLOG_4BYTE的Redo日志

/* ptr: 页面, val:要修改的值,type:写入多少字节,mtr: MTR变量 */
void mlog_write_ulint(byte *ptr, ulint val, mlog_id_t type,mtr_t *mtr) {
  /* 根据类型使用mach_write_to_1(), mach_write_to_2(), mach_write_to_4()修改页面 */
  switch (type) {
     case MLOG_1BYTE:
        mach_write_to_1(ptr, val);
        break;
     …
  }
  /* 申请mtr::m_impl::m_log动态数组中的空间 */
  byte *log_ptr = nullptr;
  if (!mlog_open(mtr, REDO_LOG_INITIAL_INFO_SIZE + 2 + 5, log_ptr)) {
    return;
  }
  /* 写入一条redo日志的头部信息,LOG_TYPE, SPACE_ID, PAGE_NO */
  log_ptr = mlog_write_initial_log_record_fast(ptr, type, log_ptr, mtr);
  /* 写入要修改的位置的偏移量 */
  mach_write_to_2(log_ptr, page_offset(ptr));
  log_ptr += 2;
  /* 写入要修改的值 */
  log_ptr += mach_write_compressed(log_ptr, val);
  /* 更新动态数组中已使用的字节 */
  mlog_close(mtr, log_ptr);
}

4.MTR提交。

mtr_t::commit()中,会对已经完成修改的MTR进行提交,其具体函数的调用可以参考文章结尾【1】。

下图描述了它的主要流程。它主要会做如下几件事:

  • 2.png更新MTR状态:在开始时设置状态为MTR_STATE_COMMITTING,在提交结束后设置MTR_STATE_COMMITTED。
  • Redo日志及脏页拷贝:如果有Redo产生,则使用mtr_t::Command::execute()MTR中暂存的所有Redo Log拷贝到全局的redo log buffer中,并且将对应的页面添加到flush list中。 

  • 资源释放:在所有的Redo log和脏页添加好后,会通过mtr_t::Command::release_all()mtr_t::Command::release_resources ()释放m_memom_log申请的内存块。

mtr_t::Command::execute()将日志拷贝到全局的Log Buffer前,会先执行mtr_t::Command::prepare_write(),进行拷贝前的准备动作,涉及两个动作:

【一】为了保证一组Redo Log的操作是原子的,会在该组中的最后一条Redo日志后加上一条MLOG_MULTI_REC_END类型的Redo日志。当MTR中只有一条Redo Log时,InnoDB进行了优化,在Redo Logtype字段的第一位设置了MLOG_SINGLE_REC_FLAG在崩溃恢复时:

  • 当解析的第一条Redo记录无 MLOG_SINGLE_REC_FLAG标记时,说明当前正在解析的是包含多条Redo记录的MTR。这时会一直向后解析,直到找到类型为MLOG_MULTI_REC_ENDRedo记录,然后将之前所有解析到的Redo记录根据存储到Hash Table中进行回放。

    如果在发现 MLOG_MULTI_REC_END Redo记录前得到了具有MLOG_SINGLE_REC_FLAG标识的Redo记录或一直无法找到带有MLOG_MULTI_REC_END的记录,则说明MTR提交时可能存在异常,会丢弃前面所有Redo记录。这些Redo记录也就不会被应用在数据页面上。

    除此之外,解析的Redo记录如果内容是错误的,例如记录头部的Type类型不符合预期,在页面中写入的偏移量超出UNIV_PAGE_SIZE等情况,会直接停止应用这个MTR的内容。

  • 当解析的第一条Redo记录有MLOG_SINGLE_REC_FLAG标记时,表明正在解析的是只有一条Redo记录的MTR如果记录没有损坏会直接使用recv_add_to_hash_table()添加到Hash Table中等待后续应用

借助上述机制,MTR可以保证在崩溃恢复时,多条Redo记录可以原子地修改页面。具体实现在函数recv_parse_log_recs()recv_single_rec()recv_multi_rec()中。

【二】然后计算MTR暂存的Redo Log需要的空间长度,为了后续申请全局的Log Buffer空间准备。

之后会使用log_buffer_reserve()原子地增加 log_t::sn,并等待全局的Redo Log buffer有足够的位置来记录进行拷贝。然后将 mtr_t::m_log 缓冲区中的 Redo Log记录复制到全局Log Buffer (log_t::buf)中预留好的位置。之后再通过add_dirty_blocks_to_flush_list()函数来将memo中的页面加入到脏页链表中。

注意MTRcommit只是将其暂存的Redo Log记录添加到全局的redo log buffer中。在正常境况下,后台线程会定期检测Log Buffer填充情况,然后根据事务提交策略(innodb_flush_log_at_trx_commit),将包含本次MTR日志的Log Buffer内容写入操作系统缓存,并最终 fsync 到磁盘日志文件。

4、总结

本文从源码角度深入分析了MTR的实现原理和使用方式。在InnoDBMTR的主要功能和作用总结如下:

  • 物理原子性MTR保证了一组Redo Log对若干个物理页面的修改是原子性的。这是能正确崩溃恢复的基础。

  • 资源管理MTR执行时会把所有加锁的资源添加到m_memo变量中统一管理。这可以保证MTR执行期间持有必要的锁(主要是页面的latch),防止并发修改和保证修改过程中页面不会被淘汰。

  • 高效日志生成MTR使用了私有缓冲区(m_log对象),允许页面修改函数高效、按需地追加Redo记录,避免了频繁操作全局Log Buffer的锁竞争。在mtr_commit 时一次性批量复制到全局Log Buffer ,提高了效率。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。