数据库内核月报

数据库内核月报 - 2020 / 08

MySQL · 引擎特性 · INNODB UNDO LOG分配

Author: 江疑

INNODB UNDO LOG分配

本文只描述独立UNDO表空间下的undo log的分配算法以及实现。根据我们之前的UNDO LOG物理格式描述,分配undo log得先分配回滚段,然后再从回滚段内分配undo log。

分配undo回滚段

每个独立UNDO表空间存在若干个(默认128)个回滚段,而每个回滚段又默认存在1024个UNDO LOG SLOT,分配undo log其实质便是在所有UNDO表空间中找到一个空闲的UNDO LOG SLOT。

分配回滚段的工作在函数trx_assign_rseg_durable进行,分配策略是采用round-robin方式。

void trx_start_low(trx_t *trx, bool read_write)
{
  trx->state = TRX_STATE_ACTIVE;
  if (!trx->read_only && (read_write || trx->ddl_operation)) {
    trx_assign_rseg_durable(trx);
  }
}

void trx_assign_rseg_durable(trx_t *trx) {
  trx->rsegs.m_redo.rseg = srv_read_only_mode ? nullptr : get_next_redo_rseg();
}

trx_rseg_t *get_next_redo_rseg() {
  ulong target = srv_undo_tablespaces;

  // 从系统表空间分配
  if (target == 0) {
    return (get_next_redo_rseg_from_trx_sys());
  } else {
    // 从独立UNDO表空间分配
    return (get_next_redo_rseg_from_undo_spaces(target));
  }
}

trx_rseg_t *get_next_redo_rseg_from_undo_spaces(...)
{
  ulong target_rollback_segments = srv_rollback_segments;
  static ulint rseg_counter = 0;
  ulint current = rseg_counter;
  // rseg_counter表示下一次要分配的回滚段编号
  // 然后根据该编号来计算space id和segment id
  os_atomic_increment_ulint(&rseg_counter, 1);
  while (rseg == nullptr) {
    ulint window =
        current % (target_rollback_segments * target_undo_tablespaces);
    ulint spaces_slot = window % target_undo_tablespaces;
    ulint rseg_slot = window / target_undo_tablespaces;

    current++;

    undo_space = undo::spaces->at(spaces_slot);

    rseg = undo_space->rsegs()->at(rseg_slot);
    rseg->trx_ref_count++;
  }
  return (rseg);
}

‌分配成功时,递增rseg->trx_ref_count,保证rseg的表空间不会被truncate。

‌临时表操作不记redo log,最终调用get_next_noredo_rseg函数进行分配;其他情况调用get_next_redo_rseg。

分配undo log

‌一旦分配好回滚段,接下来就是在回滚段内分配undo log了,这在函数trx_undo_assign_undo内完成:

dberr_t trx_undo_assign_undo(
    trx_t *trx,            
    trx_undo_ptr_t *undo_ptr, 
    ulint type)      
{
  // 分配的回滚段
  rseg = undo_ptr->rseg;

  // 首先尝试从回滚段缓存中分配
  undo = trx_undo_reuse_cached(trx, rseg, type, trx->id, trx->xid, &mtr);
  if (undo == NULL) {
    err = trx_undo_create(trx, rseg, type, trx->id, trx->xid, &undo, &mtr);
  }

  // 加分配的回滚段根据其类型(insert/update)加入至特定链表
  if (type == TRX_UNDO_INSERT) {
    UT_LIST_ADD_FIRST(rseg->insert_undo_list, undo);
    // 该事务后续所有的insert涉及的undo log都会使用这个
    undo_ptr->insert_undo = undo;
  } else {
    UT_LIST_ADD_FIRST(rseg->update_undo_list, undo);
    // 该事务后续所有的update涉及的undo log都会使用这个
    undo_ptr->update_undo = undo;
  }
}

从回滚段缓存中分配

trx_undo_t *trx_undo_reuse_cached(...)
{
  if (type == TRX_UNDO_INSERT) {
    undo = UT_LIST_GET_FIRST(rseg->insert_undo_cached);
    UT_LIST_REMOVE(rseg->insert_undo_cached, undo);
  } else {
    undo = UT_LIST_GET_FIRST(rseg->update_undo_cached);
    UT_LIST_REMOVE(rseg->update_undo_cached, undo);
  }

  undo_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
                                undo->page_size, mtr);
  if (type == TRX_UNDO_INSERT) {
    offset = trx_undo_insert_header_reuse(undo_page, trx_id, mtr);
    trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
  } else {
    // 被cache的update undo log,其内容可能尚未被purge
    // 因而我们不能直接复用,需要在其后创建一个新的update undo log
		// 这也导致了一个undo page中存在多个update undo log情况
    offset = trx_undo_header_create(undo_page, trx_id, mtr);
    trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
  }

  trx_undo_mem_init_for_reuse(undo, trx_id, xid, offset);
  return (undo);
}

‌使用cache是为了提升undo log的分配效率。一个undo log在使用完成变得不再有效后便会被释放,一旦满足某些条件,它会被加入到回滚段的undo cache链表,insert 和update undo log有自己独立的链表。

从cache分配就很简单了,只需要从相应类型的缓存链表中取出第一项,然后初始化这个被复用的undo log即可。这里的逻辑比较简单,就不再赘述了。感兴趣的读者请自行研究。

如果undo log类型是update,这时候还要创建一个新的undo log header,而不能复用之前undo log。这是因为:这个被缓存的update undo log可能还在history list中尚未被purge。因而,我们只能在原来的undo page中创建一个新的undo log header,这就导致了每个update undo log page上可能会存在多个undo log,与我之前想象的有所不同。

创建新的undo log

如果无法从缓存中分配undo log,那也只能退化成来实际分配了,在函数trx_undo_create中执行:

dberr_t trx_undo_create(...)
{
  rseg_header =
      trx_rsegf_get(rseg->space_id, rseg->page_no, rseg->page_size, mtr);

  // 创建undo segment, undo page为segment的第一个page
  err = trx_undo_seg_create(rseg, rseg_header, type, &id, &undo_page, mtr);

  page_no = page_get_page_no(undo_page);

  // 创建undo log header
  offset = trx_undo_header_create(undo_page, trx_id, mtr);

  trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);

  *undo = trx_undo_mem_create(rseg, id, type, trx_id, xid, page_no, offset);

  return (err);
}

‌我们在前面的章节“UNDO LOG物理格式”中说过,创建undo log的关键是分配undo segment。它是个独立的段,每个undo segment包含1个header page(第1个undo page)和若干个记录undo日志的undo page。

‌第1个undo page中存储的是元信息: 首先存储的是undo page的元信息,位于TRX_UNDO_PAGE_HDR到TRX_UNDO_SEG_HDR之间。

‌因此,如果理解了undo log的物理格式,上面的过程就非常简单了,这里不作过多描述。‌

UNDO LOG空间不足时如何处理

在函数trx_undo_report_row_operation中,如果出现了已分配的undo log的空间不足以容纳当前记录的老版本,这时候就需要对UNDO LOG进行扩充。

dberr_t trx_undo_report_row_operation(...)
{
  // 省略分配undo逻辑
  page_no = undo->last_page_no;

  undo_block = buf_page_get_gen(
      page_id_t(undo->space, page_no), undo->page_size, RW_X_LATCH...);

  do {
    page_t *undo_page;
    ulint offset;
    undo_page = buf_block_get_frame(undo_block);

    // 开始正式在undo log page内写入旧版本记录内容
    switch (op_type) {
      case TRX_UNDO_INSERT_OP:
        offset = trx_undo_page_report_insert(undo_page, trx, index, clust_entry, &mtr);
        break;
      default:
        offset = trx_undo_page_report_modify(undo_page, trx, index, rec, offsets,
                                             update, cmpl_info, clust_entry, &mtr);
    }

    // offset 返回值为0表示失败
    if (UNIV_UNLIKELY(offset == 0)) {
      // 不确定这是在干什么
      if (!trx_undo_erase_page_end(undo_page, &mtr)) { ... }
    } else {
      // 返回值不为0表示成功,此时直接返回即可,在这里不作讨论
      ...
      return (DB_SUCCESS);
    }
    // 走到这里意味着空间不足,我们需要扩充一个新page
    // 然后尝试用这个新page继续写入
    // 调用函数trx_undo_add_page
    undo_block = trx_undo_add_page(trx, undo, undo_ptr, &mtr);
    page_no = undo->last_page_no;
  } while (undo_block != NULL);
  ...
}

buf_block_t *trx_undo_add_page(...)
{
  rseg = undo_ptr->rseg;

  // UNDO LOG SEGMENT header page
  header_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
                                  undo->page_size, mtr);

  if (!fsp_reserve_free_extents(&n_reserved, undo->space, 1, FSP_UNDO, mtr)) {
    return (NULL);
  }

  // 分配新的空闲page其no为undo->top_page_no + 1
  new_block = fseg_alloc_free_page_general(
      TRX_UNDO_SEG_HDR + TRX_UNDO_FSEG_HEADER + header_page,
      undo->top_page_no + 1, FSP_UP, TRUE, mtr, mtr);

  fil_space_release_free_extents(undo->space, n_reserved);

  undo->last_page_no = new_block->page.id.page_no();

  new_page = buf_block_get_frame(new_block);

  // 初始化新分配的page
  trx_undo_page_init(new_page, undo->type, mtr);

  // 将新分配的page加入至UNDO SEGMENT PAGE的page list中
  flst_add_last(header_page + TRX_UNDO_SEG_HDR + TRX_UNDO_PAGE_LIST,
                new_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_NODE, mtr);
  undo->size++;
  rseg->curr_size++;

  return (new_block);
}