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);
  8. row_upd_index_entry_sys_field(entry, index, DATA_ROLL_PTR, roll_ptr);
  9. return (DB_SUCCESS);
  10. }
  11. // 最终走到这里
  12. ulint trx_undo_page_report_insert(...)
  13. {
  14. // 找到该undo page下一个空闲位置
  15. first_free = mach_read_from_2(undo_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_FREE);
  16. ptr = undo_page + first_free;
  17. // 预留的2字节用于记录下一个undo log record位置
  18. ptr += 2;
  19. *ptr++ = TRX_UNDO_INSERT_REC;
  20. ptr += mach_u64_write_much_compressed(ptr, trx->undo_no);
  21. ptr += mach_u64_write_much_compressed(ptr, index->table->id);
  22. // index->n_uniq代表主键中包含的column数量
  23. // 如果是联合主键,则>1
  24. for (i = 0; i < dict_index_get_n_unique(index); i++) {
  25. const dfield_t *field = dtuple_get_nth_field(clust_entry, i);
  26. ulint flen = dfield_get_len(field);
  27. ptr += mach_write_compressed(ptr, flen);
  28. ut_memcpy(ptr, dfield_get_data(field), flen);
  29. }
  30. return (trx_undo_page_set_next_prev_and_add(undo_page, ptr, mtr));
  31. }

‌对于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;
  7. // 更新非聚簇索引不记录undo log
  8. if (!dict_index_is_clust(index)) {
  9. return(lock_sec_rec_modify_check_and_lock(
  10. flags, btr_cur_get_block(cursor), rec,
  11. index, thr, mtr));
  12. }
  13. // 记录undo log
  14. return(trx_undo_report_row_operation(
  15. mtr, flags, TRX_UNDO_MODIFY_OP, thr,
  16. index, NULL, update,
  17. cmpl_info, rec, offsets, roll_ptr));
  18. }
  19. dberr_t trx_undo_report_row_operation(...)
  20. {
  21. do {
  22. undo_page = buf_block_get_frame(undo_block);
  23. switch (op_type) {
  24. default:
  25. offset = trx_undo_page_report_modify(
  26. undo_page, trx, index, rec, offsets, update,
  27. cmpl_info, &mtr);
  28. }
  29. }
  30. }
  31. ulint
  32. trx_undo_page_report_modify(...)
  33. {
  34. table = index->table;
  35. first_free = mach_read_from_2(undo_page + TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_FREE);
  36. ptr = undo_page + first_free;
  37. ptr += 2;
  38. // 需要注意这玩意儿:如果update为null时,undo log record中的type会被设置为
  39. // TRX_UNDO_DEL_MARK_REC
  40. // 那什么时候update会为null呢?
  41. // 跟踪了一下发现可能有以下两种场景:
  42. // 1. btr_cur_ins_lock_and_undo:即插入一条新记录
  43. // 2. btr_cur_del_mark_set_clust_rec: 从聚簇索引中删除一条老记录
  44. // 而2可能会发生在两种场景下:
  45. // 1. 更新一个已有主键值的主键:此时会先插入新的主键,再将老的主键值标记删除
  46. // 2. 删除一个已有主键值
  47. if (!update) {
  48. // 对已有记录标记删除
  49. type_cmpl = TRX_UNDO_DEL_MARK_REC;
  50. } else if (rec_get_deleted_flag(rec, dict_table_is_comp(table))) {
  51. // 更新删除记录,什么时候会这样呢?
  52. type_cmpl = TRX_UNDO_UPD_DEL_REC;
  53. } else {
  54. // 更新一个已存在的记录
  55. type_cmpl = TRX_UNDO_UPD_EXIST_REC;
  56. }
  57. type_cmpl |= cmpl_info * TRX_UNDO_CMPL_INFO_MULT;
  58. type_cmpl_ptr = ptr;
  59. *ptr++ = (byte) type_cmpl;
  60. ptr += mach_ull_write_much_compressed(ptr, trx->undo_no);
  61. ptr += mach_ull_write_much_compressed(ptr, table->id);
  62. // 不确定info_bits内到底存储什么玩意
  63. *ptr++ = (byte) rec_get_info_bits(rec, dict_table_is_comp(table));
  64. // 获得老记录的trx id和roll ptr值
  65. // 并将其记录在该UNDO LOG的rollptr和trx_id字段
  66. // 这样才可以将所有的更新串成一个更新链
  67. // ---------- ---------- ---------- -----------
  68. // | UNDO-1 | <-- | UNDO-2 | <-- | UNDO-3 | <-- | row rec |
  69. // ---------- ---------- ---------- -----------
  70. // 假如现在来了一次更新记录在UNDO-4中,那形成的历史链应该如下:
  71. // ---------- ---------- ---------- ---------- -----------
  72. // | UNDO-1 | <-- | UNDO-2 | <-- | UNDO-3 | <-- | UNDO-4 | <-- | row rec |
  73. // ---------- ---------- ---------- ---------- -----------
  74. // 更新前的row rec中记录的rollptr指向UNDO-3
  75. // 因此UNDO-4中记录的内容是row rec,且其rollptr指向UNDO-3
  76. field = rec_get_nth_field(rec, offsets, dict_index_get_sys_col_pos(index, DATA_TRX_ID), &flen);
  77. trx_id = trx_read_trx_id(field);
  78. ptr += mach_ull_write_compressed(ptr, trx_id);
  79. field = rec_get_nth_field(rec, offsets,
  80. dict_index_get_sys_col_pos(
  81. index, DATA_ROLL_PTR), &flen);
  82. ptr += mach_ull_write_compressed(ptr, trx_read_roll_ptr(field));
  83. // 记录聚簇索引包含的column的旧值,如果是联合索引,那么可能会包含多个column
  84. for (i = 0; i < dict_index_get_n_unique(index); i++) {
  85. field = rec_get_nth_field(rec, offsets, i, &flen);
  86. ptr += mach_write_compressed(ptr, flen);
  87. if (flen != UNIV_SQL_NULL) {
  88. ut_memcpy(ptr, field, flen);
  89. ptr += flen;
  90. }
  91. }
  92. // 接下来记录被更新column的旧值
  93. // 注意:只需要记录旧值即可,更新后的值无需记录,因为无论的回滚还是MVCC,都只需要旧值即可
  94. // 每个column记录三个字段:
  95. // 1. column的field no
  96. // 2. column field的长度
  97. // 3. column field的值
  98. if (update) {
  99. ptr += mach_write_compressed(ptr, upd_get_n_fields(update));
  100. for (i = 0; i < upd_get_n_fields(update); i++) {
  101. // pos保存field no
  102. ulint pos = upd_get_nth_field(update, i)->field_no;
  103. ptr += mach_write_compressed(ptr, pos);
  104. // field保存被更新column的旧值, flen记录其长度
  105. field = rec_get_nth_field(rec, offsets, pos, &flen);
  106. ptr += mach_write_compressed(ptr, flen);
  107. if (flen != UNIV_SQL_NULL) {
  108. ut_memcpy(ptr, field, flen);
  109. ptr += flen;
  110. }
  111. }
  112. }
  113. }

删除记录

在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);
  9. // 更新记录的trx_id和rollptr列
  10. row_upd_rec_sys_fields(rec, page_zip, index, offsets, trx, roll_ptr);
  11. return (err);
  12. }
  13. // 在记录删除时传入的update为nullptr
  14. ulint
  15. trx_undo_page_report_modify(ulint flags,
  16. ulint op_type,
  17. que_thr_t *thr,
  18. dict_index_t *index,
  19. const dtuple_t *clust_entry,
  20. const upd_t *update,
  21. const rec_t *rec,
  22. const ulint *offsets,
  23. roll_ptr_t *roll_ptr)
  24. {
  25. // 需要注意这玩意儿:如果update为null时,undo log record中的type会被设置为
  26. // TRX_UNDO_DEL_MARK_REC
  27. // 那什么时候update会为null呢?
  28. // 跟踪了一下发现可能有以下两种场景:
  29. // 1. btr_cur_ins_lock_and_undo:即插入一条新记录
  30. // 2. btr_cur_del_mark_set_clust_rec: 从聚簇索引中删除一条老记录
  31. // 不过对于1是insert场景,最终会走trx_undo_page_report_insert()
  32. // 而2可能会发生在两种场景下:
  33. // 1. 更新一个已有主键值的主键:此时会先插入新的主键,再将老的主键值标记删除
  34. // 2. 删除一个已有主键值
  35. if (!update) {
  36. // 对已有记录标记删除
  37. type_cmpl = TRX_UNDO_DEL_MARK_REC;
  38. } else if (rec_get_deleted_flag(rec, dict_table_is_comp(table))) {
  39. // 更新删除记录,什么时候会这样呢?
  40. type_cmpl = TRX_UNDO_UPD_DEL_REC;
  41. } else {
  42. // 更新一个已存在的记录
  43. type_cmpl = TRX_UNDO_UPD_EXIST_REC;
  44. }
  45. ...
  46. // 获得老记录的trx id和roll ptr值
  47. // 并将其记录在该UNDO LOG的rollptr和trx_id字段
  48. // 这个与上面的update记录流程一致,不再重复介绍
  49. // 记录聚簇索引包含的column的旧值,如果是联合索引,那么可能会包含多个column
  50. // 这个也与上面的update记录流程一致,不再重复介绍
  51. ...
  52. // 如果是删除,那要将所有的column都记录在undo log中
  53. if (!update || !(cmpl_info & UPD_NODE_NO_ORD_CHANGE)) {
  54. ...
  55. trx->update_undo->del_marks = TRUE;
  56. ptr += 2;
  57. for (col_no = 0; col_no < dict_table_get_n_cols(table); col_no++)
  58. {
  59. ...
  60. }
  61. mach_write_to_2(old_ptr, ptr - old_ptr);
  62. }
  63. }

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

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