《MySQL运维内参》节选 | InnoDB日志管理机制(六)

运维派——国内最大的IT运维社区,欢迎关注运维派微信公众号(ID: yunweipai),获取更多资讯

本文接上文,开始接受MySQL的日志刷盘和扫描问题,在本文中涉及到很多代码片段解析,代码注释是关键,建议收藏本文并在电脑上阅读。

日志刷盘时机

前面已经介绍了大部分关于REDO日志的内容了,但还有一个问题没有讲,就是日志刷盘的时机,也就是什么时候才会将日志刷入磁盘。

现在已经知道,当MTR提交时,所产生的日志,都会先写入到Log
Buffer中,这是日志产生的最初来源。从这个源头开始,InnoDB会在不同的时机,将这些日志写入到磁盘,分别有下面5种时机。

1.Log Buffer空间用完了,便会将已经产生的Log
Buffer中的日志刷到磁盘中,这个时机在前面介绍Mtr时已经说过了。这是最普遍的一种方式。

2.Master线程在后台每秒钟刷一次,将当前Log Buffer中的日志刷到磁盘中。

3.每次执行DML操作时,都会主动检查日志空间是否足够,如果使用空间的量已经超过了一个预设的经验值,就会主动去刷日志,以保证在后面真正执行时,不会在执行过程中被动地等待刷盘,但这里只会是写文件(写入OS缓存中),不会刷磁盘。

4.在做检查点的时候,要保证所有要刷的数据页面中LSN值最小(最旧)的日志已经刷入到磁盘。不然,如果此时数据库挂了,日志不存在,但数据页面已经被修改,从而导致数据不一致,就违背了先写日志的原则。

5.提交逻辑事务时,会因为参数innodb_flush_log_at_trx_commit值的不同,产生不同的行为。如果设置为0,则在事务提交时,根本不会去刷日志缓冲区,这种设置是最危险的,如果此时运气不好,那对数据库最新的修改都会丢失,即使事务已经提交了,但丢失的事务一般是最新1秒内产生的,因为Master线程会每隔1秒刷一次。如果设置为1,则在事务提交时会将日志缓冲区中的日志写入到文件中,同时会将这次写入强刷到磁盘中,保证数据完全不丢失,但这种设置会使得数据库性能下降很多,影响性能。如果设置为2,则在事务提交时会将日志写入到文件中,但不会去刷盘,只要操作系统不挂,即使数据库挂了,数据还是不会丢失,一般都是设置为2即可。关于这个问题,可以用下面的图来简单表示。数据库

上面所说的基本上就是全部的日志刷盘时机了,相关内容都已经介绍清楚,在接下来的两小节中,会重要讲述数据库的恢复问题。

日志刷盘

REDO日志恢复

前面已经很全面地介绍了日志的生成、格式、刷盘、工作原理等,但这些实际上只是数据库运行时的一个“累赘”,没办法才会这样做,因为如果数据库不挂,日志是没有用的,但不挂是不可能的,所以日志是必须的。而前面介绍的所有内容都是建立在有日志的前提下,解决如何提高性能,如何保证数据完整性等问题的。那这里将介绍关于日志的新内容,日志的用途之一:数据库恢复。

在其他章节中,已经介绍了在InnoDB存储引擎的启动过程中,InnoDB需要做的事情有哪些,具体细节可以参考相关章节了解。在这一节中,需要重点关注的主要有两个,包括recv_recovery_from_checkpoint_start及recv_recovery_from_checkpoint_finish两个函数的处理(关于两个函数的关系,请参阅InnoDB启动相关章节)。

InnoDB启动之前,肯定是处于shutdown状态的,而导致shutdown的原因只有两种可能性,即正常关闭及Crash关闭。这里所说的数据恢复,主要处理的就是针对异常关闭时的情况。当然了,有一个叫innodb_fast_shutdown的参数,如果设置为2,也相当于是一次Crash了,道理也是一样的。

那可能有人就要问了,如果正常关闭(innodb_fast_shutdown设置为0或者1),那是不是就不执行数据库恢复了?其实不是这样的,不管如何关闭数据库,启动时都会做数据库恢复的操作,只不过正常关闭的情况下,不存在没有做过checkpoint的日志,或者说,最新的checkpoint已经在最新的LSN位置了,又或者说所有的数据页面都已经被刷成了最新的状态。说法可以有多种,但意义其实是一样的。

日志扫描

在开始准备做数据库恢复时,首先要做的就是从日志文件中找到最新的检查点信息。我们已经知道,在日志文件最开始的4个页面(每个页面512字节)中,存储的是用来管理日志文件及日志写入情况的信息,具体格式可以从前面看到。这里所关注的检查点信息是存储在第1号页面和第3号页面中的,即所谓的LOG_CHECKPOINT_1和LOG_CHECKPOINT_2。在做检查点时,这两个存储位置是轮换着使用的。

基于此,想要找到最新的检查点位置,就需要从上面的两个位置中找到一个最大值,也就是在这个点之前所有的日志都是失效的,并且对应的数据页面都是完整的。而在这个位置之后的页面,有可能是完整的,也有可能需要做REDO,这个决定于当时Buffer
Pool的刷盘情况,如果正好有被淘汰出去的页面,那就是完整的,否则还需要通过REDO日志来恢复。

先来看一下对应的精简之后的代码,如下。

UNIV_INTERN dberr_t
recv_recovery_from_checkpoint_start_func(
lsn_t min_flushed_lsn,/*!< in: min flushed lsn from data files */
lsn_t max_flushed_lsn)/*!< in: max flushed lsn from data files */
{
/* loval variables … */

if (srv_force_recovery >= SRV_FORCE_NO_LOG_REDO) {
ib_logf(IB_LOG_LEVEL_INFO,
“The user has set SRV_FORCE_NO_LOG_REDO on, “
“skipping log redo”);
return(DB_SUCCESS);
}

recv_recovery_on = TRUE;

mutex_enter(&(log_sys->mutex));

/* Look for the latest checkpoint from any of the log groups */

/* 如上所述,这里的工作就是用来从两个Checkpoint的位置,找到最新的
max_cp_group中保存的Checkpoint对应的信息,包括最新LSN信息、LSN对应的
日志文件中位置信息等。前面已经知道,5.6版本之后的InnoDB都支持
总空间超过4G大小的日志文件,所以这个位置信息包括了低32位值和高32
位值。max_cp_field用来表示最新位置是LOG_CHECKPOINT_1还是LOG_CHECKPOINT_1*/
err = recv_find_max_checkpoint(&max_cp_group, &max_cp_field);
if (err != DB_SUCCESS) {
mutex_exit(&(log_sys->mutex));
return(err);
}

/* 根据前面找到的max_cp_field信息,把这个位置对应的检查点信息全部读取出来,并
存储到log_sys->checkpoint_buf空间中,下面会用到这部分数据 */
log_group_read_checkpoint_info(max_cp_group, max_cp_field);
buf = log_sys->checkpoint_buf;

/* 从上面的log_sys->checkpoint_buf中拿到最新的检查点对应的LSN值及checkpoint_no值。
checkpoint_no就是在InnoDB做检查点时,给每一次分配的一个编号,顺序增长,值越大,
表示这个检查点越是最近做的 */
checkpoint_lsn = mach_read_from_8(buf + LOG_CHECKPOINT_LSN);
checkpoint_no = mach_read_from_8(buf + LOG_CHECKPOINT_NO);

/* Read the first log file header to print a note if this is
a recovery from a restored InnoDB Hot Backup */

/* 读出日志头的前4个页面(一个页面512字节)*/
fil_io(OS_FILE_READ | OS_FILE_LOG, true, max_cp_group->space_id, 0,
0, 0, LOG_FILE_HDR_SIZE,
log_hdr_buf, max_cp_group);

/* 从上面读取出的信息中,找到存储了ib_logfile的文件管理中,每一个块儿大小的
的位置。什么?文件块儿大小可以改变?是的,在MySQL官方版本中,块儿大小是不可以
修改的,都是512字节,但Percona为了适应存储设备方面的科技进步,就支持了这个功能。
当然,支持是支持了,但不用也没关系,如果不用,那么这个位置的值就是0,就认为还是默认值512字节*/

/* 声明:
不过需要注意的是,这里是为了说明一下这个特性在Percona中已经得到了支持。在前面
章节中,之所以在说明日志文件格式时没有讲到这个值,是因为在前面讲到的内容中,在
LOG_FILE_WAS_CREATED_BY_HOT_BACKUP之后,就没有其他内容了,这个页面就是空的了。
而Percona是将块儿大小的信息追加到这个信息之后,做到了与官方MySQL的兼容 */
log_hdr_log_block_size = mach_read_from_4(log_hdr_buf + LOG_FILE_OS_FILE_LOG_BLOCK_SIZE);
if (log_hdr_log_block_size == 0) {
/* 0 means default value */
log_hdr_log_block_size = 512;
}

/* Percona在这里很亲切地问候你,如果日志文件中存储的块儿大小和
当前系统设置的值不一样,也就是说这次数据库启动时修改了这个参数,
那么它会告诉你,并且会给出友好的建议,可以RECREATE日志文件,很贴心 */
if (UNIV_UNLIKELY(log_hdr_log_block_size != srv_log_block_size)) {
fprintf(stderr,
“InnoDB: Error: The block size of ib_logfile (” ULINTPF
“) is not equal to innodb_log_block_size.\n”
“InnoDB: Error: Suggestion – Recreate log files.\n”,
log_hdr_log_block_size);
return(DB_ERROR);
}

/* Start reading the log groups from the checkpoint lsn up. The
variable contiguous_lsn contains an lsn up to which the log is
known to be contiguously written to all log groups. */

/* 到此为止,用来做恢复的信息,都已经获取到了:
checkpoint_lsn:表示的是从这个位置开始,后面的日志需要做APPLY操作 */
recv_sys->parse_start_lsn = checkpoint_lsn;
recv_sys->scanned_lsn = checkpoint_lsn;
recv_sys->scanned_checkpoint_no = 0;
recv_sys->recovered_lsn = checkpoint_lsn;
srv_start_lsn = checkpoint_lsn;

/* 因为文件读取需要对齐到块儿大小,所以recv_sys->scanned_lsn
会做对齐处理,contiguous_lsn表示的就是对齐之后的值 */
contiguous_lsn = ut_uint64_align_down(recv_sys->scanned_lsn, OS_FILE_LOG_BLOCK_SIZE);

/* 目前,InnoDB只支持一个GROUP,所以这里的遍历实际上没有什么意义,
这里的处理是最重要的,所做的工作就是从contiguous_lsn的位置开始
扫描所有的日志数据,然后进一步做分析、恢复等操作 */
group = UT_LIST_GET_FIRST(log_sys->log_groups);
while (group) {
recv_group_scan_log_recs(group, &contiguous_lsn, &group_scanned_lsn);
group->scanned_lsn = group_scanned_lsn;
group = UT_LIST_GET_NEXT(log_groups, group);
}

/* other codes … */
/* 做完数据库恢复之后,要处理一下收尾工作。这个收尾工作非常重要,
类似于一个工程,在工作实施完成之后,还有一步是最后验收,验收的
时候一般会打上一个验收合格的标志,那么这里的操作也是同样的道理,
具体的操作就是再做一次检查点,更新一下最新的检查点信息,这样之前
处理的所有REDO日志就失效了,如果数据库再挂了,那也是重新洗牌,与
这次就没有什么关系了 */
recv_synchronize_groups();
/* The database is now ready to start almost normal processing of user
transactions: transaction rollbacks and the application of the log
records in the hash table can be run in background. */

return(DB_SUCCESS);
}

上面的代码,其实就是我们所熟悉的函数recv_recovery_from_checkpoint_start_func的执行过程。归纳起来,其所做的操作包括以下两部分。

  1. 从日志文件的固定位置找到最新的检查点信息。
  2. 从最新的检查点位置开始扫描日志文件,做数据库恢复。

现在,主要的工作就落在了recv_group_scan_log_recs上面,这个函数所要做的工作,就是将checkpoint_lsn位置开始的日志分片处理,每一片为2M大小,对应的精简之后的代码如下。

static void recv_group_scan_log_recs(
log_group_t* group,
lsn_t* contiguous_lsn,
lsn_t* group_scanned_lsn
)
{

/* local variables … */
finished = FALSE;
start_lsn = *contiguous_lsn;

/* 等待分析完毕 */
while (!finished) {

/* RECV_SCAN_SIZE大小为4*16K,也就是分片大小为64K,
因为已经知道,InnoDB的日志LSN的增长和数据量写入的增长是同步的。
也就是说LSN加1,表示日志就多写入一个字节,所以这里在LSN的计算中,加上
64K,表示的就是2M的日志量 */
end_lsn = start_lsn + RECV_SCAN_SIZE;

/* 在下面这个函数中,会根据之前读出来的LSN所对应的日志文件偏移位置,
将2M内容读取出来,存到log_sys->buf中,以待后面分析 */
log_group_read_log_seg(LOG_RECOVER, log_sys->buf, group, start_lsn, end_lsn, FALSE);

/* recv_scan_log_recs中,会检查到日志已经分析完毕,那
数据库的REDO就算基本完成了,上面的while循环停止,具体如何判断日志
内容读取完毕,请待进一步的讲述 */

finished = recv_scan_log_recs(
(buf_pool_get_n_pages()
– (recv_n_pool_free_frames * srv_buf_pool_instances))
* UNIV_PAGE_SIZE,
TRUE, log_sys->buf, RECV_SCAN_SIZE,
start_lsn, contiguous_lsn, group_scanned_lsn);

/* 下一个分片,从上一个分片的结束位置开始 */
start_lsn = end_lsn;
}
}

从上面的函数可以看到,数据库恢复时会根据最新检查点的位置,将日志不断分片读取,然后进行分片处理,这里再来分析一下InnoDB是如何做分片处理的。继续看精简之后的代码,如下。

UNIV_INTERN
ibool
recv_scan_log_recs(
ulint available_memory,
ibool store_to_hash,
const byte* buf, /*!< in: buffer containing a log segment or garbage */
ulint len, /*!< in: buffer length */
lsn_t start_lsn, /*!< in: buffer start lsn */
lsn_t* contiguous_lsn,
lsn_t* group_scanned_lsn)
{
/* local variables … */
/* 通过finished来表示恢复过程是否已经做完,如果做完则返回值为true */
finished = FALSE;
/* 存储了64KB的日志 */
log_block = buf;
scanned_lsn = start_lsn;
more_data = FALSE;

do {
/* 读出当前块中存储的数据量,一个块儿,默认大小为512字节,
如果没有扫描到最后一块,这个大小就都是512,因为日志都是连续存储的 */
data_len = log_block_get_data_len(log_block);

scanned_lsn += data_len;

/* 如果当前块儿中的数据量大于0,就会处理当前块 */
if (scanned_lsn > recv_sys->scanned_lsn) {
/* recv_sys,用来存储分析之后的日志。这里的工作是将从日志
文件中读取出来的原始数据去掉头(12字节)尾(4字节)数据之后,
将中间真正的日志取出来,放到recv_sys所指的缓存空间中,这部分数据
才是REDO恢复真正需要的数据,而在日志文件中存储的原始日志(包括头尾)
是为了更好更方便地管理而设置的,所以在这里会有这么一个转换的步骤。*/

/* 如果recv_sys的缓存空间已快要超过分析缓冲区大小(RECV_PARSING_BUF_SIZE=2M),
则说明当前recv_sys中缓存的日志太多,并且这些日志还不能满足APPLY的条件。
此时说明日志存储出现了错误,会在errlog中报出下面的信息,表示Recovery可能失败了。
为什么是RECV_PARSING_BUF_SIZE的大小呢?因为InnoDB认为,在写日志时,不会有
MTR所写的日志量超过这个值,如果有,则只能是日志存储或者解析出了问题。*/
if (recv_sys->len + 4 * OS_FILE_LOG_BLOCK_SIZE >= RECV_PARSING_BUF_SIZE) {
fprintf(stderr, “InnoDB: Error: log parsing”
” buffer overflow.”
” Recovery may have failed!\n”);
recv_sys->found_corrupt_log = TRUE;
} else if (!recv_sys->found_corrupt_log) {
/* 这里就是将当前块中真正的日志内容拿出来,存储到recv_sys缓存中去*/
more_data = recv_sys_add_to_parsing_buf(log_block, scanned_lsn);
}

/* 更新scanned_lsn,表示已经扫描的LSN值已经到了这个位置 */
recv_sys->scanned_lsn = scanned_lsn;
recv_sys->scanned_checkpoint_no = log_block_get_checkpoint_no(log_block);
}

/* 从这里也可以印证上面所述,如果一个日志块不足OS_FILE_LOG_BLOCK_SIZE(默认512字节),
则说明整个REDO日志扫描已经结束,已经扫描到了日志结尾的位置 */
if (data_len < OS_FILE_LOG_BLOCK_SIZE) {
/* Log data for this group ends here */
finished = TRUE;
break;
} else {
/* 没有结束则向前扫描OS_FILE_LOG_BLOCK_SIZE(512字节)的偏移量*/
log_block += OS_FILE_LOG_BLOCK_SIZE;
}
} while (log_block < buf + len && !finished);

*group_scanned_lsn = scanned_lsn;
/* 上面已经将当前块或之前块的日志放入到了recv_sys的缓冲区中了,
下面就会对这部分日志做一次处理,调用的核心函数为recv_parse_log_recs,
这个函数所要做的工作,接下来会以代码讲解的方式详细讲述 */
if (more_data && !recv_sys->found_corrupt_log) {
/* Try to parse more log records */
recv_parse_log_recs(store_to_hash);

/* 从这里看到,recv_parse_log_recs将日志进一步处理之后,如果占用的
缓存空间大于available_memory,就需要APPLY了,而这个缓存空间就是用于
恢复HASH表,这个HASH表后面会讲述。available_memory的大小,与Buffer Pool
有关系,InnoDB会拿一部分Buffer Pool空间来做REDO日志的恢复。
下面这个函数recv_apply_hashed_log_recs,
也会在后面说到 */
if (store_to_hash && mem_heap_get_size(recv_sys->heap) > available_memory) {
recv_apply_hashed_log_recs(FALSE);
}

/* 在recv_parse_log_recs中,处理掉一部分日志之后,缓冲区中
一般会有剩余的不完整的日志,这部分日志还不能被处理,需要等待读取
更多的日志进来,拼接之后才能继续处理,那么这里就需要将剩余的
这部分日志移到缓冲区最开始的位置,以便继续拼接更多的日志内容 */
if (recv_sys->recovered_offset > RECV_PARSING_BUF_SIZE / 4) {
/* Move parsing buffer data to the buffer start */
recv_sys_justify_left_parsing_buf();
}
}

return(finished);
}

日志

从上面的代码中,可以知道,InnoDB为了更好地管理日志文件,将连续的日志内容以块为单位来存储,加上头尾信息,继续连续存储,而在使用它的时候,又将这些日志以块为单位读取进来,掐头去尾,拼接在一起,进一步做分析处理。下面就看一下recv_parse_log_recs是如何做日志分析的。

static ibool recv_parse_log_recs(
ibool store_to_hash
)
{
/* local variables … */
/* 一个大的循环,连续处理恢复缓冲区中的日志内容,
直到处理完,或者剩下的不是一个完整的MTR为止 */
loop:
/* 当前日志缓冲区中,日志的开始位置 */
ptr = recv_sys->buf + recv_sys->recovered_offset;
/* 当前日志缓冲区中,日志的结束位置 */
end_ptr = recv_sys->buf + recv_sys->len;
if (ptr == end_ptr) {
return(FALSE);
}

/* MLOG_SINGLE_REC_FLAG表示的是,当前日志所对应的MTR,只写了这一条日志,
所以这里就作为特殊情况特别处理了。一般情况下,初始化一个页面,或者创建
一个页面等,属于这种情况,在写日志的时候,会在日志头中加上这个标志 */
single_rec = (ulint)*ptr & MLOG_SINGLE_REC_FLAG;
if (single_rec || *ptr == MLOG_DUMMY_RECORD) {
/* The mtr only modified a single page, or this is a file op */
old_lsn = recv_sys->recovered_lsn;

/* 如注释所述:Try to parse a log record, fetching its type, space id,
page no, and a pointer to the body of the log record */
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);

/* 更新进度 */
recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = new_recovered_lsn;

if (type == MLOG_DUMMY_RECORD) {
/* Do nothing */
} else if (!store_to_hash) {
/* In debug checking, update a replicate page
according to the log record, and check that it
becomes identical with the original page */
} else if (type == MLOG_FILE_CREATE || type == MLOG_FILE_CREATE2
|| type == MLOG_FILE_RENAME || type == MLOG_FILE_DELETE) {
/* In normal mysqld crash recovery we do not try to
replay file operations */
} else {
/* 将分析出来的日志信息存到一个HASH表中,又是一层缓存,
这是第三层。后面可以了解HASH表的管理方法 */
recv_add_to_hash_table(type, space, page_no, body, ptr + len, old_lsn,
recv_sys->recovered_lsn);
}
} else {
/* 与上面相反的是,这里表示的是,一个MTR,
包括多个日志记录,所以这里需要一个个地去分析处理 */
total_len = 0;
n_recs = 0;

/* 这里很关键,在前面介绍的日志记录类型中,已经提到过关于
MLOG_MULTI_REC_END类型的作用,它用来标志一个MTR是不是结束
了。如果找到了这么一条日志,则说明前面的日志是完整的,那这个MTR
就是可以做APPLY的。而MTR,为何被称为mini-transaction,也正是因为
事务所具备的特性是原子性,要么全做,要么全不做,只有找到了
这个标志,才说明这个MTR(物理事务)是完整的,这部分日志才可以被
APPLY。可能有人会问,这个标志有没有可能找不到?答案是有可能。
如果真地找不到,这个日志就不正常,说明这个MTR后面一部分日志
没有被完整地写入日志文件,那这个逻辑事务必定未提交或未提交成功
(如果提交,则与参数innodb_flush_log_at_trx_commit有关),这个MTR
就被忽略了。不过可以肯定的是,这个MTR也是本次数据库启动时,涉及
日志内容中的最后一个MTR了(除非日志文件内容存储或者解析出错了)*/
for (;;) {
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);
/* 没有完整内容了,则返回,不会继续处理了 */
if (len == 0 || recv_sys->found_corrupt_log) {
if (recv_sys->found_corrupt_log) {
recv_report_corrupt_log( ptr, type, space, page_no);
}
return(FALSE);
}
total_len += len;
n_recs++;
ptr += len;
if (type == MLOG_MULTI_REC_END) {
/* Found the end mark for the records */
break;
}
}

/* 能到这里,说明上面已经找到了MTR的结束标志,说明这个MTR是完整的,这样
就会重新处理这部分日志。啊?重新处理?是的,将上面检查过的重新扫描一遍。
不过这次就可以自信满满地去处理每一个日志记录了,而不需要担心日志的
原子性问题了 */

/* 不过,这里的代码是不是可以做一些优化?对于每一个
MTR,都要扫描两遍?这样感觉会对性能造成不小的影响。
至于如何优化,方法总是有的,事在人为,关键是对于那些将Log文件设置得
很大,并且经常出现异常挂机的用户来说,他们有没有对性能的需求。方法总是
跟着需求走的,有了需求,问题自然可以解决。 */
/* Add all the records to the hash table */
ptr = recv_sys->buf + recv_sys->recovered_offset;
for (;;) {
old_lsn = recv_sys->recovered_lsn;
/* 继续分析日志记录,找到类型、表空间ID、页面号及日志内容 */
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);

/* 更新进度 */
recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len);
/* 又见MLOG_MULTI_REC_END,说明已经处理完了这个MTR,则需要继续处理下一个
MTR。结束之后,做一次大循环,直接goto loop,从头再来。*/
if (type == MLOG_MULTI_REC_END) {
/* Found the end mark for the records */
break;
}

/* 将每一个分析出来的日志记录,加入到HASH表中。如此看来,这个HASH
表的管理,就是下一步要研究清楚的内容了。 */
if (store_to_hash) {
recv_add_to_hash_table(type, space, page_no, body, ptr + len,
old_lsn, new_recovered_lsn);
}

ptr += len;
}
}

/* 从头再来,下一个MTR */
goto loop;
}

从上面的代码中可以看出来,InnoDB拿到连续的日志内容之后,以一个mini-transaction(MTR,物理事务)所包含的日志为单位做分析,再将一个MTR中所有的日志记录一个个地分开,存储到HASH表中,以便做APPLY。那么下面再来看加入到HASH表中的操作是如何做的。

static
void
recv_add_to_hash_table(
/*===================*/
byte type, /*!< in: log record type */
ulint space, /*!< in: space id */
ulint page_no, /*!< in: page number */
byte* body, /*!< in: log record body */
byte* rec_end, /*!< in: log record end */
lsn_t start_lsn, /*!< in: start lsn of the mtr */
lsn_t end_lsn) /*!< in: end lsn of the mtr */
{
recv_t* recv;
ulint len;
recv_data_t* recv_data;
recv_data_t** prev_field;
recv_addr_t* recv_addr;

len = rec_end – body;
/* 针对每一条日志记录,都会有一个recv_t的结构来存储它,其包括的成员从下面可以看到 */
recv = static_cast<recv_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_t)));

/* 成员赋值 */
recv->type = type;
recv->len = rec_end – body;
recv->start_lsn = start_lsn;
recv->end_lsn = end_lsn;
/* 这里很重要,可以看到,InnoDB是根据space和page_no获取一个recv_addr。
如果没有recv_addr,就创建一个,被管理到recv_sys->addr_hash的HASH表中,这里
出现了上面提到的HASH表,也就是说,这个HASH表的键值是space, page_no
的组合值,也就是所有日志中对应的表空间页面,都会有这样一个缓存对象 */
recv_addr = recv_get_fil_addr_struct(space, page_no);
if (recv_addr == NULL) {
recv_addr = static_cast<recv_addr_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_addr_t)));
recv_addr->space = space;
recv_addr->page_no = page_no;
recv_addr->state = RECV_NOT_PROCESSED;
UT_LIST_INIT(recv_addr->rec_list);
HASH_INSERT(recv_addr_t, addr_hash, recv_sys->addr_hash,
recv_fold(space, page_no), recv_addr);
recv_sys->n_addrs++;
}

/* 将当前日志记录,放到与之对应的缓存对象中,表示当前日志所要恢复的位置
就是在space, page_no页面中 */
UT_LIST_ADD_LAST(rec_list, recv_addr->rec_list, recv);

/* 存储日志内容时,会用到下面代码 */
prev_field = &(recv->data);

/* 如上面注释所述,将日志记录的内容,即日志体(body)
写入到日志记录recv_t结构对象的data中*/
while (rec_end > body) {
len = rec_end – body;
if (len > RECV_DATA_BLOCK_SIZE) {
len = RECV_DATA_BLOCK_SIZE;
}
recv_data = static_cast<recv_data_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_data_t) + len));
*prev_field = recv_data;
memcpy(recv_data + 1, body, len);
prev_field = &(recv_data->next);
body += len;
}

*prev_field = NULL;
}

上面这段代码让我们明白,InnoDB将每一个日志记录分开之后,存储到了以表空间ID及页面号为键值的HASH表中。也就是说,相同的页面肯定是存储在一起的,并且在同一个页面上的日志是以先后顺序挂在这个对应的HASH节点中的,从而保证了REDO操作的有序性。

从这些代码段中可以看到,缓存到HASH表之后,应该是可以找合适的时机去APPLY了。那什么时候才是合适的时机呢?返回去看到函数recv_scan_log_recs的最后调用了函数recv_apply_hashed_log_recs,那么这就是真正做APPLY的函数了。下面详细看一下它的实现。函数

(作者注:懂了源码,学习MySQL还算个球?)

文章来自微信公众号:DBAce

网友评论comments

发表评论

电子邮件地址不会被公开。 必填项已用*标注

暂无评论

Copyright © 2012-2017 YUNWEIPAI.COM - 运维派 - 粤ICP备14090526号-3
扫二维码
扫二维码
返回顶部