【华为云MySQL技术专栏】MySQL Mini-Transaction (MTR) 作用及源码解析
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.cc和mtr0log.cc中,其中部分直接定义在了storage/innobase/include
目录下以mtr0开头的.h和.ic的文件中。
InnoDB引擎中的每一个MTR都是通过mtr_t类型的内存结构体来维护的,其中主要的成员变量存储在mtr_t::Impl类中:
mtr_t::Impl::m_memo和mtr_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)的类型(S,X或SX锁)添加到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添加到mtr的memo动态数组中。
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】。
下图描述了它的主要流程。它主要会做如下几件事:
更新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_memo和m_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 Log的type字段的第一位设置了MLOG_SINGLE_REC_FLAG。在崩溃恢复时:
-
当解析的第一条Redo记录无 MLOG_SINGLE_REC_FLAG标记时,说明当前正在解析的是包含多条Redo记录的MTR。这时会一直向后解析,直到找到类型为MLOG_MULTI_REC_END的Redo记录,然后将之前所有解析到的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中的页面加入到脏页链表中。
注意MTR的commit只是将其暂存的Redo Log记录添加到全局的redo log buffer中。在正常境况下,后台线程会定期检测Log Buffer填充情况,然后根据事务提交策略(innodb_flush_log_at_trx_commit
),将包含本次MTR日志的Log Buffer内容写入操作系统缓存,并最终 fsync
到磁盘日志文件。
4、总结
本文从源码角度深入分析了MTR的实现原理和使用方式。在InnoDB中MTR的主要功能和作用总结如下:
-
物理原子性:MTR保证了一组Redo Log对若干个物理页面的修改是原子性的。这是能正确崩溃恢复的基础。
-
资源管理:MTR执行时会把所有加锁的资源添加到
m_memo
变量中
统一管理。这可以保证MTR执行期间持有必要的锁(主要是页面的latch),防止并发修改和保证修改过程中页面不会被淘汰。
-
高效日志生成:MTR使用了私有缓冲区(m_log对象),允许页面修改函数高效、按需地追加Redo记录,避免了频繁操作全局Log Buffer的锁竞争。在
mtr_commit
时一次性批量复制到全局Log Buffer ,提高了效率。
- 点赞
- 收藏
- 关注作者
评论(0)