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页链表的第一个页见,下图: 
mysql 5.6 & 5.7 中针对BLOB数据的修改,会创建一个全新的BLOB数据,将主键上lob ref指向新的BLOB数据,undo中对应的lob ref会保存旧的BLOB数据。这样的实现,使得 5.6 和 5.7 中BLOB数据的多版本,完全是由undo来实现,blob只是由lob ref指向定位到数据,可参考下图: 
8.0 BLOB
由于mysql 5.6 和5.7中,所有针对blob数据的修改,必须整体替换一个BLOB对象,而BLOB对象占据的空间相对比较大,这样的实现方式一是造成空间的浪费,二是修改效率的降低。针对这些问题,在mysql 8.0中 innodb对BLOB进行了重新设计实现:给BLOB页链表加上索引lob index,这样可以快速定位到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; 
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. ... ...
6. // 遍历big_rec_vec,即遍历所有要插入或者更改的blob
7. for (i = 0; i < big_rec_vec->n_fields; i++) {
8. field_ref = btr_rec_get_field_ref(
9. rec, offsets, big_rec_vec->fields[i].field_no);
10. extern_len = big_rec_vec->fields[i].len;
11. prev_page_no = FIL_NULL;
14. // 对每个blob
15. for (;;) {
16. buf_block_t* block;
17. page_t* page;
19. mtr_start(&mtr);
21. if (prev_page_no == FIL_NULL) {
22. hint_page_no = 1 + rec_page_no;
23. } else {
24. hint_page_no = prev_page_no + 1;
25. }
26. // 分配page
27. alloc_another:
28. block = btr_page_alloc(index, hint_page_no,
29. FSP_NO_DIR, 0, alloc_mtr, &mtr);
31. page_no = buf_block_get_page_no(block);
32. page = buf_block_get_frame(block);
34. // 将 prev page和刚分配的page 连接起来
35. if (prev_page_no != FIL_NULL) {
36. buf_block_t* prev_block;
37. page_t* prev_page;
39. prev_block = buf_page_get(space_id, zip_size,
40. prev_page_no,
41. RW_X_LATCH, &mtr);
42. buf_block_dbg_add_level(prev_block,
43. SYNC_EXTERN_STORAGE);
44. prev_page = buf_block_get_frame(prev_block);
46. if (page_zip) {
47. mlog_write_ulint(
48. prev_page + FIL_PAGE_NEXT,
49. page_no, MLOG_4BYTES, &mtr);
50. memcpy(buf_block_get_page_zip(
51. prev_block)
52. ->data + FIL_PAGE_NEXT,
53. prev_page + FIL_PAGE_NEXT, 4);
54. } else {
55. mlog_write_ulint(
56. prev_page + FIL_PAGE_DATA
57. + BTR_BLOB_HDR_NEXT_PAGE_NO,
58. page_no, MLOG_4BYTES, &mtr);
59. }
61. } else if (dict_index_is_online_ddl(index)) {
62. row_log_table_blob_alloc(index, page_no);
63. }
65. // 压缩页写入blob page
66. if (page_zip) {
67. ... ...
68. } else {
69. // 非压缩页写入blob page,并和prev page串成链表
70. mlog_write_ulint(page + FIL_PAGE_TYPE,
71. FIL_PAGE_TYPE_BLOB,
72. MLOG_2BYTES, &mtr);
74. if (extern_len > (UNIV_PAGE_SIZE
75. - FIL_PAGE_DATA
76. - BTR_BLOB_HDR_SIZE
77. - FIL_PAGE_DATA_END)) {
78. store_len = UNIV_PAGE_SIZE
79. - FIL_PAGE_DATA
80. - BTR_BLOB_HDR_SIZE
81. - FIL_PAGE_DATA_END;
82. } else {
83. store_len = extern_len;
84. }
86. // 将blob的部分数据写入分配的page中
87. mlog_write_string(page + FIL_PAGE_DATA
88. + BTR_BLOB_HDR_SIZE,
89. (const byte*)
90. big_rec_vec->fields[i].data
91. + big_rec_vec->fields[i].len
92. - extern_len,
93. store_len, &mtr);
94. mlog_write_ulint(page + FIL_PAGE_DATA
95. + BTR_BLOB_HDR_PART_LEN,
96. store_len, MLOG_4BYTES, &mtr);
97. mlog_write_ulint(page + FIL_PAGE_DATA
98. + BTR_BLOB_HDR_NEXT_PAGE_NO,
99. FIL_NULL, MLOG_4BYTES, &mtr);
101. extern_len -= store_len;
103. if (alloc_mtr == &mtr) {
104. rec_block = buf_page_get(
105. space_id, zip_size,
106. rec_page_no,
107. RW_X_LATCH, &mtr);
108. buf_block_dbg_add_level(
109. rec_block,
110. SYNC_NO_ORDER_CHECK);
111. }
113. // 更新blob ref
114. mlog_write_ulint(field_ref + BTR_EXTERN_LEN, 0,
115. MLOG_4BYTES, alloc_mtr);
116. mlog_write_ulint(field_ref
117. + BTR_EXTERN_LEN + 4,
118. big_rec_vec->fields[i].len
119. - extern_len,
120. MLOG_4BYTES, alloc_mtr);
122. if (prev_page_no == FIL_NULL) {
123. btr_blob_dbg_add_blob(
124. rec, big_rec_vec->fields[i]
125. .field_no, page_no, index,
126. "store");
128. mlog_write_ulint(field_ref
129. + BTR_EXTERN_SPACE_ID,
130. space_id, MLOG_4BYTES,
131. alloc_mtr);
133. mlog_write_ulint(field_ref
134. + BTR_EXTERN_PAGE_NO,
135. page_no, MLOG_4BYTES,
136. alloc_mtr);
138. mlog_write_ulint(field_ref
139. + BTR_EXTERN_OFFSET,
140. FIL_PAGE_DATA,
141. MLOG_4BYTES,
142. alloc_mtr);
143. }
145. prev_page_no = page_no;
147. mtr_commit(&mtr);
149. if (extern_len == 0) {
150. break;
151. }
152. }
153. }
154. }
156. func_exit:
157. return(error);
158. }
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);
10. if (ext_zip_size) {
11. // 压缩页释放blob page
12. ... ...
13. } else {
14. // 非压缩页释放blob page
15. ut_a(!page_zip);
16. btr_check_blob_fil_page_type(space_id, page_no, page,
17. FALSE);
19. // 记录下一个待释放的page no
20. next_page_no = mach_read_from_4(
21. page + FIL_PAGE_DATA
22. + BTR_BLOB_HDR_NEXT_PAGE_NO);
24. // 释放当前page
25. btr_page_free_low(index, ext_block, 0, &mtr);
27. // 更新blob ref相关数据,page no,extern len
28. mlog_write_ulint(field_ref + BTR_EXTERN_PAGE_NO,
29. next_page_no,
30. MLOG_4BYTES, &mtr);
31. /* Zero out the BLOB length. If the server
32. crashes during the execution of this function,
33. trx_rollback_or_clean_all_recovered() could
34. dereference the half-deleted BLOB, fetching a
35. wrong prefix for the BLOB. */
36. mlog_write_ulint(field_ref + BTR_EXTERN_LEN + 4,
37. 0,
38. MLOG_4BYTES, &mtr);
39. }
41. /* Commit mtr and release the BLOB block to save memory. */
42. btr_blob_free(ext_block, TRUE, &mtr);
43. }
44. }
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;
20. mtr_start(&mtr);
22. block = buf_page_get(space_id, 0, page_no, RW_S_LATCH, &mtr);
23. buf_block_dbg_add_level(block, SYNC_EXTERN_STORAGE);
24. page = buf_block_get_frame(block);
26. btr_check_blob_fil_page_type(space_id, page_no, page, TRUE);
28. blob_header = page + offset;
29. part_len = btr_blob_get_part_len(blob_header);
30. copy_len = ut_min(part_len, len - copied_len);
32. memcpy(buf + copied_len,
33. blob_header + BTR_BLOB_HDR_SIZE, copy_len);
34. copied_len += copy_len;
36. page_no = btr_blob_get_next_page_no(blob_header);
38. mtr_commit(&mtr);
40. if (page_no == FIL_NULL || copy_len != part_len) {
41. UNIV_MEM_ASSERT_RW(buf, copied_len);
42. return(copied_len);
43. }
45. /* On other BLOB pages except the first the BLOB header
46. always is at the page data start: */
48. offset = FIL_PAGE_DATA;
50. ut_ad(copied_len <= len);
51. }
52. }
8.0 blob实现
mysql 8.0中 innob对blob的实现相对复杂一点,下面我们针对8.0 blob的多版本、代码及数据结构、及围绕着增删改查等做进一步的实现说明。
多版本
上面我们已经针对mysql 8.0的blob多版本做了一些介绍,这里我们展开说明下,在mysql 8.0中,blob的多版本应该分为三种情况:
- 没有partial update,插入及更新都是全量blob的替换,这种情况blob多版本和5.6 5.7 版本一致,only undo多版本
- 有partial update,则在由undo构建到指定主键版本后,需要基于主键上lob ref的lob version在blob内部构建出合适的version, undo多版本+blob多版本
- 还有一种情况当针对一个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;
6. /** One byte of flag bits. Currently only one bit (the least
7. significant bit) is used, other 7 bits are available for future use.*/
8. static const ulint OFFSET_FLAGS = FIL_PAGE_DATA + 1;
10. /** LOB version. 4 bytes.*/
11. static const uint32_t OFFSET_LOB_VERSION = OFFSET_FLAGS + 1;
13. /** The latest transaction that modified this LOB. */
14. static const ulint OFFSET_LAST_TRX_ID = OFFSET_LOB_VERSION + 4;
16. /** The latest transaction undo_no that modified this LOB. */
17. static const ulint OFFSET_LAST_UNDO_NO = OFFSET_LAST_TRX_ID + 6;
19. /** Length of data stored in this page. 4 bytes. */
20. static const ulint OFFSET_DATA_LEN = OFFSET_LAST_UNDO_NO + 4;
22. /** The trx that created the data stored in this page. */
23. static const ulint OFFSET_TRX_ID = OFFSET_DATA_LEN + 4;
25. /** The offset where the list base node is located. This is the list
26. of LOB pages. */
27. static const ulint OFFSET_INDEX_LIST = OFFSET_TRX_ID + 6;
29. /** The offset where the list base node is located. This is the list
30. of free nodes. */
31. static const ulint OFFSET_INDEX_FREE_NODES =
32. OFFSET_INDEX_LIST + FLST_BASE_NODE_SIZE;
34. /** The offset where the contents of the first page begins. */
35. static const ulint LOB_PAGE_DATA =
36. OFFSET_INDEX_FREE_NODES + FLST_BASE_NODE_SIZE;
38. static const ulint LOB_PAGE_TRAILER_LEN = FIL_PAGE_DATA_END;
39. ... ...
40. }
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;
6. /** Points to base node of the list of versions. The size of base node is
7. 16 bytes. */
8. static const ulint OFFSET_VERSIONS = OFFSET_NEXT + FIL_ADDR_SIZE;
10. /** The creator trx id. */
11. static const ulint OFFSET_TRXID = OFFSET_VERSIONS + FLST_BASE_NODE_SIZE;
13. /** The modifier trx id. */
14. static const ulint OFFSET_TRXID_MODIFIER = OFFSET_TRXID + 6;
15. static const ulint OFFSET_TRX_UNDO_NO = OFFSET_TRXID_MODIFIER + 6;
17. /** The undo number of the modifier trx. */
18. static const ulint OFFSET_TRX_UNDO_NO_MODIFIER = OFFSET_TRX_UNDO_NO + 4;
19. static const ulint OFFSET_PAGE_NO = OFFSET_TRX_UNDO_NO_MODIFIER + 4;
20. static const ulint OFFSET_DATA_LEN = OFFSET_PAGE_NO + 4;
22. /** The LOB version number. */
23. static const ulint OFFSET_LOB_VERSION = OFFSET_DATA_LEN + 4;
25. /** Total length of an index node. */
26. static const ulint SIZE = OFFSET_LOB_VERSION + 4;
27. ... ...
28. }
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) {
6. ... ...
7. /* Create a blob operation context. */
8. BtrContext btr_ctx(btr_mtr, pcur, index, rec, offsets, rec_block, op);
9. InsertContext ctx(btr_ctx, big_rec_vec);
11. // 设置rec中所有blob字段的blob ref标记为beging modified,在函数返回时,该类析构时去掉该标志
12. Being_modified bm(btr_ctx, big_rec_vec, pcur, offsets, op, btr_mtr);
15. // 遍历rec中所有的blob 字段,逐个插入或者更新blob data
16. for (uint i = 0; i < big_rec_vec->n_fields; i++) {
17. ulint field_no = big_rec_vec->fields[i].field_no;
18. byte *field_ref = btr_rec_get_field_ref(index, rec, offsets, field_no);
19. ref_t blobref(field_ref);
21. // 判断blob是否有能进行partial update字段,如果是则会尝试进行blob partial update,构建blob多版本,
22. // 如果否,则完成插入一个全新的blob
23. bool can_do_partial_update = false;
24. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
25. big_rec_vec->fields[i].ext_in_old) {
26. can_do_partial_update = blobref.is_lob_partially_updatable(index);
27. }
29. // 压缩页场景的blob插入及更新,此处不详细展开
30. if (page_zip != nullptr) {
31. ... ...
32. } else {
33. // 普通页类型的blob插入与更新
34. bool do_insert = true;
36. if (op == lob::OPCODE_UPDATE && upd != nullptr &&
37. blobref.is_big(rec_block->page.size) && can_do_partial_update) {
38. if (upd->is_partially_updated(field_no)) {
39. /* Do partial update. */
40. error = lob::update(ctx, trx, index, upd, field_no, blobref);
41. ... ...
42. } else {
43. // 标记旧的blob为不能进行partial update,后面会插入一个全新的blob,包括全新的first page
44. // 此时旧的blob即可以被purge掉了
45. blobref.mark_not_partially_updatable(trx, btr_mtr, index,
46. dict_table_page_size(table));
47. }
48. }
50. // 插入一个全新的blob
51. if (do_insert) {
52. error = lob::insert(&ctx, trx, blobref, &big_rec_vec->fields[i], i);
53. ... ...
54. }
55. }
57. if (error != DB_SUCCESS) {
58. break;
59. }
60. }
61. return (error);
62. ... ...
63. }
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. }
13. // 分配并初始化first page,直接初始化 10个 lobindex entry,及其他信息
14. first_page_t first(mtr, index);
15. buf_block_t *first_block = first.alloc(mtr, ctx->is_bulk());
16. ... ...
18. // 写一部分blob data数据到first page
19. ulint to_write = first.write(trxid, ptr, len);
20. total_written += to_write;
21. ulint remaining = len;
23. {
24. /* Insert an index entry in LOB index. */
25. flst_node_t *node = first.alloc_index_entry(ctx->is_bulk());
27. // 更新一些信息到first index entry,并且设置blob data page为first page
28. index_entry_t entry(node, mtr, index);
29. entry.set_page_no(first.get_page_no());
30. entry.set_data_len(to_write);
31. entry.set_lob_version(1);
32. flst_add_last(index_list, node, mtr);
34. first.set_trx_id(trxid);
35. first.set_data_len(to_write);
36. }
38. ulint nth_blob_page = 0;
39. const ulint commit_freq = 4;
41. // 写余下的blob数据到其他blob page,并为每个blob page建立对应的index entry
42. while (remaining > 0) {
43. // 分配data page
44. data_page_t data_page(mtr, index);
45. buf_block_t *block = data_page.alloc(mtr, ctx->is_bulk());
47. if (block == nullptr) {
48. ret = DB_OUT_OF_FILE_SPACE;
49. break;
50. }
52. // 写数据到blob data page
53. to_write = data_page.write(ptr, remaining);
54. total_written += to_write;
55. data_page.set_trx_id(trxid);
57. /* Allocate a new index entry */
58. flst_node_t *node = first.alloc_index_entry(ctx->is_bulk());
60. if (node == nullptr) {
61. ret = DB_OUT_OF_FILE_SPACE;
62. break;
63. }
65. // 更新信息到index entry
66. index_entry_t entry(node, mtr, index);
67. entry.set_page_no(data_page.get_page_no());
68. entry.set_data_len(to_write);
69. ... ...
70. // 添加entry到entry list
71. entry.push_back(first.index_list());
73. ut_ad(!entry.get_self().is_equal(entry.get_prev()));
74. ut_ad(!entry.get_self().is_equal(entry.get_next()));
76. page_type_t type = fil_page_get_type(block->frame);
77. ut_a(type == FIL_PAGE_TYPE_LOB_DATA);
79. // 基于一定频率更新blob ref
80. if (++nth_blob_page % commit_freq == 0) {
81. ctx->check_redolog();
82. ref.set_ref(ctx->get_field_ref(field->field_no));
83. first.load_x(first_page_id, page_size);
84. }
85. }
87. if (ret == DB_SUCCESS) {
88. // 更新blob ref
89. ref.update(space_id, first_page_no, 1, mtr);
90. ref.set_length(total_written, mtr);
91. }
93. return ret;
94. }
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) {
4. // 判断此次update是不是small update,改变的字节少于100为small change,small change的更新
5. // 直接更新blob data,并且undo log记录相应变更,即small change的多版本有undo log来实现,
6. // 非 small change则需要为对应的page增加新的page版本,即此时blob的多版本由blob的index entry
7. // 结合多版本blob data page实现
8. const bool small_change =
9. (bytes_changed <= ref_t::LOB_SMALL_CHANGE_THRESHOLD);
11. upd_field_t *uf = upd->get_field_by_field_no(field_no, index);
13. // 更新信息到first page
14. first_page_t first_page(mtr, index);
15. first_page.load_x(first_page_id, page_size);
16. first_page.set_last_trx_id(trx->id);
17. first_page.set_last_trx_undo_no(undo_no);
18. uint32_t lob_version = 0;
20. // small change不变更blob version,否则增加对应blob version
21. if (small_change) {
22. lob_version = first_page.get_lob_version();
23. } else {
24. lob_version = first_page.incr_lob_version();
25. }
27. for (Binary_diff_vector::const_iterator iter = bdiff_vector->begin();
28. iter != bdiff_vector->end(); ++iter, ++count) {
29. const Binary_diff *bdiff = iter;
31. if (small_change) {
32. // small change直接更改blob data page的内容
33. err = replace_inline(ctx, trx, index, blobref, first_page,
34. bdiff->offset(), bdiff->length(),
35. (byte *)bdiff->new_data(uf->mysql_field));
37. } else {
38. // 非 small change,则需为page增加新的data page,将数据写入新的page,并为新的page
39. // 分配index entry,将index entry加入entry list的头部
40. err = replace(ctx, trx, index, blobref, first_page, bdiff->offset(),
41. bdiff->length(), (byte *)bdiff->new_data(uf->mysql_field),
42. count);
43. }
45. if (err != DB_SUCCESS) {
46. break;
47. }
48. }
50. blobref.set_offset(lob_version, mtr);
52. return err;
53. }
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. }
12. // rollback blob操作
13. if (is_rollback) {
14. rollback(ctx, index, trxid, undo_no, rec_type, uf);
15. return;
16. }
18. ... ...
20. // 双重循环遍历所有index entry,及index entry对应的version 列表,全量blob purge和paritial update purge
21. // 均是在此处实现,基于vers_entry.can_be_purged结合trx_id和undo_no判断是否可以purge对应blob page
22. while (!fil_addr_is_null(node_loc)) {
23. flst_node_t *node = first.addr2ptr_x(node_loc);
24. cur_entry.reset(node);
26. flst_base_node_t *vers = cur_entry.get_versions_list();
27. fil_addr_t ver_loc = flst_get_first(vers, &lob_mtr);
29. /* Scan the older versions. */
30. while (!fil_addr_is_null(ver_loc)) {
31. flst_node_t *ver_node = first.addr2ptr_x(ver_loc);
32. index_entry_t vers_entry(ver_node, &lob_mtr, index);
34. if (vers_entry.can_be_purged(trxid, undo_no)) {
35. ver_loc = vers_entry.purge_version(index, vers, free_list);
36. } else {
37. ver_loc = vers_entry.get_next();
38. }
39. }
41. node_loc = cur_entry.get_next();
42. cur_entry.reset(nullptr);
44. /* Ensure that the parent mtr (btr_mtr) and the child mtr (lob_mtr)
45. do not make conflicting modifications. */
46. ut_ad(!lob_mtr.conflicts_with(mtr));
47. mtr_commit(&lob_mtr);
48. mtr_start(&lob_mtr);
49. lob_mtr.set_log_mode(log_mode);
50. first.load_x(page_id, page_size);
51. }
53. // purge完后更新blob ref
54. ref.set_page_no(FIL_NULL, mtr);
55. ref.set_length(0, mtr);
56. }
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);
7. node = first_page.addr2ptr_s_cache(cached_blocks, node_loc);
8. cur_entry.reset(node);
10. cur_entry.read(entry_mem);
12. const uint32_t entry_lob_version = cur_entry.get_lob_version();
14. // 如果entry第一个版本大于lob ref的lob_version,则遍历该entry的version列表,找到符合版本要求的index entry(对应的version<=lob_version)
15. if (entry_lob_version > lob_version) {
16. flst_base_node_t *ver_list = cur_entry.get_versions_list();
17. /* Look at older versions. */
18. fil_addr_t node_versions = flst_get_first(ver_list, &mtr);
19. // 遍历entry version列表找到合适的版本
20. while (!fil_addr_is_null(node_versions)) {
21. flst_node_t *node_old_version =
22. first_page.addr2ptr_s_cache(cached_blocks, node_versions);
24. old_version.reset(node_old_version);
26. old_version.read(entry_mem);
28. const uint32_t old_lob_version = old_version.get_lob_version();
30. if (old_lob_version <= lob_version) {
31. /* The current trx can see this
32. entry. */
33. break;
34. }
35. node_versions = old_version.get_next();
36. old_version.reset(nullptr);
37. }
38. }
40. page_no_t read_from_page_no = FIL_NULL;
41. // 将数据从该entry对应的data page中读取出来
42. if (old_version.is_null()) {
43. read_from_page_no = cur_entry.get_page_no();
44. } else {
45. read_from_page_no = old_version.get_page_no();
46. }
48. actual_read = 0;
49. if (read_from_page_no != FIL_NULL) {
50. if (read_from_page_no == first_page_no) {
51. actual_read = first_page.read(page_offset, ptr, want);
52. ptr += actual_read;
53. want -= actual_read;
55. } else {
56. buf_block_t *block = buf_page_get(
57. page_id_t(ctx->m_space_id, read_from_page_no), ctx->m_page_size,
58. RW_S_LATCH, UT_LOCATION_HERE, &data_mtr);
60. data_page_t page(block, &data_mtr);
61. actual_read = page.read(page_offset, ptr, want);
62. ptr += actual_read;
63. want -= actual_read;
65. page_type_t type = page.get_page_type();
66. ut_a(type == FIL_PAGE_TYPE_LOB_DATA);
68. if (++data_pages_count % commit_freq == 0) {
69. mtr_commit(&data_mtr);
70. mtr_start(&data_mtr);
71. }
72. }
73. }
75. total_read += actual_read;
76. page_offset = 0;
77. node_loc = cur_entry.get_next();
78. }
80. mtr_commit(&mtr);
81. mtr_commit(&data_mtr);
82. return total_read;
83. }
参考
- https://dev.mysql.com/blog-archive/externally-stored-fields-in-innodb/
- https://dev.mysql.com/blog-archive/mysql-8-0-innodb-introduces-lob-index-for-faster-updates/
- https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/
