MySQL · 源码分析 · BLOB字段UPDATE流程分析

准备

MySQL 8.0.25

相关背景

在游戏等业务场景中,常常会使用到如BLOB格式的可变长大字段,此类可变长大字段的处理与其余字段格式有所不同。 在处理如VARCHAR、VARBINARY、BLOB、TEXT等可变长度列时,若数据的长度过长,InnoDB不会直接将字段完整容纳在记录所在的B-Tree页上,而是会将过长的变长字段单独存放在溢出页(off-page)中,B-Tree页只会存储列值的部分前缀。

在分析BLOB字段的操作流程前,首先需要了解Innodb的行格式,具体介绍可以参见MySQL行格式介绍,目前实际应用中的最常见的是compact(5.6的默认)和dynamic(5.7及以后的默认)这两种。

行格式紧凑格式增强型变长列大索引前缀支持压缩支持的表空间
REDUNDANTNoNoNoNosystem, general, file-per-table
COMPACTYesNoNoNosystem, general, file-per-table
DYNAMICYesYesYesNosystem, general, file-per-table
COMPRESSEDYesYesYesYesfile-per-table, general

这里主要介绍一下Innodb采用不同行格式时对于过长字段的溢出处理(off-page列),当前版本Innodb判断是否将变长列存储在off-page上由页大小和记录长度决定,Innodb会把行中最长的列放到off-page直到数据页能存放下两行数据,可见page_zip_rec_needs_ext()函数。各个行格式的溢出列的转化逻辑略有不同,具体转化方法可参考 dtuple_convert_big_rec()函数:

  • Redundant 行格式会把变长字段值的前768字节存在B-Tree的索引记录中,若字段超出长度,则其剩余数据被放在溢出页进行存储。对大于这个768字节的固定长度列,会被编码为可变长度列。对于采用溢出处理的off-page列,列数据末尾会存有指向剩余数据所在页地址的指针(占用20字节);对于过长的溢出列(长度超过一个页),会以链表链接的形式存储在多个溢出页上。
  • Compact 行格式在溢出页的处理上和redundant行格式基本一致,也是将超过768字节后的变长列放至溢出页上;但是此外,Compact行格式在行前有变长列表,其中对于off-page列的长度的存储记录为788字节 = 768字节 + 外部指针(reference)长度20字节。
  • Dynamic 行格式和Compact行格式有类似的行存储格式(有变长列表等),但在其基础上增加了long variable-length columns和large index key prefixes特点,即会将大变长列完全存储在off-page上,聚集索引上只含有20字节的指针。
  • Compressed 行格式在dynamic行格式基础上增加了压缩特性,对于off-page列的处理模式与Dynamic行格式基本一致。

BLOB字段UPDATE流程

这里使用的是COMPACT行格式,RC隔离级别,BLOB字段的实际大小都约为10KB(此大小会导致溢出页的使用),表格格式如下:

  1. CREATE TABLE `blobtest1`(
  2. `uid` int(10) NOT NULL DEFAULT '0',
  3. `bin_data` mediumblob NOT NULL,
  4. `last_save_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  5. PRIMARY KEY (`uid`) USING BTREE
  6. ) engine = InnoDB;

如下给出使用主键对BLOB字段进行UPDATE的代码执行流程:

  1. ...
  2. ->>ha_innobase::read_range_first
  3. ...
  4. ->>row_search_mvcc
  5. ... (循环搜索至matching的记录)
  6. ->>sel_set_rec_lock (对recordX锁)
  7. ...
  8. ...
  9. ->>ha_innobase::update_row
  10. ... (检查log空间、row update node)
  11. ->>row_upd_clust_step (启动mtr)
  12. ->>btr_pcur_restore_position (乐观获取leaf节点x-latch)
  13. ->>row_upd_clust_rec
  14. ->>btr_cur_optimistic_update (存在溢出列,乐观更新失败,返回DB_OVERFLOW)
  15. ->>btr_cur_prefetch_siblings
  16. ->>mtr->commitmtr->start(释放获取的mtr资源,重启mtr)
  17. ->>btr_pcur_restore_position (进入悲观模式,重新定位cursor)
  18. ->>btr_cur_search_to_nth_level(获取indexsx-latch,从root搜索至目标叶子结点,最终会施加目标节点、父节点、左右节点的x-latch)
  19. ->>btr_cur_pessimistic_update
  20. ->>btr_cur_optimistic_update (同上失败)
  21. ->>row_rec_to_index_entry (获得index entry,不包括溢出列)
  22. ->>row_upd_index_replace_new_col_vals_index_pos (更新index entry)
  23. ->>row_upd_index_replace_new_col_val_func (更新对应列,此时更新的大字段会被完全copyindex entry,老的溢出列会被标记)
  24. ->>dtuple_convert_big_rec (构建溢出列对象)
  25. ->>btr_cur_upd_lock_and_undo (进行undo记录)
  26. ->>mtr_x_lock_space(space, mtr) (在锁定LOB page前锁定file space,避免死锁)
  27. ->>fsp_reserve_free_extents (预留文件空间)
  28. ... (锁处理、删除原index记录)
  29. ->>btr_cur_insert_if_possible (乐观插入)
  30. |乐观插入失败|->>btr_cur_pessimistic_insert (悲观插入)
  31. ->>unmark_extern_fields (标记溢出列)
  32. ->>fil_space_release_free_extents (释放预留空间)
  33. ->>btr_store_big_rec_extern_fields (存储溢出列)
  34. ->>InsertContext::check_redolog (重定位cursor、重启mtr,可见index列和off-page列并不是一个mtr)
  35. |遍历所有溢出列|... (检查是否可以部分更新)
  36. ->>lob::insert (将溢出列实际插入tablespace)
  37. ->>first_page_t::alloc (分配首lob页)
  38. ->>first_page_t::write、设定相关信息
  39. |有后续lob页|->>data_page_t::allocdata_page_t::write、设定相关信息
  40. ->> ... (更新upd_field_t中的溢出列ref)
  41. ->>mtr->commit (提交mtr,释放indexpage锁等资源)
  42. ->>dtuple_big_rec_free (清理溢出页内存)
  43. ... (清理工作)
  44. ->>ha_innobase::read_range_next
  45. ...

源码分析

下面抽取出BLOB UPDATE过程中相关函数的核心部分并做注释,结合上述执行流程帮助理解:

  1. static dberr_t row_upd_clust_rec()
  2. {
  3. // 部分边界分支省略,仅抽象核心路径...
  4. // 乐观更新失败...
  5. mtr->start();
  6. // 重定位cursor,对index及所需leaf节点加锁
  7. ut_a(btr_pcur_restore_position(BTR_MODIFY_TREE, pcur, mtr));
  8. // 更新index列
  9. err = btr_cur_pessimistic_update(
  10. flags | BTR_NO_LOCKING_FLAG | BTR_KEEP_POS_FLAG, btr_cur, &offsets,
  11. offsets_heap, heap, &big_rec, node->update, node->cmpl_info, thr, trx_id,
  12. trx->undo_no, mtr);
  13. if (big_rec) {
  14. // 更新off-page列
  15. err = lob::btr_store_big_rec_extern_fields(
  16. trx, pcur, node->update, offsets, big_rec, mtr, lob::OPCODE_UPDATE);
  17. }
  18. mtr->commit();
  19. // 清理工作...
  20. return (err);
  21. }
  1. dberr_t btr_cur_pessimistic_update()
  2. {
  3. // 部分边界分支省略,仅抽象核心路径...
  4. // 构建更新后的dtuple_t
  5. dtuple_t *new_entry = row_rec_to_index_entry(rec, index, *offsets, entry_heap);
  6. row_upd_index_replace_new_col_vals_index_pos(new_entry, index, update, FALSE, entry_heap);
  7. // 选定溢出列,会copy老的溢出页指针
  8. if (page_zip_rec_needs_ext(/*...*/)) {
  9. big_rec_vec = dtuple_convert_big_rec(index, update, new_entry);
  10. }
  11. err = btr_cur_upd_lock_and_undo(flags, cursor, *offsets, update, cmpl_info,
  12. thr, mtr, &roll_ptr);
  13. if (err != DB_SUCCESS) {
  14. goto err_exit;
  15. }
  16. if (optim_err == DB_OVERFLOW) {
  17. // 提前锁定file space,防止死锁
  18. fil_space_t *space = fil_space_get(index->space);
  19. mtr_x_lock_space(space, mtr);
  20. }
  21. // 标记原溢出页首页为不可部分更新
  22. lob::mark_not_partially_updatable(trx, index, update, mtr);
  23. if (optim_err == DB_OVERFLOW) {
  24. //为索引树的文件段预留足够的空间
  25. ulint n_extents = cursor->tree_height / 16 + 3;
  26. if (!fsp_reserve_free_extents(
  27. &n_reserved, index->space, n_extents,
  28. flags & BTR_NO_UNDO_LOG_FLAG ? FSP_CLEANING : FSP_NORMAL, mtr)) {
  29. err = DB_OUT_OF_FILE_SPACE;
  30. goto err_exit;
  31. }
  32. }
  33. // 更新系统列...
  34. // 将record锁移至下边界以更新record
  35. if (!dict_table_is_locking_disabled(index->table)) {
  36. lock_rec_store_on_page_infimum(block, rec);
  37. }
  38. // 删除index上的原纪录
  39. btr_search_update_hash_on_delete(cursor);
  40. page_cursor = btr_cur_get_page_cur(cursor);
  41. page_cur_delete_rec(page_cursor, index, *offsets, mtr);
  42. page_cur_move_to_prev(page_cursor);
  43. // 乐观插入
  44. rec = btr_cur_insert_if_possible(cursor, new_entry, offsets, offsets_heap, mtr);
  45. if (rec) {
  46. // 乐观插入成功
  47. page_cursor->rec = rec;
  48. // 将锁从下边界移回更新后的record上
  49. if (!dict_table_is_locking_disabled(index->table)) {
  50. lock_rec_restore_from_page_infimum(btr_cur_get_block(cursor), rec, block);
  51. }
  52. if (!rec_get_deleted_flag(rec, rec_offs_comp(*offsets))) {
  53. // 更新新index列的溢出列标记
  54. lob::BtrContext btr_ctx(mtr, pcur, index, rec, *offsets, block);
  55. btr_ctx.unmark_extern_fields();
  56. }
  57. bool adjust = big_rec_vec && (flags & BTR_KEEP_POS_FLAG);
  58. // 尝试压缩...
  59. err = DB_SUCCESS;
  60. goto return_after_reservations;
  61. } else {
  62. // 空间不足,插入失败...
  63. }
  64. if (big_rec_vec != nullptr && !index->table->is_intrinsic()) {
  65. // btr_cur_pessimistic_insert()会释放index的sx锁,在此先再次加锁保证持有index锁,以在同一mtr构建和存储big_rec
  66. ut_ad(mtr_memo_contains_flagged(mtr, dict_index_get_lock(index),
  67. MTR_MEMO_X_LOCK | MTR_MEMO_SX_LOCK));
  68. mtr_sx_lock(dict_index_get_lock(index), mtr);
  69. }
  70. was_first = page_cur_is_before_first(page_cursor);
  71. err = btr_cur_pessimistic_insert(
  72. BTR_NO_UNDO_LOG_FLAG | BTR_NO_LOCKING_FLAG | BTR_KEEP_SYS_FLAG, cursor,
  73. offsets, offsets_heap, new_entry, &rec, &dummy_big_rec, nullptr, mtr);
  74. // 更新系统列...
  75. if (!rec_get_deleted_flag(rec, rec_offs_comp(*offsets))) {
  76. // 更新新index列的溢出列标记
  77. buf_block_t *rec_block = btr_cur_get_block(cursor);
  78. page_zip = buf_block_get_page_zip(rec_block);
  79. lob::BtrContext btr_ctx(mtr, nullptr, index, rec, *offsets, rec_block);
  80. btr_ctx.unmark_extern_fields();
  81. }
  82. if (!dict_table_is_locking_disabled(index->table)) {
  83. lock_rec_restore_from_page_infimum(btr_cur_get_block(cursor), rec, block);
  84. }
  85. if (!was_first && !dict_table_is_locking_disabled(index->table)) {
  86. btr_cur_pess_upd_restore_supremum(btr_cur_get_block(cursor), rec, mtr);
  87. }
  88. return_after_reservations:
  89. // 清理环境、传递big_rec_vec溢出列对象...
  90. return err;
  91. }
  1. dberr_t btr_store_big_rec_extern_fields()
  2. { /* 将big_rec_vec中的溢出字段存储到表空间并将指针指向对应的record。
  3. 存储这些字段的页面会从索引的叶节点文件段上分配。*/
  4. // 建立context
  5. BtrContext btr_ctx(btr_mtr, pcur, index, rec, offsets, rec_block, op);
  6. InsertContext ctx(btr_ctx, big_rec_vec);
  7. // 设定溢出列的"being modified"位
  8. Being_modified bm(btr_ctx, big_rec_vec, pcur, offsets, op, btr_mtr);
  9. // 流程为:store position -> commit mtr -> check log free -> start mtr -> restore position
  10. // mtr的提交,重启后在btr_pcur_restore_position里走悲观加锁,重新获取index及相应page锁
  11. ctx.check_redolog();
  12. // 显示Uncompressed LOB的路径
  13. //
  14. for (uint i = 0; i < big_rec_vec->n_fields; i++) {
  15. // 参数传递,记录定位...
  16. // 根据partially updatable标志及页类型检测是否可以部分更新
  17. bool can_do_partial_update = false;
  18. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
  19. big_rec_vec->fields[i].ext_in_old) {
  20. can_do_partial_update = blobref.is_lob_partially_updatable(index);
  21. }
  22. if (page_zip != nullptr) {
  23. // 省略压缩格式...
  24. } else {
  25. // 非压缩格式溢出列
  26. bool do_insert = true;
  27. // 尝试以update方式更新溢出列
  28. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
  29. blobref.is_big(rec_block->page.size) && can_do_partial_update) {
  30. // 尝试部分更新,成功返回,失败则标记以通知purge thread可以清除老溢出列...
  31. }
  32. // 使用insert方式更新溢出列
  33. if (do_insert) {
  34. // 实际将LOB的溢出列部分插入tablespace,首先会
  35. error = lob::insert(&ctx, trx, blobref, &big_rec_vec->fields[i], i);
  36. if (op == lob::OPCODE_UPDATE && upd != nullptr) {
  37. upd_field_t *uf = upd->get_field_by_field_no(field_no, index);
  38. if (uf != nullptr) {
  39. // 更新upd_field_t中溢出列的reference
  40. dfield_t *new_val = &uf->new_val;
  41. if (dfield_is_ext(new_val)) {
  42. byte *field_ref = new_val->blobref();
  43. blobref.copy(field_ref);
  44. ref_t::set_being_modified(field_ref, false, nullptr);
  45. }
  46. }
  47. }
  48. }
  49. }
  50. }
  51. return (error);
  52. // ...
  53. // 其他特殊格式溢出页处理...
  54. }

注意事项

  1. 当更新目标存在off-page列时,Innodb会默认走悲观更新逻辑,会持有index的sx-latch,目标节点、相邻节点的x-latch,大表并发情况下可能产生瓶颈。
  2. 由于LOB字段本身较大,redo的产生量较大;过程中check并记录redo,redo刷写性能不高的情况下持续写入大字段可导致redo buffer打满而卡住写入。
  3. 考虑性能等问题,记录的index页和LOB页的写入、过大LOB字端本身的写入(每写64KLOB数据mtr提交一次)并不是在同一个mtr commit流程中,将原子性拆分。
  4. 目前的机制确保超过约8K的记录才会移至溢出页,由于每次分配LOB页空间会分配一个空白页,因此LOB字段可以存在空间浪费(写放大)的情况。