MySQL redo log 恢复原理 | StoneDB 技术分享会 #5

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: redo log 类型innodb 的 redo log 是带有逻辑意义的物理日志:物理指的是 redo log 是针对某一个页来说的,每条 redo log 都会有 Type、Space ID、Page Number 等信息,如下图所示;逻辑指的是一条 redo log 中可能描述的不是在页面上的某个偏移量的位置上写入若干个字节的数据,而是描述在页面上插入或者删除一条什么样的记录。redo log 的通用结构为

redo log 类型
innodb 的 redo log 是带有逻辑意义的物理日志:物理指的是 redo log 是针对某一个页来说的,每条 redo log 都会有 Type、Space ID、Page Number 等信息,如下图所示;逻辑指的是一条 redo log 中可能描述的不是在页面上的某个偏移量的位置上写入若干个字节的数据,而是描述在页面上插入或者删除一条什么样的记录。
redo log 的通用结构为

Type (1) + Space ID (4) + Page Number (4) + Body
Type 的最高位是一个 Single Record Flag 标志位,如果为 1,表示该 redo log 单独构成一个 mtr。
redo log 根据作用的对象,又可以分为作用于 Page 的 redo log,作用于 space 的 redo log 和提供额外信息的 redo log。

作用于 page 的 redo log
大多数的 redo log 属于这一类别,常见的有 MLOG_1BYTE、MLOG_2BYTES、MLOG_4BYTES、MLOG_8BYTES、MLOG_REC_INSERT、MLOG_REC_CLUST_DELETE_MARK、MLOG_REC_UPDATE_IN_PLACE 等。其中 MLOG_1BYTE、MLOG_2BYTES、MLOG_4BYTES、MLOG_8BYTES 描述了在页面的某个偏移量处写入若干个字节的数据;MLOG_REC_INSERT 描述了在页面上插入一条记录;MLOG_REC_CLUST_DELETE_MARK 描述了在聚簇索引的页面上删除一条记录(用户线程删除的操作只会打 delete 标记,物理删除的操作由 purge 线程来做);MLOG_REC_UPDATE_IN_PLACE 描述了在聚簇索引的页面上原地更新一条记录(即修改的是非索引列的字段,二级索引上的更新不会产生该条日志,因为二级索引上的记录没有版本链,所以更新操作产生的 redo log 为 MLOG_REC_CLUST_DELETE_MARK + MLOG_REC_INSERT)。

MLOG_REC_INSERT
MLOG_REC_INSERT 类型的 redo log body 部分的格式为

version (1): 版本信息
flag (1)
n (2): 字段的数量
inst_cols (2)
n_uniq (2): 主键的数量
n个字段的长度 (n * 2)
offset (2): 前一条记录在页面中的偏移量
end_seg_len (compressed): 从mismatch_index开始的记录长度,最低位是标志位
info_and_status_bits (1)
origin_offset (compressed): record header的长度
mismatch_index (compressed): 和前一个记录相比第一个不一样的位置
data (end_seg_len >> 1): 该redo log对应的记录从mismatch_index开始的数据
可见,MLOG_REC_INSERT 类型的 redo log 进行了前缀压缩

MLOG_REC_CLUST_DELETE_MARK
version (1): 版本信息
flag (1)
n (2): 字段的数量
inst_cols (2)
n_uniq (2): 主键的数量
n个字段的长度 (n * 2)
flags (1)
val (1): 设置还是取消delete flag
pos (compressed): trx_id在记录中的偏移量
roll_ptr (7)
trx_id (compressed)
offset (2): 记录origin offset的位置在页面中的偏移量

MLOG_REC_UPDATE_IN_PLACE
version (1): 版本信息
flag (1)
n (2): 字段的数量
inst_cols (2)
n_uniq (2): 主键的数量
n个字段的长度 (n * 2)
flags (1)
pos (compressed): trx_id在记录中的偏移量
roll_ptr (7)
trx_id (compressed)
rec_offset (2): 记录在页面中的偏移量
info_bits (1)
n_fields (compressed): 修改的字段的数量
对n_fields个修改字段的描述
field_no (compressed): 字段的编号
len (compressed): 字段的长度
data (len): 数据

作用于 space 的 redo log
这类 redo log 描述的是针对一个 space 文件的修改,由于这类文件不是 write ahead 的,而是在文件操作后才记录的,所以在恢复的过程中只会对于文件的状态做一些检查。这类 rede log 不是本文的重点,在后续不再赘述。

提供额外信息的 redo log
这一类的 redo log 主要指的是 MLOG_MULTI_REC_END,只由一个字节的 Type 构成,用于标识一个 mini transaction(简称 mtr)的结尾。

recovery 原理
innodb 的 recovery 从 innodb 启动的时候开始执行,大概流程如下:
1、从 ib_logfile 文件的 header 中找到 checkpoint lsn,作为 recovery 的起点
2、每次从 ib_logfile 文件中读取 64KB 的 redo log 到内存中
3、将每个 log block 的 header 和 trail 去掉后,拼出一份连续的日志
4、以 mtr 为单位进行解析
4.1、判断 MLOG_SINGLE_REC_FLAG 标志位,如果一个 mtr 只由单条日志构成,直接解析后放入哈希表;
4.2、如果一个 mtr 由多条日志构成,需要先找到 MLOG_MULTI_REC_END 类型的日志,确定 mtr 的终点,并加入缓存中,然后将缓存中所有的日志都放入哈希表中
5、将哈希表中的 redo log 进行重放
note:这里不直接在解析的时候回放,而是插入哈希表中回放的好处是:可能会有很多 redo log 作用在同一个 page 上,将这些 redo log 使用一次 IO 进行重放,可以加快重放的速度。该哈希表包括两层,第一层以 space_id 为 key,第二层以 page_no 为 key。
调用栈如下所示(下面的源码基于 MySQL8.0.30 版本)

// storage/innobase/srv/srv0start.cc
srv_start
// 从系统表的第一个页中获取flushed_lsn
// 如果是正常shutdown的话,会做一次同步的全量checkpoint,会在系统表的第一个页中写入checkpoint的lsn
srv_sys_space.open_or_create(false, create_new_db, &sum_of_new_sizes, &flushed_lsn);
read_lsn_and_check_flags(flush_lsn);
it->validate_first_page(it->m_space_id, flushed_lsn, false);
*flush_lsn = mach_read_from_8(m_first_page + FIL_PAGE_FILE_FLUSH_LSN);

recv_recovery_from_checkpoint_start(*log_sys, flushed_lsn);
// 每个ib_logfile文件有2KB的header,在header的第2个log block和第4个log block中的8字节偏移量处分别存有checkpoint1和checkpoint2
// 当checkpoint_no为偶数时,写入checkpoint1,为奇数时,写入checkpoint2
// 遍历所有的ib_logfile文件,分别从其header中取出两个checkpoint lsn,取最大值返回
// note: 其实在第一个ib_logfile中寻找checkpoint lsn即可,因为做checkpoint的时候只会往第一个ib_logfile中写入
Log_checkpoint_location checkpoint;
recv_find_max_checkpoint(log, checkpoint)

// 从checkpoint lsn开始解析redo log并且apply
recv_recovery_begin
  recv_read_log_seg
  recv_scan_log_recs
    recv_parse_log_recs
      recv_single_rec
        recv_parse_log_rec
          mlog_parse_initial_log_record
          recv_parse_or_apply_log_rec_body
      recv_multi_rec
        recv_parse_log_rec
          mlog_parse_initial_log_record
          recv_parse_or_apply_log_rec_body

// 将哈希表中的redo log进行重放
recv_apply_hashed_log_recs
下面对从 recv_recovery_begin 开始的流程进行详细阐述,在解析 redo log 的时候以解析 MLOG_REC_INSERT 类型的 redo log 为例进行分析。为了突出主干,对代码做了简化。
innodb 将解析和重放的逻辑是写在一起的,当传入的 block 为空时,只解析不重放,当传入的 block 非空时,解析并且重放。

recv_recovery_begin
该函数负责循环从 ib_logfile 文件中读取 64KB 的 redo log 到内存中进行解析,并放入哈希表中

// storage/innobase/log/log0recv.cc
static dberr_t recv_recovery_begin(log_t &log, const lsn_t checkpoint_lsn) {

// 初始化recv_sys
recv_sys->len = 0;
...

// checkpoint_lsn向下向512KB对齐
lsn_t start_lsn = ut_uint64_align_down(checkpoint_lsn, OS_FILE_LOG_BLOCK_SIZE);

bool finished = false;

// 循环读取ib_logfile中的内容到
while (!finished) {
// 读取从start_lsn开始的64KB的数据到log.buf中
const lsn_t end_lsn =
recv_read_log_seg(log, log.buf, start_lsn, start_lsn + RECV_SCAN_SIZE);

if (end_lsn == start_lsn) {
  /* This could happen if we crashed just after completing file,
  and before next file has been successfully created. */
  break;
}

dberr_t err;

finished = recv_scan_log_recs(log, max_mem, log.buf, end_lsn - start_lsn,
                              start_lsn, &log.m_scanned_lsn, err);

if (err != DB_SUCCESS) {
  return err;
}

start_lsn = end_lsn;

}

return DB_SUCCESS;
}

recv_read_log_seg
该函数负责从 ib_logfile 文件中读取 64KB 的 redo log 到内存中。

// storage/innobase/log/log0recv.cc
static lsn_t recv_read_log_seg(log_t &log, byte *buf, lsn_t start_lsn,
const lsn_t end_lsn) {

// 找到start_lsn所在的ib_logfile文件
auto file = log.m_files.find(start_lsn);

if (file == log.m_files.end()) {
/ Missing valid file ! /
return start_lsn;
}

do {

os_offset_t source_offset;

// 计算start_lsn在ib_logfile文件中的偏移量
// LOG_FILE_HDR_SIZE + (lsn - file_start_lsn);
source_offset = file->offset(start_lsn);

os_offset_t len = end_lsn - start_lsn;

bool switch_to_next_file = false;

if (source_offset + len > file->m_size_in_bytes) {
  len = file->m_size_in_bytes - source_offset;
  switch_to_next_file = true;
}

// 读取文件
const dberr_t err =
    log_data_blocks_read(file_handle, source_offset, len, buf);

start_lsn += len;
buf += len;

if (switch_to_next_file) {

  // 切换到下一个文件
  ...

}

} while (start_lsn != end_lsn);

return end_lsn;
}

// 每个ib_logfile文件的header中记录有该文件起始的file_start_lsn
os_offset_t offset(lsn_t lsn)
os_offset_t offset(lsn_t lsn, lsn_t file_start_lsn)
return LOG_FILE_HDR_SIZE + (lsn - file_start_lsn);

recv_scan_log_recs
该函数先将每个 log block 的 header 和 trail 去掉后,拼出一份连续的日志,然后以 mtr 为单位进行解析

struct Log_data_block_header {
...

/* Offset up to which this block has data inside, computed from the
beginning of the block.
/
// 该log block中前m_data_len个字节是有内容的
uint16_t m_data_len;

/* Offset to the first mtr starting in this block, or 0 if there is no
mtr starting in this block.
/
// 该log block中第一个从该block中开始的mtr的起始位置
uint16_t m_first_rec_group;
};

static bool recv_scan_log_recs(log_t &log,
size_t max_memory, const byte buf, size_t len,
lsn_t start_lsn, lsn_t
read_upto_lsn,
dberr_t &err) {

const byte *log_block = buf;
lsn_t scanned_lsn = start_lsn;
bool finished = false;
bool more_data = false;

// 每个log block有header和trail,导致跨block的日志是不连续的,不能直接解析
// 所以需要先将每个block的header和trail去掉,将所有block的主体内容拼起来
do {

// 解析log block header
Log_data_block_header block_header;
log_data_block_header_deserialize(log_block, block_header);

...

const auto data_len = block_header.m_data_len;

...

// 如果解析redo log的起点位置还没确定并且存在mtr从该block中开始,就确定解析的起点
if (!recv_sys->parse_start_lsn && block_header.m_first_rec_group > 0) {

  recv_sys->parse_start_lsn = scanned_lsn + block_header.m_first_rec_group;

  if (recv_sys->parse_start_lsn < recv_sys->checkpoint_lsn) {
    recv_sys->bytes_to_ignore_before_checkpoint =
        recv_sys->checkpoint_lsn - recv_sys->parse_start_lsn;
  }

  recv_sys->scanned_lsn = recv_sys->parse_start_lsn;
  recv_sys->recovered_lsn = recv_sys->parse_start_lsn;
}

scanned_lsn += data_len;

if (scanned_lsn > recv_sys->scanned_lsn) {

  // buf空间不够用,扩容
  if (recv_sys->len + 4 * OS_FILE_LOG_BLOCK_SIZE >= recv_sys->buf_len) {
    recv_sys_resize_buf();
  }

  if (!recv_sys->found_corrupt_log) {
    // 将该log block去掉header和trail后接到recv_sys->buf的尾部
    more_data = recv_sys_add_to_parsing_buf(log_block, scanned_lsn);
  }

  recv_sys->scanned_lsn = scanned_lsn;
}

// 该log block没有满,那么解析redo log的终点就是这个block
if (data_len < OS_FILE_LOG_BLOCK_SIZE) {
  /* Log data for this group ends here */
  finished = true;

  break;

} else {
  log_block += OS_FILE_LOG_BLOCK_SIZE;
}

} while (log_block < buf + len);

if (more_data && !recv_sys->found_corrupt_log) {

// 解析redo log
recv_parse_log_recs();

if (recv_sys->recovered_offset > recv_sys->buf_len / 4) {
  /* Move parsing buffer data to the buffer start */

  recv_reset_buffer();
}

}

return finished;
}

recv_parse_log_recs
该函数判断 MLOG_SINGLE_REC_FLAG 标志位,根据一个 mtr 是由一条日志组成还是多条日志组成,分开处理。

static void recv_parse_log_recs() {
ut_ad(recv_sys->parse_start_lsn != 0);

// 解析redo log以mtr为基本单位
for (;;) {
byte *ptr = recv_sys->buf + recv_sys->recovered_offset;

byte *end_ptr = recv_sys->buf + recv_sys->len;

if (ptr == end_ptr) {
  return;
}

bool single_rec;

switch (*ptr) {
  case MLOG_DUMMY_RECORD:
    single_rec = true;
    break;
  default:
    // 解析Type最高位的标志位,看该mtr是由单条redo log构成还是多条redo log构成
    single_rec = !!(*ptr & MLOG_SINGLE_REC_FLAG);
}

if (single_rec) {
  if (recv_single_rec(ptr, end_ptr)) { // 单条redo log构成的mtr的解析入口
    return;
  }

} else if (recv_multi_rec(ptr, end_ptr)) { // 多条redo log构成的mtr的解析入口
  return;
}

}
}

recv_single_rec
单条 redo log 构成的 mtr 的解析,将单条 redo log 解析后插入到哈希表中。

static bool recv_single_rec(byte ptr, byte end_ptr) {

lsn_t old_lsn = recv_sys->recovered_lsn;

byte *body;
mlog_id_t type;
page_no_t page_no;
space_id_t space_id;

// 解析单条redo log
ulint len =
recv_parse_log_rec(&type, ptr, end_ptr, &space_id, &page_no, &body);

lsn_t new_recovered_lsn;

new_recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len);

if (new_recovered_lsn > recv_sys->scanned_lsn) {
/ The log record filled a log block, and we
require that also the next log block should
have been scanned in
/

return true;

}

...

recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = new_recovered_lsn;

if (recv_recovery_on) {

// 将redo log加入到哈希表中
// 不直接重放的原因是可能会有很多redo log作用在同一个page上,将这些redo log使用一次IO进行重放,可以加快重放的速度
// 哈希表包括两层,第一层以space_id为key,第二层以page_no为key
recv_add_to_hash_table(type, space_id, page_no, body, ptr + len,
                             old_lsn, recv_sys->recovered_lsn);

}

return false;
}

recv_multi_rec
多条 redo log 构成的 mtr 的解析。
先确定 mtr 的重点,并将解析好的 redo log 加入缓存中,遍历该 mtr 中所有的 redo log,从缓存中取出后插入到哈希表中。

static bool recv_multi_rec(byte ptr, byte end_ptr) {

ulint n_recs = 0;
ulint total_len = 0;

// 先找到mtr的终点,即MLOG_MULTI_REC_END类型的记录
for (;;) {
mlog_id_t type = MLOG_BIGGEST_TYPE;
byte *body;
page_no_t page_no = 0;
space_id_t space_id = 0;

ulint len =
    recv_parse_log_rec(&type, ptr, end_ptr, &space_id, &page_no, &body);

// 将部分解析的redo log缓存起来
recv_sys->save_rec(n_recs, space_id, page_no, type, body, len);

total_len += len;
++n_recs;

ptr += len;

if (type == MLOG_MULTI_REC_END) {
  break;
}

}

lsn_t new_recovered_lsn =
recv_calc_lsn_on_data_add(recv_sys->recovered_lsn, total_len);

// 重置ptr的位置,开始扫第二遍
ptr = recv_sys->buf + recv_sys->recovered_offset;

for (ulint i = 0; i < n_recs; i++) {

lsn_t old_lsn = recv_sys->recovered_lsn;

space_id_t space_id = 0;
page_no_t page_no = 0;

mlog_id_t type = MLOG_BIGGEST_TYPE;

byte *body = nullptr;
size_t len = 0;

// 从第一遍扫的缓存中取出一条redo log
recv_sys->get_saved_rec(i, space_id, page_no, type, body, len);

recv_sys->recovered_offset += len;

recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len);

if (recv_recovery_on) {
  // 将redo log加入到哈希表中
  recv_add_to_hash_table(type, space_id, page_no, body, ptr + len,
                               old_lsn, new_recovered_lsn);
}

ptr += len;

}

return false;
}

recv_parse_log_rec
该函数负责对单条 redo log 日志进行解析,先解析 Type、Space ID、Page Number,再解析 body

static ulint recv_parse_log_rec(mlog_id_t type, byte ptr, byte end_ptr,
space_id_t
space_id, page_no_t page_no,
byte *
body) {

byte *new_ptr;

*body = nullptr;

switch (ptr) {
case MLOG_MULTI_REC_END:
case MLOG_DUMMY_RECORD:
page_no = FIL_NULL;
space_id = SPACE_UNKNOWN; type = static_cast(*ptr);
return 1;

...

}

// 解析Type、Space ID、Page Number
new_ptr =
mlog_parse_initial_log_record(ptr, end_ptr, type, space_id, page_no);

*body = new_ptr;

if (new_ptr == nullptr) {
return 0;
}

// 解析body部分
new_ptr = recv_parse_or_apply_log_rec_body(
type, new_ptr, end_ptr, space_id, *page_no, nullptr, nullptr,
new_ptr - ptr, recv_sys->recovered_lsn);

if (new_ptr == nullptr) {
return 0;
}

return new_ptr - ptr;
}

mlog_parse_initial_log_record
该函数负责解析 Type、Space ID、Page Number

// storage/innobase/mtr/mtr0log.cc
byte mlog_parse_initial_log_record(
const byte
ptr, /!< in: buffer /
const byte end_ptr, /!< in: buffer end /
mlog_id_t
type, /!< out: log record type: MLOG_1BYTE, ... /
space_id_t space, /!< out: space id /
page_no_t
page_no) /!< out: page number /
{
if (end_ptr < ptr + 1) {
return (nullptr);
}

// 解析Type
type = (mlog_id_t)((ulint)ptr & ~MLOG_SINGLE_REC_FLAG);
ut_ad(*type <= MLOG_BIGGEST_TYPE);

ptr++;

if (end_ptr < ptr + 2) {
return (nullptr);
}

// 解析Space ID
*space = mach_parse_compressed(&ptr, end_ptr);

if (ptr != nullptr) {
// 解析Page Number
*page_no = mach_parse_compressed(&ptr, end_ptr);
}

return (const_cast(ptr));
}

recv_parse_or_apply_log_rec_body
该函数负责解析 body,枚举所有的 type 类型,分别进行处理。
这里以 MLOG_REC_INSERT 的日志为例,会先解析字段数量、主键数量、字段长度等信息,构建出索引字典,然后解析剩余的部分,构建出完整的记录,最后插入对应的页中。

// storage/innobase/log/log0recv.cc
static byte recv_parse_or_apply_log_rec_body(
mlog_id_t type, byte
ptr, byte end_ptr, space_id_t space_id,
page_no_t page_no, buf_block_t
block, mtr_t *mtr, ulint parsed_bytes,
lsn_t start_lsn) {

...

dict_index_t *index = nullptr;

...

// 这里枚举了所有的redo log类型
switch (type) {

...

case MLOG_REC_INSERT:
  // 解析字段数量、主键数量、字段长度等信息,构建出索引字典
  if (nullptr != (ptr = mlog_parse_index(ptr, end_ptr, &index))) {

    // 解析剩余的部分,构建出完整的记录,插入到对应的页中
    ptr = page_cur_parse_insert_rec(false, ptr, end_ptr, block, index, mtr);
  }
  break;

...

}

if (index != nullptr) {
dict_table_t *table = index->table;

dict_mem_index_free(index);
dict_mem_table_free(table);

}

return ptr;
}

mlog_parse_index
该函数负责解析字段的数量,主键的数量和每个字段的长度,构建索引字典

byte mlog_parse_index(byte ptr, const byte end_ptr, dict_index_t *index) {

/ Read the 1 byte for index log version /
uint8_t index_log_version = 0;
ptr = parse_index_log_version(ptr, end_ptr, index_log_version);

/ Read the 1 byte flag /
uint8_t flag = 0;
ptr = parse_index_flag(ptr, end_ptr, flag);

/ Read n and n_uniq /
// 解析字段的数量n和主键的数量n_uniq
uint16_t n = 0;
uint16_t n_uniq = 0;
uint16_t inst_cols = 0;
ptr = parse_index_column_counts(ptr, end_ptr, is_comp, is_versioned,
is_instant, n, n_uniq, inst_cols);

/ Create a dummy dict_table_t /
dict_table_t *table =
dict_mem_table_create(RECOVERY_INDEX_TABLE_NAME, DICT_HDR_SPACE, n, 0, 0,
is_comp ? DICT_TF_COMPACT : 0, 0);

/ Create a dummy dict_index_t /
dict_index_t *ind =
dict_mem_index_create(RECOVERY_INDEX_TABLE_NAME,
RECOVERY_INDEX_TABLE_NAME, DICT_HDR_SPACE, 0, n);

ind->table = table;
ind->n_uniq = (unsigned int)n_uniq;
if (n_uniq != n) {
ind->type = DICT_CLUSTERED;
}

/ Read each index field info /
// 解析每个字段的长度,填充index的feild信息
ptr = parse_index_fields(ptr, end_ptr, n, n_uniq, is_versioned, ind, table);
if (ptr == nullptr) {
*index = ind;
return ptr;
}

...

*index = ind;
return (ptr);
}

parse_index_fields
该函数负责解析每个字段的长度,填充索引的 field 列表

static byte parse_index_fields(byte ptr, const byte end_ptr, uint16_t n,
uint16_t n_uniq, bool is_versioned,
dict_index_t
&ind, dict_table_t *&table) {

for (size_t i = 0; i < n; i++) {

uint16_t len = 0;
// 读取字段的长度信息
ptr = read_2_bytes(ptr, end_ptr, len);

// 这里构建出来的field字段的类型并不是准确的,只能区分出是变长还是定长,因为redo log中只有字段长度相关的信息,并没有类型相关的信息
dict_mem_table_add_col(
    table, nullptr, nullptr,
    ((len + 1) & 0x7fff) <= 1 ? DATA_BINARY : DATA_FIXBINARY,
    len & 0x8000 ? DATA_NOT_NULL : 0, len & 0x7fff, true, phy_pos, v_added,
    v_dropped);

dict_index_add_col(ind, table, table->get_col(i), 0, true);

}

// 加上trx_id和roll_ptr的列
dict_table_add_system_columns(table, table->heap);

/ Identify DB_TRX_ID and DB_ROLL_PTR in the index. /
// index中字段的顺序和物理记录保持一致
// 如果是聚簇索引,trx_id和roll_ptr放在主键的后面
if (is_versioned || (n_uniq != n)) {
size_t i = 0;
i = DATA_TRX_ID - 1 + n_uniq;
ind->fields[i].col = &table->cols[n + DATA_TRX_ID];
ind->fields[i].col->set_phy_pos(table->cols[i].get_phy_pos());

i = DATA_ROLL_PTR - 1 + n_uniq;
ind->fields[i].col = &table->cols[n + DATA_ROLL_PTR];
ind->fields[i].col->set_phy_pos(table->cols[i].get_phy_pos());

}

return ptr;
}

page_cur_parse_insert_rec
由于 MLOG_REC_INSERT 类型的 redo log 里做了压缩,只记录了和上一条记录不一样的部分,所以需要先解析出上一条记录在页面中的偏移量、待插入记录和上一条记录第一个不相同的字节 mismatch_index 和待插入记录从 mismatch_index 开始的记录长度 eng_seg_len,然后定位到上一条记录,取出前 mismatch_index 个字节,并从 redo log 中解析出待插入记录从 mismatch_index 开始的部分,那么待插入记录就是两部分拼起来,最后插入到 B + 树中。

// storage/innobase/page/page0cur.cc
byte page_cur_parse_insert_rec(
bool is_short, /
!< in: true if short inserts /
const byte
ptr, /!< in: buffer /
const byte end_ptr, /!< in: buffer end /
buf_block_t
block, /!< in: page or NULL /
dict_index_t index, /!< in: record descriptor /
mtr_t
mtr) /!< in: mtr or NULL /
{
ulint origin_offset = 0; / remove warning /
ulint end_seg_len;
ulint mismatch_index = 0; / remove warning /
page_t page;
rec_t
cursor_rec{nullptr};
byte buf1[1024];
// buf描述待插入记录
byte buf;
const byte
ptr2 = ptr;
ulint info_and_status_bits = 0; / remove warning /
page_cur_t cursor;
mem_heapt *heap = nullptr;
ulint offsets
[REC_OFFS_NORMALSIZE];
// offsets描述每个字段在物理记录中的偏移量
ulint *offsets = offsets
;
// offsets[0]存offsets数组占用的内存大小
rec_offsinit(offsets);

page = block ? buf_block_get_frame(block) : nullptr;

ulint offset;
// 前一条记录在页面中的偏移量
offset = mach_read_from_2(ptr);
ptr += 2;

if (page != nullptr) cursor_rec = page + offset;

// 该redo log对应的记录和前一条记录不一样的部分的长度,最低位是一个标志位
end_seg_len = mach_parse_compressed(&ptr, end_ptr);

info_and_status_bits = mach_read_from_1(ptr);
ptr++;

// 该redo log对应的record header的长度
origin_offset = mach_parse_compressed(&ptr, end_ptr);

// 和前一个记录相比第一个不一样的位置
mismatch_index = mach_parse_compressed(&ptr, end_ptr);

if (!block) {
return (const_cast(ptr + (end_seg_len >> 1)));
}

...

// end_seg_len的最低位是一个标志位,所以真实的大小还需要除以2
end_seg_len >>= 1;

// 如果buf在栈上分配的内存不够,就从堆上分配进行扩容
if (mismatch_index + end_seg_len < sizeof buf1) {
buf = buf1;
} else {
buf = static_cast(ut::malloc_withkey(UT_NEW_THIS_FILE_PSI_KEY,
mismatch_index + end_seg_len));
}

// 待插入记录 = 前一条记录的前mismatch_index个字节 + 从ptr开始的eng_seg_len个字节
if (mismatch_index) {
ut_memcpy(buf, rec_get_start(cursor_rec, offsets), mismatch_index);
}
ut_memcpy(buf + mismatch_index, ptr, end_seg_len);

...

// 将cursor定位到前一条记录的位置
page_cur_position(cursor_rec, block, &cursor);

// 构建offsets数组,用于描述每个字段在记录中的偏移量
offsets = rec_get_offsets(buf + origin_offset, index, offsets,
ULINT_UNDEFINED, UT_LOCATION_HERE, &heap);

// 插入到B+树中
page_cur_rec_insert(&cursor, buf + origin_offset, index,
offsets, mtr);

if (buf != buf1) {
ut::free(buf);
}

if (UNIV_LIKELY_NULL(heap)) {
mem_heap_free(heap);
}

return (const_cast(ptr + end_seg_len));
}

总结
这篇文章我们介绍了 redo log 的分类,不同种类的 redo log 的结构,并且分析了 redo log 在恢复时的流程相关的源码,欢迎大家关注 StoneDB 的开源代码。

StoneDB 介绍

StoneDB 是石原子科技自主设计研发的国内首款完全兼容于 MySQL 生态的开源 一体化实时 HTAP 数据库产品,具备行列混存、智能索引等核心特性,为 MySQL 数据库提供在线数据实时就近分析服务,能够高效解决 MySQL 数据库在分析型应用场景中面临的能力问题。同时,StoneDB 使用多存储引擎架构的设计,事务引擎具有数据强一致特性,具备完整的事务并发处理能力,使得 StoneDB 可以替代 MySQL 数据库满足在线事务处理场景的需求,使用 MySQL 的用户,通过 StoneDB 可以实现 TP+AP 混合负载,分析性能提升 10 倍以上显著提升,不需要进行数据迁移,也无需与其他 AP 集成,弥补 MySQL 分析领域的空白。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1天前
|
存储 SQL 关系型数据库
【MySQL】主从同步原理、分库分表
【MySQL】主从同步原理、分库分表
10 0
|
1天前
|
存储 算法 关系型数据库
MySQL连接的原理⭐️4种优化连接的手段性能提升240%🚀
MySQL连接的原理⭐️4种优化连接的手段性能提升240%🚀
|
1天前
|
SQL 存储 关系型数据库
MySQL查询原理,看这一篇就够了!
MySQL查询原理,看这一篇就够了!
|
1天前
|
关系型数据库 MySQL 数据管理
MySQL通过 bin-log 恢复从备份点到灾难点之间数据
MySQL通过 bin-log 恢复从备份点到灾难点之间数据
|
1天前
|
DataWorks 关系型数据库 MySQL
DataWorks产品使用合集之在DataWorks中,如何通过PolarDB for MySQL来查看binlog日志
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
31 1
|
1天前
|
存储 关系型数据库 MySQL
MySQL 8 索引原理详细分析
了解索引的详细原则,不仅有助于优化,能把索引搞清楚的,面试中优势也会很突显。 关于数据库优化的话题,V哥觉得还有很多地方可以聊,如果你有兴趣,欢迎关注一起讨论。
MySQL 8 索引原理详细分析
|
1天前
|
SQL 监控 关系型数据库
【MySQL学习】MySQL的慢查询日志和错误日志
【MySQL学习】MySQL的慢查询日志和错误日志
|
1天前
|
关系型数据库 MySQL Linux
【mysql】MySql主从复制,从原理到实践!
【mysql】MySql主从复制,从原理到实践!
30 0
|
1天前
|
关系型数据库 MySQL 数据库
docker MySQL删除数据库时的错误(errno: 39)
docker MySQL删除数据库时的错误(errno: 39)
20 0
|
1天前
|
前端开发 关系型数据库 MySQL
【MySQL × SpringBoot 突发奇想】全面实现流程 · 数据库导出Excel表格文件的接口
【MySQL × SpringBoot 突发奇想】全面实现流程 · 数据库导出Excel表格文件的接口
16 0