INNODB UNDO LOG写入

我们在之前的章节中提到,UNDO LOG分为INSERT和UPDATE两种类型。接下来我们分别描述这两种记录如何写入UNDO LOG RECORD。

行记录在插入或者更新时,除写入索引记录外,还会同时写入UNDO LOG。对于新插入的行记录,会将新插入的行记录写入UNDO LOG,而对于被更新的行记录,则是将更新前的记录旧值写入UNDO LOG,并在新记录的隐藏字段rollptr中记录该UNDO LOG位置信息。

接下来我们分别描述INSERT和UPDATE时的UNDO LOG写入实现。

INSERT记录

新插入的记录会写到undo log中,在函数btr_cur_ins_lock_and_undo中实现:


1. db_err_t btr_cur_ins_lock_and_undo(...)
2. {
3. rec = btr_cur_get_rec(cursor);
4. index = cursor->index;
5. ...
6. err = trx_undo_report_row_operation(flags, TRX_UNDO_INSERT_OP, thr, index,
7. entry, NULL, 0, NULL, NULL, &roll_ptr);

9. row_upd_index_entry_sys_field(entry, index, DATA_ROLL_PTR, roll_ptr);
10. return (DB_SUCCESS);
11. }

13. // 最终走到这里
14. ulint trx_undo_page_report_insert(...)
15. {
16. // 找到该undo page下一个空闲位置
17. first_free = mach_read_from_2(undo_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_FREE);
18. ptr = undo_page + first_free;

20. // 预留的2字节用于记录下一个undo log record位置
21. ptr += 2;

23. *ptr++ = TRX_UNDO_INSERT_REC;
24. ptr += mach_u64_write_much_compressed(ptr, trx->undo_no);
25. ptr += mach_u64_write_much_compressed(ptr, index->table->id);

27. // index->n_uniq代表主键中包含的column数量
28. // 如果是联合主键,则>1
29. for (i = 0; i < dict_index_get_n_unique(index); i++) {
30. const dfield_t *field = dtuple_get_nth_field(clust_entry, i);
31. ulint flen = dfield_get_len(field);
32. ptr += mach_write_compressed(ptr, flen);
33. ut_memcpy(ptr, dfield_get_data(field), flen);
34. }
35. return (trx_undo_page_set_next_prev_and_add(undo_page, ptr, mtr));
36. }

‌对于insert 操作,会将新记录写入至undo log。写入的只是聚簇索引包含的column。例如,创建的表结构如下:


1. create table t(id1 int, id2 int, value int, primary key (id1, id2));
2. insert into t values(1, 2, 3);

‌此时写入内容其实只包含列id1和id2的内容。完成后,会生成一个rollptr,且记录在dtuple_t的rollptr列中。

UPDATE记录

更新一个已存在记录时,会将该记录的老版本(即修改前版本)记录在UNDO LOG。与INSERT操作类似:只记录其聚簇索引中包含的column的值以及更新涉及的column值。记完UNDO LOG后,再将数据页中的记录更新为新值,同时将UNDO LOG位置记录在更新后行记录的rollptr字段,形成一个历史更新链。


1. dberr_t
2. btr_cur_upd_lock_and_undo(...)
3. {
4. // rec指向了待更新记录的内容(更新前value)
5. rec = btr_cur_get_rec(cursor);
6. index = cursor->index;

8. // 更新非聚簇索引不记录undo log
9. if (!dict_index_is_clust(index)) {
10. return(lock_sec_rec_modify_check_and_lock(
11. flags, btr_cur_get_block(cursor), rec,
12. index, thr, mtr));
13. }
14. // 记录undo log
15. return(trx_undo_report_row_operation(
16. mtr, flags, TRX_UNDO_MODIFY_OP, thr,
17. index, NULL, update,
18. cmpl_info, rec, offsets, roll_ptr));
19. }

21. dberr_t trx_undo_report_row_operation(...)
22. {
23. do {
24. undo_page = buf_block_get_frame(undo_block);
25. switch (op_type) {
26. default:
27. offset = trx_undo_page_report_modify(
28. undo_page, trx, index, rec, offsets, update,
29. cmpl_info, &mtr);
30. }
31. }
32. }

34. ulint
35. trx_undo_page_report_modify(...)
36. {
37. table = index->table;
38. first_free = mach_read_from_2(undo_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_FREE);
39. ptr = undo_page + first_free;
40. ptr += 2;

42. // 需要注意这玩意儿:如果update为null时,undo log record中的type会被设置为
43. // TRX_UNDO_DEL_MARK_REC
44. // 那什么时候update会为null呢?
45. // 跟踪了一下发现可能有以下两种场景:
46. // 1. btr_cur_ins_lock_and_undo:即插入一条新记录
47. // 2. btr_cur_del_mark_set_clust_rec: 从聚簇索引中删除一条老记录
48. // 而2可能会发生在两种场景下:
49. // 1. 更新一个已有主键值的主键:此时会先插入新的主键,再将老的主键值标记删除
50. // 2. 删除一个已有主键值
51. if (!update) {
52. // 对已有记录标记删除
53. type_cmpl = TRX_UNDO_DEL_MARK_REC;
54. } else if (rec_get_deleted_flag(rec, dict_table_is_comp(table))) {
55. // 更新删除记录,什么时候会这样呢?
56. type_cmpl = TRX_UNDO_UPD_DEL_REC;
57. } else {
58. // 更新一个已存在的记录
59. type_cmpl = TRX_UNDO_UPD_EXIST_REC;
60. }

62. type_cmpl |= cmpl_info * TRX_UNDO_CMPL_INFO_MULT;
63. type_cmpl_ptr = ptr;

65. *ptr++ = (byte) type_cmpl;
66. ptr += mach_ull_write_much_compressed(ptr, trx->undo_no);

68. ptr += mach_ull_write_much_compressed(ptr, table->id);
69. // 不确定info_bits内到底存储什么玩意
70. *ptr++ = (byte) rec_get_info_bits(rec, dict_table_is_comp(table));

72. // 获得老记录的trx id和roll ptr值
73. // 并将其记录在该UNDO LOG的rollptr和trx_id字段
74. // 这样才可以将所有的更新串成一个更新链
75. // ----------        ----------        ----------        -----------
76. // | UNDO-1 |  <--   | UNDO-2 |  <--   | UNDO-3 |  <--   | row rec |
77. // ----------        ----------        ----------        -----------
78. // 假如现在来了一次更新记录在UNDO-4中,那形成的历史链应该如下:
79. // ----------        ----------        ----------        ----------       -----------
80. // | UNDO-1 |  <--   | UNDO-2 |  <--   | UNDO-3 |  <--   | UNDO-4 |  <--  | row rec |
81. // ----------        ----------        ----------        ----------       -----------
82. // 更新前的row rec中记录的rollptr指向UNDO-3
83. // 因此UNDO-4中记录的内容是row rec,且其rollptr指向UNDO-3
84. field = rec_get_nth_field(rec, offsets, dict_index_get_sys_col_pos(index, DATA_TRX_ID), &flen);

86. trx_id = trx_read_trx_id(field);
87. ptr += mach_ull_write_compressed(ptr, trx_id);
88. field = rec_get_nth_field(rec, offsets,
89. dict_index_get_sys_col_pos(
90. index, DATA_ROLL_PTR), &flen);
91. ptr += mach_ull_write_compressed(ptr, trx_read_roll_ptr(field));

93. // 记录聚簇索引包含的column的旧值,如果是联合索引,那么可能会包含多个column
94. for (i = 0; i < dict_index_get_n_unique(index); i++) {
95. field = rec_get_nth_field(rec, offsets, i, &flen);
96. ptr += mach_write_compressed(ptr, flen);
97. if (flen != UNIV_SQL_NULL) {
98. ut_memcpy(ptr, field, flen);
99. ptr += flen;
100. }
101. }

103. // 接下来记录被更新column的旧值
104. // 注意:只需要记录旧值即可,更新后的值无需记录,因为无论的回滚还是MVCC,都只需要旧值即可
105. // 每个column记录三个字段:
106. // 1. column的field no
107. // 2. column field的长度
108. // 3. column field的值
109. if (update) {
110. ptr += mach_write_compressed(ptr, upd_get_n_fields(update));
111. for (i = 0; i < upd_get_n_fields(update); i++) {
112. // pos保存field no
113. ulint pos = upd_get_nth_field(update, i)->field_no;
114. ptr += mach_write_compressed(ptr, pos);
115. // field保存被更新column的旧值, flen记录其长度
116. field = rec_get_nth_field(rec, offsets, pos, &flen);
117. ptr += mach_write_compressed(ptr, flen);
118. if (flen != UNIV_SQL_NULL) {
119. ut_memcpy(ptr, field, flen);
120. ptr += flen;
121. }
122. }
123. }
124. }

删除记录

在innodb中,删除一个行记录实现上是标记删除,即不立即从物理页面中删除该记录(因为该记录很有可能还被其他事务所访问),只是将该行记录标记为删除,并记录UNDO LOG,以后在回收UNDO LOG时判断该行记录不再被访问时再清理该行记录。


1. dberr_t btr_cur_del_mark_set_clust_rec(...)
2. {
3. ...
4. // 为标记删除记录undo log
5. // 注意倒数第五个参数为nullptr,代表的是删除
6. err =
7. trx_undo_report_row_operation(flags, TRX_UNDO_MODIFY_OP, thr, index,
8. entry, nullptr, 0, rec, offsets, &roll_ptr);

10. // 更新记录的trx_id和rollptr列
11. row_upd_rec_sys_fields(rec, page_zip, index, offsets, trx, roll_ptr);

13. return (err);
14. }

16. // 在记录删除时传入的update为nullptr
17. ulint
18. trx_undo_page_report_modify(ulint flags,
19. ulint op_type,
20. que_thr_t *thr,
21. dict_index_t *index,
22. const dtuple_t *clust_entry,
23. const upd_t *update,
24. const rec_t *rec,
25. const ulint *offsets,
26. roll_ptr_t *roll_ptr)
27. {
28. // 需要注意这玩意儿:如果update为null时,undo log record中的type会被设置为
29. // TRX_UNDO_DEL_MARK_REC
30. // 那什么时候update会为null呢?
31. // 跟踪了一下发现可能有以下两种场景:
32. // 1. btr_cur_ins_lock_and_undo:即插入一条新记录
33. // 2. btr_cur_del_mark_set_clust_rec: 从聚簇索引中删除一条老记录
34. // 不过对于1是insert场景,最终会走trx_undo_page_report_insert()
35. // 而2可能会发生在两种场景下:
36. // 1. 更新一个已有主键值的主键:此时会先插入新的主键,再将老的主键值标记删除
37. // 2. 删除一个已有主键值
38. if (!update) {
39. // 对已有记录标记删除
40. type_cmpl = TRX_UNDO_DEL_MARK_REC;
41. } else if (rec_get_deleted_flag(rec, dict_table_is_comp(table))) {
42. // 更新删除记录,什么时候会这样呢?
43. type_cmpl = TRX_UNDO_UPD_DEL_REC;
44. } else {
45. // 更新一个已存在的记录
46. type_cmpl = TRX_UNDO_UPD_EXIST_REC;
47. }
48. ...
49. // 获得老记录的trx id和roll ptr值
50. // 并将其记录在该UNDO LOG的rollptr和trx_id字段
51. // 这个与上面的update记录流程一致,不再重复介绍

53. // 记录聚簇索引包含的column的旧值,如果是联合索引,那么可能会包含多个column
54. // 这个也与上面的update记录流程一致,不再重复介绍
55. ...
56. // 如果是删除,那要将所有的column都记录在undo log中
57. if (!update || !(cmpl_info & UPD_NODE_NO_ORD_CHANGE)) {
58. ...
59. trx->update_undo->del_marks = TRUE;
60. ptr += 2;
61. for (col_no = 0; col_no < dict_table_get_n_cols(table); col_no++)
62. {
63. ...
64. }
65. mach_write_to_2(old_ptr, ptr - old_ptr);
66. }
67. }

删除记录的内部实现其实也比较简单:

  1. 将行记录设置标记删除
  2. 记录UNDO LOG RECORD,需要搞清楚里面到底记录了哪些内容
  3. 最后,更新原纪录的系统列:trx_id和rollptr

原文:http://mysql.taobao.org/monthly/2020/08/05/