暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

海山数据库(He3DB)源码详解:海山MySQL redo日志-MTR

wukong 2024-11-19
45

# 一、Mini-Transaction

MySQL对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称mtr。一个事务可以包含若干条语句,每一条语句其实是由若干个mtr组成,每一个mtr又可以包含若干条redo日志。

在这里插入图片描述

1.1 原子操作

所谓原子操作即要么全部成功,要么全部失败,不存在中间状态。

在事务执行过程中,每条语句作为一个mtr来执行。而在执行语句的过程中产生的redo日志被划分成了若干个不可分割的组

如:

  • 更新Max Row ID属性时产生的redo日志是不可分割的。
  • 向聚簇索引对应B+树的页面中插入一条记录时产生的redo日志是不可分割的。
  • 向某个二级索引对应B+树的页面中插入一条记录时产生的redo日志是不可分割的。

以向某个索引对应的B+树插入一条记录为例,在向B+树中插入这条记录之前,需要先定位到这条记录应该被插入到哪个叶子节点代表的数据页中,定位到具体的数据页之后,有两种可能的情况:

  • 情况一:该数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,那么事情很简单,直接把记录插入到这个数据页中,记录一条类型为MLOG_COMP_REC_INSERT的redo日志就好了,我们把这种情况称之为乐观插入。假如某个索引对应的B+树长这样:

在这里插入图片描述

现在我们要插入一条键值为10的记录,很显然需要被插入到页b中,由于页b现在有足够的空间容纳一条记录,所以直接将该记录插入到页b中就好了,如图所示:

在这里插入图片描述

  • 情况二:该数据页剩余的空闲空间不足。遇到这种情况要进行所谓的页分裂操作,也就是新建一个叶子节点,然后把原先数据页中的一部分记录复制到这个新的数据页中,然后再把记录插入进去,把这个叶子节点插入到叶子节点链表中,最后还要在内节点中添加一条目录项记录指向这个新创建的页面。很显然,这个过程要对多个页面进行修改,也就意味着会产生多条redo日志,因此将这种情况称之为悲观插入。假如某个索引对应的B+树长这样:

在这里插入图片描述

现在如果要插入一条键值为10的记录,很显然需要被插入到页b中,但是从图中也可以看出来,此时页b已经塞满了记录,没有更多的空闲空间来容纳这条新记录了,所以我们需要进行页面的分裂操作,就像这样:

在这里插入图片描述

如果作为内节点的页a的剩余空闲空间也不足以容纳增加一条目录项记录,那需要继续做内节点页a的分裂操作,也就意味着会修改更多的页面,从而产生更多的redo日志。

二、mtr源码解析

2.1 prepare_write()

/** Prepare to write the mini-transaction log to the redo log buffer. @return number of bytes to write in finish_write() */ ulint mtr_t::Command::prepare_write() { switch (m_impl->m_log_mode) { case MTR_LOG_SHORT_INSERTS: ut_ad(0); /* fall through (write no redo log) */ case MTR_LOG_NO_REDO: case MTR_LOG_NONE: ut_ad(m_impl->m_log.size() == 0); log_mutex_enter(); m_end_lsn = m_start_lsn = log_sys->lsn; return (0); case MTR_LOG_ALL: break; } ulint len = m_impl->m_log.size(); ulint n_recs = m_impl->m_n_log_recs; ut_ad(len > 0); ut_ad(n_recs > 0); if (len > log_sys->buf_size / 2) { // 如果当前日志的大小(len)超过了redo日志缓冲区大小的一半 log_buffer_extend((len + 1) * 2); // 扩展缓冲区大小 } ut_ad(m_impl->m_n_log_recs == n_recs); fil_space_t *space = m_impl->m_user_space; if (space != NULL && is_system_or_undo_tablespace(space->id)) { /* Omit MLOG_FILE_NAME for predefined tablespaces. */ space = NULL; } log_mutex_enter(); if (fil_names_write_if_was_clean(space, m_impl->m_mtr)) { /* 如果这是自上次检查点以来第一次修改表空间,则需要添加一些文件名记录到日志中, 并在日志末尾添加MLOG_MULTI_REC_END标记。 */ ut_ad(m_impl->m_n_log_recs > n_recs); mlog_catenate_ulint(&m_impl->m_log, MLOG_MULTI_REC_END, MLOG_1BYTE); len = m_impl->m_log.size(); } else { /* 如果这不是第一次修改表空间,则检查是否只有一个日志记录。 如果是,则将该记录标记为单记录(MLOG_SINGLE_REC_FLAG)。 如果有多个记录,则在日志末尾添加MLOG_MULTI_REC_END标记。 */ ut_ad(n_recs == m_impl->m_n_log_recs); if (n_recs <= 1) { ut_ad(n_recs == 1); /* Flag the single log record as the only record in this mini-transaction. */ *m_impl->m_log.front()->begin() |= MLOG_SINGLE_REC_FLAG; } else { /* Because this mini-transaction comprises multiple log records, append MLOG_MULTI_REC_END at the end. */ mlog_catenate_ulint(&m_impl->m_log, MLOG_MULTI_REC_END, MLOG_1BYTE); len++; } } /* 检查是否需要触发检查点,以确保日志系统不会超出其容量限制 */ log_margin_checkpoint_age(len); return (len); }
  • 该函数是数据库事务日志系统的一部分,用于准备将mtr写入重做日志缓冲区。它根据事务日志的模式(m_log_mode)来决定如何处理日志写入。

  • 代码逻辑:

    • 1、日志模式判断
    • 2、日志长度和记录数检查
    • 3、日志缓冲区扩展
    • 4、表空间处理
    • 5、进入日志互斥锁
    • 6、文件名记录处理
      如果这是自最新检查点以来首次修改表空间,则需要添加文件名记录到日志中。
      更新记录数,并在日志末尾添加MLOG_MULTI_REC_END标记。
      重新计算日志长度。
    • 7、非首次修改表空间:
      如果不是首次修改表空间,则根据记录数判断是单个记录还是多个记录。
      单个记录:标记该记录为单记录事务。
      多个记录:在日志末尾添加MLOG_MULTI_REC_END标记,并增加日志长度。
    • 8、检查点处理
    • 9、返回日志长度

2.2 execute()

/** Write the redo log record, add dirty pages to the flush list and release the resources. */ void mtr_t::Command::execute() { ut_ad(m_impl->m_log_mode != MTR_LOG_NONE); if (const ulint len = prepare_write()) { finish_write(len); } if (m_impl->m_made_dirty) { log_flush_order_mutex_enter(); } /* It is now safe to release the log mutex because the flush_order mutex will ensure that we are the first one to insert into the flush list. */ log_mutex_exit(); m_impl->m_mtr->m_commit_lsn = m_end_lsn; release_blocks(); if (m_impl->m_made_dirty) { log_flush_order_mutex_exit(); } release_all(); release_resources(); }
  • 该函数的作用是执行一系列与事务相关的操作,包括写入重做日志记录、将脏页添加到刷新列表,并释放相关资源。

1、检查前置条件

ut_ad(m_impl->m_log_mode != MTR_LOG_NONE);
  • 使用ut_ad调试宏,用于在开发过程中捕获逻辑错误。
  • 这里用于检查日志模式是否是MTR_LOG_NONE,确保在尝试写入日志之前,日志模式是有效的。
    2、准备写入日志
if (const ulint len = prepare_write()) { finish_write(len); }
  • 首先调用prepare_write函数准备写入日志,并获取要写入的日志长度。
  • 如果返回长度不为0,则表示有日志需要写入,调用finish_write函数完成日志的写入。

3、处理脏页

if (m_impl->m_made_dirty) { log_flush_order_mutex_enter(); }
  • 如果事务过程中产生了脏页,则需要进入log_flush_order_mutex互斥锁。
  • 这个锁用于确保在将脏页添加到刷新列表时,没有其他线程同时修改这个列表。

4、释放日志互斥锁

release_blocks();
  • 在确保脏页将被安全处理后,可以释放log_mutex。

5、更新提交日志序列号

m_impl->m_mtr->m_commit_lsn = m_end_lsn;
  • 更新事务的提交日志序列号(LSN)为当前操作的结束LSN。

6、释放资源并退出锁

release_blocks(); // 释放数据块 if (m_impl->m_made_dirty) { log_flush_order_mutex_exit(); // 退出互斥锁 } release_all(); // 释放所有资源 release_resources(); // 释放额外资源

2.3 finish_write()

/** Append the redo log records to the redo log buffer @param[in] len number of bytes to write */ void mtr_t::Command::finish_write( ulint len) { ut_ad(m_impl->m_log_mode == MTR_LOG_ALL); ut_ad(log_mutex_own()); ut_ad(m_impl->m_log.size() == len); ut_ad(len > 0); if (m_impl->m_log.is_small()) { const mtr_buf_t::block_t* front = m_impl->m_log.front(); ut_ad(len <= front->used()); m_end_lsn = log_reserve_and_write_fast( front->begin(), len, &m_start_lsn); if (m_end_lsn > 0) { return; } } /* Open the database log for log_write_low */ m_start_lsn = log_reserve_and_open(len); mtr_write_log_t write_log; m_impl->m_log.for_each_block(write_log); m_end_lsn = log_close(); }
  • 该函数的目的是将指定长度的redo日志记录追加到redo日志缓冲区中

1、断言检查

ut_ad(m_impl->m_log_mode == MTR_LOG_ALL); // 确保当前的日志模式是记录所有更改 ut_ad(log_mutex_own()); // 确保当前线程持有日志互斥锁 ut_ad(m_impl->m_log.size() == len); // 确保redo日志缓冲区中的日志记录大小与要写入的大小相同 ut_ad(len > 0); // 确保要写入的长度大于0

2、快速写入检查

if (m_impl->m_log.is_small()) { const mtr_buf_t::block_t* front = m_impl->m_log.front(); ut_ad(len <= front->used()); m_end_lsn = log_reserve_and_write_fast( front->begin(), len, &m_start_lsn); if (m_end_lsn > 0) { return; } }
  • 如果redo日志缓冲区中的日志记录较小,则使用快速写入路径。
  • 获取缓冲区的前端块(front),并检查要写入的长度是否小于或等于该块已使用的空间。
  • 调用log_reserve_and_write_fast函数尝试快速写入。成功则直接返回。

3、常规写入路径

m_start_lsn = log_reserve_and_open(len); mtr_write_log_t write_log; m_impl->m_log.for_each_block(write_log); m_end_lsn = log_close();
  • 如果快速写入失败或不适用于当前情况,则进入常规写入路径。
  • 调用log_reserve_and_open函数为日志写入预留空间,并获取起始日志序列号。
  • 使用m_impl->m_log.for_each_block(write_log);遍历redo日志缓冲区中的每个块,并准备将它们写入到日志文件中。
  • 调用log_close函数完成日志写入,并获取结束日志序列号。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论