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. }
9. void trx_assign_rseg_durable(trx_t *trx) {
10. trx->rsegs.m_redo.rseg = srv_read_only_mode ? nullptr : get_next_redo_rseg();
11. }
13. trx_rseg_t *get_next_redo_rseg() {
14. ulong target = srv_undo_tablespaces;
16. // 从系统表空间分配
17. if (target == 0) {
18. return (get_next_redo_rseg_from_trx_sys());
19. } else {
20. // 从独立UNDO表空间分配
21. return (get_next_redo_rseg_from_undo_spaces(target));
22. }
23. }
25. trx_rseg_t *get_next_redo_rseg_from_undo_spaces(...)
26. {
27. ulong target_rollback_segments = srv_rollback_segments;
28. static ulint rseg_counter = 0;
29. ulint current = rseg_counter;
30. // rseg_counter表示下一次要分配的回滚段编号
31. // 然后根据该编号来计算space id和segment id
32. os_atomic_increment_ulint(&rseg_counter, 1);
33. while (rseg == nullptr) {
34. ulint window =
35. current % (target_rollback_segments * target_undo_tablespaces);
36. ulint spaces_slot = window % target_undo_tablespaces;
37. ulint rseg_slot = window / target_undo_tablespaces;
39. current++;
41. undo_space = undo::spaces->at(spaces_slot);
43. rseg = undo_space->rsegs()->at(rseg_slot);
44. rseg->trx_ref_count++;
45. }
46. return (rseg);
47. }
分配成功时,递增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;
9. // 首先尝试从回滚段缓存中分配
10. undo = trx_undo_reuse_cached(trx, rseg, type, trx->id, trx->xid, &mtr);
11. if (undo == NULL) {
12. err = trx_undo_create(trx, rseg, type, trx->id, trx->xid, &undo, &mtr);
13. }
15. // 加分配的回滚段根据其类型(insert/update)加入至特定链表
16. if (type == TRX_UNDO_INSERT) {
17. UT_LIST_ADD_FIRST(rseg->insert_undo_list, undo);
18. // 该事务后续所有的insert涉及的undo log都会使用这个
19. undo_ptr->insert_undo = undo;
20. } else {
21. UT_LIST_ADD_FIRST(rseg->update_undo_list, undo);
22. // 该事务后续所有的update涉及的undo log都会使用这个
23. undo_ptr->update_undo = undo;
24. }
25. }
从回滚段缓存中分配
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. }
11. undo_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
12. undo->page_size, mtr);
13. if (type == TRX_UNDO_INSERT) {
14. offset = trx_undo_insert_header_reuse(undo_page, trx_id, mtr);
15. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
16. } else {
17. // 被cache的update undo log,其内容可能尚未被purge
18. // 因而我们不能直接复用,需要在其后创建一个新的update undo log
19. // 这也导致了一个undo page中存在多个update undo log情况
20. offset = trx_undo_header_create(undo_page, trx_id, mtr);
21. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
22. }
24. trx_undo_mem_init_for_reuse(undo, trx_id, xid, offset);
25. return (undo);
26. }
使用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);
6. // 创建undo segment, undo page为segment的第一个page
7. err = trx_undo_seg_create(rseg, rseg_header, type, &id, &undo_page, mtr);
9. page_no = page_get_page_no(undo_page);
11. // 创建undo log header
12. offset = trx_undo_header_create(undo_page, trx_id, mtr);
14. trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);
16. *undo = trx_undo_mem_create(rseg, id, type, trx_id, xid, page_no, offset);
18. return (err);
19. }
我们在前面的章节“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;
6. undo_block = buf_page_get_gen(
7. page_id_t(undo->space, page_no), undo->page_size, RW_X_LATCH...);
9. do {
10. page_t *undo_page;
11. ulint offset;
12. undo_page = buf_block_get_frame(undo_block);
14. // 开始正式在undo log page内写入旧版本记录内容
15. switch (op_type) {
16. case TRX_UNDO_INSERT_OP:
17. offset = trx_undo_page_report_insert(undo_page, trx, index, clust_entry, &mtr);
18. break;
19. default:
20. offset = trx_undo_page_report_modify(undo_page, trx, index, rec, offsets,
21. update, cmpl_info, clust_entry, &mtr);
22. }
24. // offset 返回值为0表示失败
25. if (UNIV_UNLIKELY(offset == 0)) {
26. // 不确定这是在干什么
27. if (!trx_undo_erase_page_end(undo_page, &mtr)) { ... }
28. } else {
29. // 返回值不为0表示成功,此时直接返回即可,在这里不作讨论
30. ...
31. return (DB_SUCCESS);
32. }
33. // 走到这里意味着空间不足,我们需要扩充一个新page
34. // 然后尝试用这个新page继续写入
35. // 调用函数trx_undo_add_page
36. undo_block = trx_undo_add_page(trx, undo, undo_ptr, &mtr);
37. page_no = undo->last_page_no;
38. } while (undo_block != NULL);
39. ...
40. }
42. buf_block_t *trx_undo_add_page(...)
43. {
44. rseg = undo_ptr->rseg;
46. // UNDO LOG SEGMENT header page
47. header_page = trx_undo_page_get(page_id_t(undo->space, undo->hdr_page_no),
48. undo->page_size, mtr);
50. if (!fsp_reserve_free_extents(&n_reserved, undo->space, 1, FSP_UNDO, mtr)) {
51. return (NULL);
52. }
54. // 分配新的空闲page其no为undo->top_page_no + 1
55. new_block = fseg_alloc_free_page_general(
56. TRX_UNDO_SEG_HDR + TRX_UNDO_FSEG_HEADER + header_page,
57. undo->top_page_no + 1, FSP_UP, TRUE, mtr, mtr);
59. fil_space_release_free_extents(undo->space, n_reserved);
61. undo->last_page_no = new_block->page.id.page_no();
63. new_page = buf_block_get_frame(new_block);
65. // 初始化新分配的page
66. trx_undo_page_init(new_page, undo->type, mtr);
68. // 将新分配的page加入至UNDO SEGMENT PAGE的page list中
69. flst_add_last(header_page + TRX_UNDO_SEG_HDR + TRX_UNDO_PAGE_LIST,
70. new_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_NODE, mtr);
71. undo->size++;
72. rseg->curr_size++;
74. return (new_block);
75. }
