# 一、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进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




