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方式。

  1. void trx_start_low(trx_t *trx, bool read_write)
  2. {
  3. trx->state = TRX_STATE_ACTIVE;
  4. if (!trx->read_only && (read_write || trx->ddl_operation)) {
  5. trx_assign_rseg_durable(trx);
  6. }
  7. }
  8. void trx_assign_rseg_durable(trx_t *trx) {
  9. trx->rsegs.m_redo.rseg = srv_read_only_mode ? nullptr : get_next_redo_rseg();
  10. }
  11. trx_rseg_t *get_next_redo_rseg() {
  12. ulong target = srv_undo_tablespaces;
  13. // 从系统表空间分配
  14. if (target == 0) {
  15. return (get_next_redo_rseg_from_trx_sys());
  16. } else {
  17. // 从独立UNDO表空间分配
  18. return (get_next_redo_rseg_from_undo_spaces(target));
  19. }
  20. }
  21. trx_rseg_t *get_next_redo_rseg_from_undo_spaces(...)
  22. {
  23. ulong target_rollback_segments = srv_rollback_segments;
  24. static ulint rseg_counter = 0;
  25. ulint current = rseg_counter;
  26. // rseg_counter表示下一次要分配的回滚段编号
  27. // 然后根据该编号来计算space id和segment id
  28. os_atomic_increment_ulint(&rseg_counter, 1);
  29. while (rseg == nullptr) {
  30. ulint window =
  31. current % (target_rollback_segments * target_undo_tablespaces);
  32. ulint spaces_slot = window % target_undo_tablespaces;
  33. ulint rseg_slot = window / target_undo_tablespaces;
  34. current++;
  35. undo_space = undo::spaces->at(spaces_slot);
  36. rseg = undo_space->rsegs()->at(rseg_slot);
  37. rseg->trx_ref_count++;
  38. }
  39. return (rseg);
  40. }

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

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

分配undo log

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

  1. dberr_t trx_undo_assign_undo(
  2. trx_t *trx,
  3. trx_undo_ptr_t *undo_ptr,
  4. ulint type)
  5. {
  6. // 分配的回滚段
  7. rseg = undo_ptr->rseg;
  8. // 首先尝试从回滚段缓存中分配
  9. undo = trx_undo_reuse_cached(trx, rseg, type, trx->id, trx->xid, &mtr);
  10. if (undo == NULL) {
  11. err = trx_undo_create(trx, rseg, type, trx->id, trx->xid, &undo, &mtr);
  12. }
  13. // 加分配的回滚段根据其类型(insert/update)加入至特定链表
  14. if (type == TRX_UNDO_INSERT) {
  15. UT_LIST_ADD_FIRST(rseg->insert_undo_list, undo);
  16. // 该事务后续所有的insert涉及的undo log都会使用这个
  17. undo_ptr->insert_undo = undo;
  18. } else {
  19. UT_LIST_ADD_FIRST(rseg->update_undo_list, undo);
  20. // 该事务后续所有的update涉及的undo log都会使用这个
  21. undo_ptr->update_undo = undo;
  22. }
  23. }

从回滚段缓存中分配

  1. trx_undo_t *trx_undo_reuse_cached(...)
  2. {
  3. if (type == TRX_UNDO_INSERT) {
  4. undo = UT_LIST_GET_FIRST(rseg->insert_undo_cached);
  5. UT_LIST_REMOVE(rseg->insert_undo_cached, undo);
  6. } else {
  7. undo = UT_LIST_GET_FIRST(rseg->update_undo_cached);
  8. UT_LIST_REMOVE(rseg->update_undo_cached, undo);
  9. }
  10. undo_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
  11. undo->page_size, mtr);
  12. if (type == TRX_UNDO_INSERT) {
  13. offset = trx_undo_insert_header_reuse(undo_page, trx_id, mtr);
  14. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
  15. } else {
  16. // 被cache的update undo log,其内容可能尚未被purge
  17. // 因而我们不能直接复用,需要在其后创建一个新的update undo log
  18. // 这也导致了一个undo page中存在多个update undo log情况
  19. offset = trx_undo_header_create(undo_page, trx_id, mtr);
  20. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
  21. }
  22. trx_undo_mem_init_for_reuse(undo, trx_id, xid, offset);
  23. return (undo);
  24. }

‌使用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中执行:

  1. dberr_t trx_undo_create(...)
  2. {
  3. rseg_header =
  4. trx_rsegf_get(rseg->space_id, rseg->page_no, rseg->page_size, mtr);
  5. // 创建undo segment, undo page为segment的第一个page
  6. err = trx_undo_seg_create(rseg, rseg_header, type, &id, &undo_page, mtr);
  7. page_no = page_get_page_no(undo_page);
  8. // 创建undo log header
  9. offset = trx_undo_header_create(undo_page, trx_id, mtr);
  10. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
  11. *undo = trx_undo_mem_create(rseg, id, type, trx_id, xid, page_no, offset);
  12. return (err);
  13. }

‌我们在前面的章节“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进行扩充。

  1. dberr_t trx_undo_report_row_operation(...)
  2. {
  3. // 省略分配undo逻辑
  4. page_no = undo->last_page_no;
  5. undo_block = buf_page_get_gen(
  6. page_id_t(undo->space, page_no), undo->page_size, RW_X_LATCH...);
  7. do {
  8. page_t *undo_page;
  9. ulint offset;
  10. undo_page = buf_block_get_frame(undo_block);
  11. // 开始正式在undo log page内写入旧版本记录内容
  12. switch (op_type) {
  13. case TRX_UNDO_INSERT_OP:
  14. offset = trx_undo_page_report_insert(undo_page, trx, index, clust_entry, &mtr);
  15. break;
  16. default:
  17. offset = trx_undo_page_report_modify(undo_page, trx, index, rec, offsets,
  18. update, cmpl_info, clust_entry, &mtr);
  19. }
  20. // offset 返回值为0表示失败
  21. if (UNIV_UNLIKELY(offset == 0)) {
  22. // 不确定这是在干什么
  23. if (!trx_undo_erase_page_end(undo_page, &mtr)) { ... }
  24. } else {
  25. // 返回值不为0表示成功,此时直接返回即可,在这里不作讨论
  26. ...
  27. return (DB_SUCCESS);
  28. }
  29. // 走到这里意味着空间不足,我们需要扩充一个新page
  30. // 然后尝试用这个新page继续写入
  31. // 调用函数trx_undo_add_page
  32. undo_block = trx_undo_add_page(trx, undo, undo_ptr, &mtr);
  33. page_no = undo->last_page_no;
  34. } while (undo_block != NULL);
  35. ...
  36. }
  37. buf_block_t *trx_undo_add_page(...)
  38. {
  39. rseg = undo_ptr->rseg;
  40. // UNDO LOG SEGMENT header page
  41. header_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
  42. undo->page_size, mtr);
  43. if (!fsp_reserve_free_extents(&n_reserved, undo->space, 1, FSP_UNDO, mtr)) {
  44. return (NULL);
  45. }
  46. // 分配新的空闲page其no为undo->top_page_no + 1
  47. new_block = fseg_alloc_free_page_general(
  48. TRX_UNDO_SEG_HDR + TRX_UNDO_FSEG_HEADER + header_page,
  49. undo->top_page_no + 1, FSP_UP, TRUE, mtr, mtr);
  50. fil_space_release_free_extents(undo->space, n_reserved);
  51. undo->last_page_no = new_block->page.id.page_no();
  52. new_page = buf_block_get_frame(new_block);
  53. // 初始化新分配的page
  54. trx_undo_page_init(new_page, undo->type, mtr);
  55. // 将新分配的page加入至UNDO SEGMENT PAGE的page list中
  56. flst_add_last(header_page + TRX_UNDO_SEG_HDR + TRX_UNDO_PAGE_LIST,
  57. new_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_NODE, mtr);
  58. undo->size++;
  59. rseg->curr_size++;
  60. return (new_block);
  61. }