MySQL · 源码分析 · innodb-BLOB演进与实现

Author: yunqian

BLOB 介绍

InnoDB 存储引擎中所有可变长度类型的字段(如 VARCHAR、VARBINARY、BLOB 和 TEXT)可以存储在主键记录内,也可以存储在主键记录之外的单独 BLOB 页中(在同一表空间内)。所有这些字段都可以归类为大对象。这些大对象要么是二进制大对象,要么是字符大对象。二进制大对象没有关联的字符集,而字符大对象有。在 InnoDB 存储引擎中,字符大对象和二进制大对象的处理方式没有区别,我们使用“BLOB”来指代上述的大对象字段。BLOB不同大小,不同场景,可以全部存储在主键,部分前缀存储在主键,或者全部存储在外部BLOB页中,本篇文章主要集中在BLOB全部存储在外部BLOB页中时,在innodb中是如何实现的,其他的在主键内部或者前缀在主键内部不再展开说明。只有主键可以在外部存储 BLOB 字段,二级索引不能有外部存储的字段,本文的讨论都是围绕着主键展开。

BLOB 演进

5.6 和 5.7 BLOB

在mysql 5.6和5.7中,innodb实现BLOB的外部存储,实际上将BLOB数据按照页大小切分存储到一批BLOB页中,这些BLOB页从前往后连接成一个链表,在主键上对应字段位置会存储一个指针lob ref(lob ref由 space id,page no,data len等数据构成)指向BLOB页链表的第一个页见,下图: 5.6 & 5.7 BLOB

mysql 5.6 & 5.7 中针对BLOB数据的修改,会创建一个全新的BLOB数据,将主键上lob ref指向新的BLOB数据,undo中对应的lob ref会保存旧的BLOB数据。这样的实现,使得 5.6 和 5.7 中BLOB数据的多版本,完全是由undo来实现,blob只是由lob ref指向定位到数据,可参考下图: 5.6 和 5.7 BLOB数据修改

8.0 BLOB

由于mysql 5.6 和5.7中,所有针对blob数据的修改,必须整体替换一个BLOB对象,而BLOB对象占据的空间相对比较大,这样的实现方式一是造成空间的浪费,二是修改效率的降低。针对这些问题,在mysql 8.0中 innodb对BLOB进行了重新设计实现:给BLOB页链表加上索引lob index,这样可以快速定位到BLOB中任何位置,见下图: 8.0 BLOB

在mysql 8.0 中支持了部分更新(partial update),即针对BLOB数据的更新,可以只更新BLOB内的一部分数据(实际上当前只支持特定的json函数)。这个BLOB新的实现解决了mysql 5.6 和 5.7中存在的问题,由于支持partial update,所以BLOB内部就需要维护更新数据的多版本,innodb通过给lob index增加lob versions链表来实现,即lob index不只是连接了前后lob index,串联BLOB页链表,同时lob index维护了对应BLOB页的多版本链表。当对数据进行partial update时,只是对被更新BLOB页增加一个新版本,并将旧版本串联起来,这里面每次partial update都会增加lob version,主键和undo的lob ref指向的都是blob的第一个页 first page,在blob内部基于lob version访问不同版本数据,见下图一次partial update; 8.0 BLOB

5.6 & 5.7 BLOB实现

mysql 5.6 和 5.7 中blob设计实现相对简单,在主键和undo的lob ref指向外部的BLOB页链表数据,我们在此处主要分析下增删改查的关键函数实现。

insert & update

BLOB的 update 操作是创建一个全新的版本,更新主键lob ref到新的BLOB,上版本数据保存在undo的lob ref中;insert操作只是创建一个全新的BLOB数据。BLOB的insert和update操作关键实现函数均是btr_store_big_rec_extern_fields,这个函数实现上就是为每个BLOB数据分配BLOB页,数据写入,将BLOB页串成链表,更新lob ref数据,具体见下面函数注解:

  1. dberr_t btr_store_big_rec_extern_fields(
  2. ... ...)
  3. {
  4. ... ...
  5. // 遍历big_rec_vec,即遍历所有要插入或者更改的blob
  6. for (i = 0; i < big_rec_vec->n_fields; i++) {
  7. field_ref = btr_rec_get_field_ref(
  8. rec, offsets, big_rec_vec->fields[i].field_no);
  9. extern_len = big_rec_vec->fields[i].len;
  10. prev_page_no = FIL_NULL;
  11. // 对每个blob
  12. for (;;) {
  13. buf_block_t* block;
  14. page_t* page;
  15. mtr_start(&mtr);
  16. if (prev_page_no == FIL_NULL) {
  17. hint_page_no = 1 + rec_page_no;
  18. } else {
  19. hint_page_no = prev_page_no + 1;
  20. }
  21. // 分配page
  22. alloc_another:
  23. block = btr_page_alloc(index, hint_page_no,
  24. FSP_NO_DIR, 0, alloc_mtr, &mtr);
  25. page_no = buf_block_get_page_no(block);
  26. page = buf_block_get_frame(block);
  27. // 将 prev page和刚分配的page 连接起来
  28. if (prev_page_no != FIL_NULL) {
  29. buf_block_t* prev_block;
  30. page_t* prev_page;
  31. prev_block = buf_page_get(space_id, zip_size,
  32. prev_page_no,
  33. RW_X_LATCH, &mtr);
  34. buf_block_dbg_add_level(prev_block,
  35. SYNC_EXTERN_STORAGE);
  36. prev_page = buf_block_get_frame(prev_block);
  37. if (page_zip) {
  38. mlog_write_ulint(
  39. prev_page + FIL_PAGE_NEXT,
  40. page_no, MLOG_4BYTES, &mtr);
  41. memcpy(buf_block_get_page_zip(
  42. prev_block)
  43. ->data + FIL_PAGE_NEXT,
  44. prev_page + FIL_PAGE_NEXT, 4);
  45. } else {
  46. mlog_write_ulint(
  47. prev_page + FIL_PAGE_DATA
  48. + BTR_BLOB_HDR_NEXT_PAGE_NO,
  49. page_no, MLOG_4BYTES, &mtr);
  50. }
  51. } else if (dict_index_is_online_ddl(index)) {
  52. row_log_table_blob_alloc(index, page_no);
  53. }
  54. // 压缩页写入blob page
  55. if (page_zip) {
  56. ... ...
  57. } else {
  58. // 非压缩页写入blob page,并和prev page串成链表
  59. mlog_write_ulint(page + FIL_PAGE_TYPE,
  60. FIL_PAGE_TYPE_BLOB,
  61. MLOG_2BYTES, &mtr);
  62. if (extern_len > (UNIV_PAGE_SIZE
  63. - FIL_PAGE_DATA
  64. - BTR_BLOB_HDR_SIZE
  65. - FIL_PAGE_DATA_END)) {
  66. store_len = UNIV_PAGE_SIZE
  67. - FIL_PAGE_DATA
  68. - BTR_BLOB_HDR_SIZE
  69. - FIL_PAGE_DATA_END;
  70. } else {
  71. store_len = extern_len;
  72. }
  73. // 将blob的部分数据写入分配的page中
  74. mlog_write_string(page + FIL_PAGE_DATA
  75. + BTR_BLOB_HDR_SIZE,
  76. (const byte*)
  77. big_rec_vec->fields[i].data
  78. + big_rec_vec->fields[i].len
  79. - extern_len,
  80. store_len, &mtr);
  81. mlog_write_ulint(page + FIL_PAGE_DATA
  82. + BTR_BLOB_HDR_PART_LEN,
  83. store_len, MLOG_4BYTES, &mtr);
  84. mlog_write_ulint(page + FIL_PAGE_DATA
  85. + BTR_BLOB_HDR_NEXT_PAGE_NO,
  86. FIL_NULL, MLOG_4BYTES, &mtr);
  87. extern_len -= store_len;
  88. if (alloc_mtr == &mtr) {
  89. rec_block = buf_page_get(
  90. space_id, zip_size,
  91. rec_page_no,
  92. RW_X_LATCH, &mtr);
  93. buf_block_dbg_add_level(
  94. rec_block,
  95. SYNC_NO_ORDER_CHECK);
  96. }
  97. // 更新blob ref
  98. mlog_write_ulint(field_ref + BTR_EXTERN_LEN, 0,
  99. MLOG_4BYTES, alloc_mtr);
  100. mlog_write_ulint(field_ref
  101. + BTR_EXTERN_LEN + 4,
  102. big_rec_vec->fields[i].len
  103. - extern_len,
  104. MLOG_4BYTES, alloc_mtr);
  105. if (prev_page_no == FIL_NULL) {
  106. btr_blob_dbg_add_blob(
  107. rec, big_rec_vec->fields[i]
  108. .field_no, page_no, index,
  109. "store");
  110. mlog_write_ulint(field_ref
  111. + BTR_EXTERN_SPACE_ID,
  112. space_id, MLOG_4BYTES,
  113. alloc_mtr);
  114. mlog_write_ulint(field_ref
  115. + BTR_EXTERN_PAGE_NO,
  116. page_no, MLOG_4BYTES,
  117. alloc_mtr);
  118. mlog_write_ulint(field_ref
  119. + BTR_EXTERN_OFFSET,
  120. FIL_PAGE_DATA,
  121. MLOG_4BYTES,
  122. alloc_mtr);
  123. }
  124. prev_page_no = page_no;
  125. mtr_commit(&mtr);
  126. if (extern_len == 0) {
  127. break;
  128. }
  129. }
  130. }
  131. }
  132. func_exit:
  133. return(error);
  134. }

delete

由于可以外部存储的BLOB数据,只存在主键上,所以删除blob数据,实际是删除主键,这时只是给主键加了一个delete mark标志,没有实际删除,在undo purge流程中将blob数据实际删除。

purge & rollback

由于mysql 5.6 和 5.7 中对blob数据的更新删除都是整体操作,所以update blob或者删除主键,旧数据的删除均是在undo purge中;rollback操作相对 undo purge,删除的是最新的blob数据,两者在blob操作上是一致的,最终均是调用函数btr_free_externally_stored_field来删除blob数据,该函数实现相对比较简单,遍历blob页,逐个删除页,最后更新lob ref为空FIL_NULL。具体见下面函数注解:

  1. void btr_free_externally_stored_field()
  2. {
  3. ... ...
  4. // 遍历blob所有的页并释放,
  5. for (;;) {
  6. ... ...
  7. page_no = mach_read_from_4(field_ref + BTR_EXTERN_PAGE_NO);
  8. page = buf_block_get_frame(ext_block);
  9. if (ext_zip_size) {
  10. // 压缩页释放blob page
  11. ... ...
  12. } else {
  13. // 非压缩页释放blob page
  14. ut_a(!page_zip);
  15. btr_check_blob_fil_page_type(space_id, page_no, page,
  16. FALSE);
  17. // 记录下一个待释放的page no
  18. next_page_no = mach_read_from_4(
  19. page + FIL_PAGE_DATA
  20. + BTR_BLOB_HDR_NEXT_PAGE_NO);
  21. // 释放当前page
  22. btr_page_free_low(index, ext_block, 0, &mtr);
  23. // 更新blob ref相关数据,page no,extern len
  24. mlog_write_ulint(field_ref + BTR_EXTERN_PAGE_NO,
  25. next_page_no,
  26. MLOG_4BYTES, &mtr);
  27. /* Zero out the BLOB length. If the server
  28. crashes during the execution of this function,
  29. trx_rollback_or_clean_all_recovered() could
  30. dereference the half-deleted BLOB, fetching a
  31. wrong prefix for the BLOB. */
  32. mlog_write_ulint(field_ref + BTR_EXTERN_LEN + 4,
  33. 0,
  34. MLOG_4BYTES, &mtr);
  35. }
  36. /* Commit mtr and release the BLOB block to save memory. */
  37. btr_blob_free(ext_block, TRUE, &mtr);
  38. }
  39. }

fetch

blob数据在更新及读取的时候,会作为一个整体读取出来,具体实现函数是btr_copy_blob_prefix, 该函数遍历BLOB页链表,读取所有数据,见下面函数注解:

  1. ulint btr_copy_blob_prefix(
  2. /*=================*/
  3. byte* buf, /*!< out: the externally stored part of
  4. the field, or a prefix of it */
  5. ulint len, /*!< in: length of buf, in bytes */
  6. ulint space_id,/*!< in: space id of the BLOB pages */
  7. ulint page_no,/*!< in: page number of the first BLOB page */
  8. ulint offset) /*!< in: offset on the first BLOB page */
  9. {
  10. ulint copied_len = 0;
  11. // 循环遍历读出blob所有page
  12. for (;;) {
  13. mtr_t mtr;
  14. buf_block_t* block;
  15. const page_t* page;
  16. const byte* blob_header;
  17. ulint part_len;
  18. ulint copy_len;
  19. mtr_start(&mtr);
  20. block = buf_page_get(space_id, 0, page_no, RW_S_LATCH, &mtr);
  21. buf_block_dbg_add_level(block, SYNC_EXTERN_STORAGE);
  22. page = buf_block_get_frame(block);
  23. btr_check_blob_fil_page_type(space_id, page_no, page, TRUE);
  24. blob_header = page + offset;
  25. part_len = btr_blob_get_part_len(blob_header);
  26. copy_len = ut_min(part_len, len - copied_len);
  27. memcpy(buf + copied_len,
  28. blob_header + BTR_BLOB_HDR_SIZE, copy_len);
  29. copied_len += copy_len;
  30. page_no = btr_blob_get_next_page_no(blob_header);
  31. mtr_commit(&mtr);
  32. if (page_no == FIL_NULL || copy_len != part_len) {
  33. UNIV_MEM_ASSERT_RW(buf, copied_len);
  34. return(copied_len);
  35. }
  36. /* On other BLOB pages except the first the BLOB header
  37. always is at the page data start: */
  38. offset = FIL_PAGE_DATA;
  39. ut_ad(copied_len <= len);
  40. }
  41. }

8.0 blob实现

mysql 8.0中 innob对blob的实现相对复杂一点,下面我们针对8.0 blob的多版本、代码及数据结构、及围绕着增删改查等做进一步的实现说明。

多版本

上面我们已经针对mysql 8.0的blob多版本做了一些介绍,这里我们展开说明下,在mysql 8.0中,blob的多版本应该分为三种情况:

  1. 没有partial update,插入及更新都是全量blob的替换,这种情况blob多版本和5.6 5.7 版本一致,only undo多版本
  2. 有partial update,则在由undo构建到指定主键版本后,需要基于主键上lob ref的lob version在blob内部构建出合适的version, undo多版本+blob多版本
  3. 还有一种情况当针对一个blob页更新的数据小于100字节时,此时即使有partial update,也不再使用,而是直接将更改记录到undo中,这种情况在构建出blob版本后,还需要将undo中记录的blob 更改apply到对应版本的blob上,undo多版本+blob多版本+undo small change多版本apply 8.0 update blob导致blob版本发生变化图解

    代码文件及数据结构介绍

    代码文件

    8.0 blob实现代码主要集中在include/ 及lob/目录,具体文件见下面说明:

    • lob0del.h lob0del.cc lob::Deleter,在purge时,特定场景清理blob对象
    • lob0inf.h 主要是blob insert read update等接口
    • lob0ins.h lob0ins.cc Inserter实现blob的insert
    • lob0lob.h lob0lob.cc blob实现的入口函数,包括外部使用blob的一些函数及一些结构体和宏定义
    • lob0int.h lob0int.cc blob index实现
    • lob0first.h lob0first.cc blob first page实现
    • lob0undo.h lob0undo.cc 实现blob small change记录在undo上时,使用这些undo构建blob的mvcc版本
    • lob0impl.h lob0impl.cc 一些page node定义及一些blob 函数的实现
    • lob0util.h lob0util.cc blob page的数据结构及page的分配
    • lob0zip.h zlob0* 压缩页的相关blob实现

      数据结构

      8.0中blob实现主要的数据结构由first_page_t,index_entry_t及data_page_t组成,其中first_page_t表示first page上数据的组织,blob ref指向的page为frist page;index_entry_t为各个blob data page的索引node,存放在first page及特定的index page上,这些index entry串成一个链表,表示一个blob分配成多个page,index entry有助于快速定位blob中间某个位置,为partial update提供条件,另外每个index entry上会有一个version 列表,表示对应blob page的多个版本,实现blob的多版本;data_page_t为存放blob数据的page。各个数据结构的具体数据组织见下面:

  1. /** The first page of an uncompressed LOB. */
  2. struct first_page_t : public basic_page_t {
  3. /** Version information. One byte. */
  4. static const ulint OFFSET_VERSION = FIL_PAGE_DATA;
  5. /** One byte of flag bits. Currently only one bit (the least
  6. significant bit) is used, other 7 bits are available for future use.*/
  7. static const ulint OFFSET_FLAGS = FIL_PAGE_DATA + 1;
  8. /** LOB version. 4 bytes.*/
  9. static const uint32_t OFFSET_LOB_VERSION = OFFSET_FLAGS + 1;
  10. /** The latest transaction that modified this LOB. */
  11. static const ulint OFFSET_LAST_TRX_ID = OFFSET_LOB_VERSION + 4;
  12. /** The latest transaction undo_no that modified this LOB. */
  13. static const ulint OFFSET_LAST_UNDO_NO = OFFSET_LAST_TRX_ID + 6;
  14. /** Length of data stored in this page. 4 bytes. */
  15. static const ulint OFFSET_DATA_LEN = OFFSET_LAST_UNDO_NO + 4;
  16. /** The trx that created the data stored in this page. */
  17. static const ulint OFFSET_TRX_ID = OFFSET_DATA_LEN + 4;
  18. /** The offset where the list base node is located. This is the list
  19. of LOB pages. */
  20. static const ulint OFFSET_INDEX_LIST = OFFSET_TRX_ID + 6;
  21. /** The offset where the list base node is located. This is the list
  22. of free nodes. */
  23. static const ulint OFFSET_INDEX_FREE_NODES =
  24. OFFSET_INDEX_LIST + FLST_BASE_NODE_SIZE;
  25. /** The offset where the contents of the first page begins. */
  26. static const ulint LOB_PAGE_DATA =
  27. OFFSET_INDEX_FREE_NODES + FLST_BASE_NODE_SIZE;
  28. static const ulint LOB_PAGE_TRAILER_LEN = FIL_PAGE_DATA_END;
  29. ... ...
  30. }
  1. struct index_entry_t {
  2. /** Index entry offsets within node. */
  3. static const ulint OFFSET_PREV = 0;
  4. static const ulint OFFSET_NEXT = OFFSET_PREV + FIL_ADDR_SIZE;
  5. /** Points to base node of the list of versions. The size of base node is
  6. 16 bytes. */
  7. static const ulint OFFSET_VERSIONS = OFFSET_NEXT + FIL_ADDR_SIZE;
  8. /** The creator trx id. */
  9. static const ulint OFFSET_TRXID = OFFSET_VERSIONS + FLST_BASE_NODE_SIZE;
  10. /** The modifier trx id. */
  11. static const ulint OFFSET_TRXID_MODIFIER = OFFSET_TRXID + 6;
  12. static const ulint OFFSET_TRX_UNDO_NO = OFFSET_TRXID_MODIFIER + 6;
  13. /** The undo number of the modifier trx. */
  14. static const ulint OFFSET_TRX_UNDO_NO_MODIFIER = OFFSET_TRX_UNDO_NO + 4;
  15. static const ulint OFFSET_PAGE_NO = OFFSET_TRX_UNDO_NO_MODIFIER + 4;
  16. static const ulint OFFSET_DATA_LEN = OFFSET_PAGE_NO + 4;
  17. /** The LOB version number. */
  18. static const ulint OFFSET_LOB_VERSION = OFFSET_DATA_LEN + 4;
  19. /** Total length of an index node. */
  20. static const ulint SIZE = OFFSET_LOB_VERSION + 4;
  21. ... ...
  22. }
  1. struct data_page_t : public basic_page_t {
  2. static const ulint OFFSET_VERSION = FIL_PAGE_DATA;
  3. static const ulint OFFSET_DATA_LEN = OFFSET_VERSION + 1;
  4. static const ulint OFFSET_TRX_ID = OFFSET_DATA_LEN + 4;
  5. static const ulint LOB_PAGE_DATA = OFFSET_TRX_ID + 6;
  6. }

insert & update

mysql 8.0中 blob,insert操作插入一个整体的BLOB数据,update操作可能会插入一个整体的BLOB数据,也可能进行partial update,但无论哪种情形,最终都是由函数btr_store_big_rec_extern_fields实现,,该函数先将rec上所有blob字段的blob ref标记为beging modified,然后逐个blob根据情况进行insert或者partial update,并根据页的压缩与否有两套不同的insert和partial update函数实现,在插入或者更新完毕后,更新blob ref信息。我们下面对btr_store_big_rec_extern_fields、lob::insert和lob::update函数进行注解:

  1. dberr_t btr_store_big_rec_extern_fields(trx_t *trx, btr_pcur_t *pcur,
  2. const upd_t *upd, ulint *offsets,
  3. const big_rec_t *big_rec_vec,
  4. mtr_t *btr_mtr, opcode op) {
  5. ... ...
  6. /* Create a blob operation context. */
  7. BtrContext btr_ctx(btr_mtr, pcur, index, rec, offsets, rec_block, op);
  8. InsertContext ctx(btr_ctx, big_rec_vec);
  9. // 设置rec中所有blob字段的blob ref标记为beging modified,在函数返回时,该类析构时去掉该标志
  10. Being_modified bm(btr_ctx, big_rec_vec, pcur, offsets, op, btr_mtr);
  11. // 遍历rec中所有的blob 字段,逐个插入或者更新blob data
  12. for (uint i = 0; i < big_rec_vec->n_fields; i++) {
  13. ulint field_no = big_rec_vec->fields[i].field_no;
  14. byte *field_ref = btr_rec_get_field_ref(index, rec, offsets, field_no);
  15. ref_t blobref(field_ref);
  16. // 判断blob是否有能进行partial update字段,如果是则会尝试进行blob partial update,构建blob多版本,
  17. // 如果否,则完成插入一个全新的blob
  18. bool can_do_partial_update = false;
  19. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
  20. big_rec_vec->fields[i].ext_in_old) {
  21. can_do_partial_update = blobref.is_lob_partially_updatable(index);
  22. }
  23. // 压缩页场景的blob插入及更新,此处不详细展开
  24. if (page_zip != nullptr) {
  25. ... ...
  26. } else {
  27. // 普通页类型的blob插入与更新
  28. bool do_insert = true;
  29. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
  30. blobref.is_big(rec_block->page.size) && can_do_partial_update) {
  31. if (upd->is_partially_updated(field_no)) {
  32. /* Do partial update. */
  33. error = lob::update(ctx, trx, index, upd, field_no, blobref);
  34. ... ...
  35. } else {
  36. // 标记旧的blob为不能进行partial update,后面会插入一个全新的blob,包括全新的first page
  37. // 此时旧的blob即可以被purge掉了
  38. blobref.mark_not_partially_updatable(trx, btr_mtr, index,
  39. dict_table_page_size(table));
  40. }
  41. }
  42. // 插入一个全新的blob
  43. if (do_insert) {
  44. error = lob::insert(&ctx, trx, blobref, &big_rec_vec->fields[i], i);
  45. ... ...
  46. }
  47. }
  48. if (error != DB_SUCCESS) {
  49. break;
  50. }
  51. }
  52. return (error);
  53. ... ...
  54. }

blob数据insert根据压缩与否分为insert和z_insert,我们在此主要结合代码分析下非压缩场景,insert函数的实现:insert函数主要是将blob数据按照page切分,存储到多个blob data page中(first page存储开始的一部分data数据),并为每个blob data page建立对应的blob index entry,所有的index entry构成链表

  1. dberr_t insert(InsertContext *ctx, trx_t *trx, ref_t &ref,
  2. big_rec_field_t *field, ulint field_j) {
  3. ... ...
  4. // 此处原本意图是当blob长度不是足够大时,blob存储不再使用lobindex,退化使用5.6 blob页列表的方式
  5. // 但ref_t::is_big always 返回true,所以这部分代码实际走不进去,无效代码路径
  6. if (!ref_t::is_big(page_size, len)) {
  7. /* The LOB is not big enough to build LOB index. Insert the LOB without an
  8. LOB index. */
  9. Inserter blob_writer(ctx);
  10. return blob_writer.write_one_small_blob(field_j);
  11. }
  12. // 分配并初始化first page,直接初始化 10个 lobindex entry,及其他信息
  13. first_page_t first(mtr, index);
  14. buf_block_t *first_block = first.alloc(mtr, ctx->is_bulk());
  15. ... ...
  16. // 写一部分blob data数据到first page
  17. ulint to_write = first.write(trxid, ptr, len);
  18. total_written += to_write;
  19. ulint remaining = len;
  20. {
  21. /* Insert an index entry in LOB index. */
  22. flst_node_t *node = first.alloc_index_entry(ctx->is_bulk());
  23. // 更新一些信息到first index entry,并且设置blob data page为first page
  24. index_entry_t entry(node, mtr, index);
  25. entry.set_page_no(first.get_page_no());
  26. entry.set_data_len(to_write);
  27. entry.set_lob_version(1);
  28. flst_add_last(index_list, node, mtr);
  29. first.set_trx_id(trxid);
  30. first.set_data_len(to_write);
  31. }
  32. ulint nth_blob_page = 0;
  33. const ulint commit_freq = 4;
  34. // 写余下的blob数据到其他blob page,并为每个blob page建立对应的index entry
  35. while (remaining > 0) {
  36. // 分配data page
  37. data_page_t data_page(mtr, index);
  38. buf_block_t *block = data_page.alloc(mtr, ctx->is_bulk());
  39. if (block == nullptr) {
  40. ret = DB_OUT_OF_FILE_SPACE;
  41. break;
  42. }
  43. // 写数据到blob data page
  44. to_write = data_page.write(ptr, remaining);
  45. total_written += to_write;
  46. data_page.set_trx_id(trxid);
  47. /* Allocate a new index entry */
  48. flst_node_t *node = first.alloc_index_entry(ctx->is_bulk());
  49. if (node == nullptr) {
  50. ret = DB_OUT_OF_FILE_SPACE;
  51. break;
  52. }
  53. // 更新信息到index entry
  54. index_entry_t entry(node, mtr, index);
  55. entry.set_page_no(data_page.get_page_no());
  56. entry.set_data_len(to_write);
  57. ... ...
  58. // 添加entry到entry list
  59. entry.push_back(first.index_list());
  60. ut_ad(!entry.get_self().is_equal(entry.get_prev()));
  61. ut_ad(!entry.get_self().is_equal(entry.get_next()));
  62. page_type_t type = fil_page_get_type(block->frame);
  63. ut_a(type == FIL_PAGE_TYPE_LOB_DATA);
  64. // 基于一定频率更新blob ref
  65. if (++nth_blob_page % commit_freq == 0) {
  66. ctx->check_redolog();
  67. ref.set_ref(ctx->get_field_ref(field->field_no));
  68. first.load_x(first_page_id, page_size);
  69. }
  70. }
  71. if (ret == DB_SUCCESS) {
  72. // 更新blob ref
  73. ref.update(space_id, first_page_no, 1, mtr);
  74. ref.set_length(total_written, mtr);
  75. }
  76. return ret;
  77. }

partial update 实现根据页压缩与否分为update和z_update,在此我们主要说非压缩页函数update:

  1. dberr_t update(InsertContext &ctx, trx_t *trx, dict_index_t *index,
  2. const upd_t *upd, ulint field_no, ref_t blobref) {
  3. // 判断此次update是不是small update,改变的字节少于100为small change,small change的更新
  4. // 直接更新blob data,并且undo log记录相应变更,即small change的多版本有undo log来实现,
  5. // 非 small change则需要为对应的page增加新的page版本,即此时blob的多版本由blob的index entry
  6. // 结合多版本blob data page实现
  7. const bool small_change =
  8. (bytes_changed <= ref_t::LOB_SMALL_CHANGE_THRESHOLD);
  9. upd_field_t *uf = upd->get_field_by_field_no(field_no, index);
  10. // 更新信息到first page
  11. first_page_t first_page(mtr, index);
  12. first_page.load_x(first_page_id, page_size);
  13. first_page.set_last_trx_id(trx->id);
  14. first_page.set_last_trx_undo_no(undo_no);
  15. uint32_t lob_version = 0;
  16. // small change不变更blob version,否则增加对应blob version
  17. if (small_change) {
  18. lob_version = first_page.get_lob_version();
  19. } else {
  20. lob_version = first_page.incr_lob_version();
  21. }
  22. for (Binary_diff_vector::const_iterator iter = bdiff_vector->begin();
  23. iter != bdiff_vector->end(); ++iter, ++count) {
  24. const Binary_diff *bdiff = iter;
  25. if (small_change) {
  26. // small change直接更改blob data page的内容
  27. err = replace_inline(ctx, trx, index, blobref, first_page,
  28. bdiff->offset(), bdiff->length(),
  29. (byte *)bdiff->new_data(uf->mysql_field));
  30. } else {
  31. // 非 small change,则需为page增加新的data page,将数据写入新的page,并为新的page
  32. // 分配index entry,将index entry加入entry list的头部
  33. err = replace(ctx, trx, index, blobref, first_page, bdiff->offset(),
  34. bdiff->length(), (byte *)bdiff->new_data(uf->mysql_field),
  35. count);
  36. }
  37. if (err != DB_SUCCESS) {
  38. break;
  39. }
  40. }
  41. blobref.set_offset(lob_version, mtr);
  42. return err;
  43. }

purge & rollback

多版本的purge,最新版本的rollback本质上是一样的,就是在多版本中,去掉一个或者一些版本,只不过purge是最老的一些版本,rollback是最新版本的一个或者一些版本数据,mysql 8.0中blob的purge或者rollback,主要实现函数是lob::purge,该函数根据传入的参数,判断出是rollback还是purge操作,是purge部分blob数据还是整个blob数据,除了一些特殊情况考虑外,整个函数的主要实现就是一个双层while循环,最外一层循环遍历index entry,内层循环遍历index entry的多个版本entry,并基于trx_id和undo_no判断是否可以purge掉,这个判断就决定了是部分数据的purge还是全量数据的purge。下面是lob::purge函数的注解:

  1. // purge函数的一些细节比较多,但多数都有相应的注释,我们这里只说明主要处理逻辑
  2. void purge(DeleteContext *ctx, dict_index_t *index, trx_id_t trxid,
  3. undo_no_t undo_no, ulint rec_type, const upd_field_t *uf,
  4. purge_node_t *purge_node) {
  5. ... ...
  6. // 压缩页purge
  7. if (page_type == FIL_PAGE_TYPE_ZLOB_FIRST) {
  8. z_purge(ctx, index, trxid, undo_no, rec_type, purge_node);
  9. return;
  10. }
  11. // rollback blob操作
  12. if (is_rollback) {
  13. rollback(ctx, index, trxid, undo_no, rec_type, uf);
  14. return;
  15. }
  16. ... ...
  17. // 双重循环遍历所有index entry,及index entry对应的version 列表,全量blob purge和paritial update purge
  18. // 均是在此处实现,基于vers_entry.can_be_purged结合trx_id和undo_no判断是否可以purge对应blob page
  19. while (!fil_addr_is_null(node_loc)) {
  20. flst_node_t *node = first.addr2ptr_x(node_loc);
  21. cur_entry.reset(node);
  22. flst_base_node_t *vers = cur_entry.get_versions_list();
  23. fil_addr_t ver_loc = flst_get_first(vers, &lob_mtr);
  24. /* Scan the older versions. */
  25. while (!fil_addr_is_null(ver_loc)) {
  26. flst_node_t *ver_node = first.addr2ptr_x(ver_loc);
  27. index_entry_t vers_entry(ver_node, &lob_mtr, index);
  28. if (vers_entry.can_be_purged(trxid, undo_no)) {
  29. ver_loc = vers_entry.purge_version(index, vers, free_list);
  30. } else {
  31. ver_loc = vers_entry.get_next();
  32. }
  33. }
  34. node_loc = cur_entry.get_next();
  35. cur_entry.reset(nullptr);
  36. /* Ensure that the parent mtr (btr_mtr) and the child mtr (lob_mtr)
  37. do not make conflicting modifications. */
  38. ut_ad(!lob_mtr.conflicts_with(mtr));
  39. mtr_commit(&lob_mtr);
  40. mtr_start(&lob_mtr);
  41. lob_mtr.set_log_mode(log_mode);
  42. first.load_x(page_id, page_size);
  43. }
  44. // purge完后更新blob ref
  45. ref.set_page_no(FIL_NULL, mtr);
  46. ref.set_length(0, mtr);
  47. }

fetch

读取及update操作都要对blob数据进行读取,由上面我们知,mysql 8.0的blob多版本是分三种情况的,第一和第二种情况,blob数据的读取,具体实现均是由函数lob::read实现(lob::z_read读取压缩页不再详细说明)。第三种情况是结合lob::read函数读取出来的blob数据,apply undo_vers_t中的small change数据构建出需要的blob版本数据。这里我们详细说明下lob::read函数,该函数读取一个由blob ref标明的lob version的blob,函数的核心逻辑是外层循环遍历blob index entry列表,内部循环为每个entry选择合适的版本数据,读取出来。 下面是lob::read函数注解:

  1. ulint read(ReadContext *ctx, ref_t ref, ulint offset, ulint len, byte *buf) {
  2. ... ...
  3. // 遍历lob index entry列表
  4. while (!fil_addr_is_null(node_loc) && want > 0) {
  5. old_version.reset(nullptr);
  6. node = first_page.addr2ptr_s_cache(cached_blocks, node_loc);
  7. cur_entry.reset(node);
  8. cur_entry.read(entry_mem);
  9. const uint32_t entry_lob_version = cur_entry.get_lob_version();
  10. // 如果entry第一个版本大于lob ref的lob_version,则遍历该entry的version列表,找到符合版本要求的index entry(对应的version<=lob_version)
  11. if (entry_lob_version > lob_version) {
  12. flst_base_node_t *ver_list = cur_entry.get_versions_list();
  13. /* Look at older versions. */
  14. fil_addr_t node_versions = flst_get_first(ver_list, &mtr);
  15. // 遍历entry version列表找到合适的版本
  16. while (!fil_addr_is_null(node_versions)) {
  17. flst_node_t *node_old_version =
  18. first_page.addr2ptr_s_cache(cached_blocks, node_versions);
  19. old_version.reset(node_old_version);
  20. old_version.read(entry_mem);
  21. const uint32_t old_lob_version = old_version.get_lob_version();
  22. if (old_lob_version <= lob_version) {
  23. /* The current trx can see this
  24. entry. */
  25. break;
  26. }
  27. node_versions = old_version.get_next();
  28. old_version.reset(nullptr);
  29. }
  30. }
  31. page_no_t read_from_page_no = FIL_NULL;
  32. // 将数据从该entry对应的data page中读取出来
  33. if (old_version.is_null()) {
  34. read_from_page_no = cur_entry.get_page_no();
  35. } else {
  36. read_from_page_no = old_version.get_page_no();
  37. }
  38. actual_read = 0;
  39. if (read_from_page_no != FIL_NULL) {
  40. if (read_from_page_no == first_page_no) {
  41. actual_read = first_page.read(page_offset, ptr, want);
  42. ptr += actual_read;
  43. want -= actual_read;
  44. } else {
  45. buf_block_t *block = buf_page_get(
  46. page_id_t(ctx->m_space_id, read_from_page_no), ctx->m_page_size,
  47. RW_S_LATCH, UT_LOCATION_HERE, &data_mtr);
  48. data_page_t page(block, &data_mtr);
  49. actual_read = page.read(page_offset, ptr, want);
  50. ptr += actual_read;
  51. want -= actual_read;
  52. page_type_t type = page.get_page_type();
  53. ut_a(type == FIL_PAGE_TYPE_LOB_DATA);
  54. if (++data_pages_count % commit_freq == 0) {
  55. mtr_commit(&data_mtr);
  56. mtr_start(&data_mtr);
  57. }
  58. }
  59. }
  60. total_read += actual_read;
  61. page_offset = 0;
  62. node_loc = cur_entry.get_next();
  63. }
  64. mtr_commit(&mtr);
  65. mtr_commit(&data_mtr);
  66. return total_read;
  67. }

参考

  1. https://dev.mysql.com/blog-archive/externally-stored-fields-in-innodb/
  2. https://dev.mysql.com/blog-archive/mysql-8-0-innodb-introduces-lob-index-for-faster-updates/
  3. https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/

原文:http://mysql.taobao.org/monthly/2022/09/01/