InnoDB Record 细节整理
背景
InnoDB 作为目前 MySQL 的主要存储引擎,其中 record 细节繁琐,这里仅做整理以便查阅. 版本基于 MySQL-8.0.25.
数据结构
InnoDB record 的逻辑格式: dtuple_t
/** Structure for an SQL data tuple of fields (logical record) */
struct dtuple_t {
/* ... */
/** Number of fields in dtuple */
ulint n_fields; /* 当前 dtuple 记录的字段数量. */
/** number of fields which should be used in comparison services of rem0cmp.*;
the index search is performed by comparing only these fields, others are
ignored; the default value in dtuple creation is the same value as n_fields */
ulint n_fields_cmp; /* 当前 dtuple 中可以用来比较的字段数量, 可以通过
* dtuple_set_n_fields_cmp() 设置. */
/** Fields. */
dfield_t *fields; /* 当前 dtuple 的字段内容. */
/** Structure for an SQL data field */
struct dfield_t {
void *data; /*!< pointer to data */
unsigned ext : 1; /*!< TRUE=externally stored, FALSE=local */
unsigned spatial_status : 2;
/*!< spatial status of externally stored field
in undo log for purge */
unsigned len; /*!< data length; UNIV_SQL_NULL if SQL null 数据长度 */
dtype_t type; /*!< type of data 数据类型*/
/* ... */
} */
/** ... */
/** Compare a data tuple to a physical record.
* dtuple_t 与 rec_t 的比较函数. */
int compare(const rec_t *rec, const dict_index_t *index, const ulint *offsets,
ulint *matched_fields) const;
/** ... */
};MySQL SQL 层的 record 可以通过 row_sel_convert_mysql_key_to_innobase() 转换为 InnoDB 可识别的 dtuple_t 结构.
索引内存结构: dict_index_t
- index->table->n_cols: table 的列数,包含用户定义的列 + 3 列系统列(DB_ROW_ID, DB_TRX_ID, DB_ROLL_PTR).
- index->table->cols: 存上面 n_cols 个列的数组, 系统列在倒数后3个.
- index->n_fields: 当前索引包含的列数,小于等于上面的 index->table->n_cols.
- index->fields: 记录当前索引 column 的描述信息, 列名,长度, 顺序 or 倒序
Record Node:
-
对于主键索引 leaf node:
-
- 如果定义了主键, 那么系统列就没有 DB_ROW_ID,那么此时 n_fields 比 n_cols 小 1.
-
- 如果没有定义主键, 那么系统列就包含 DB_ROW_ID,那么此时 n_fields 和 n_cols 值一样.
-
-
对于主键索引 non-leaf node:
-
- n_fields 包含所有唯一字段 + Page Number, 数量为 index->n_uniq + 1.
-
-
对于二级索引 leaf node:
-
- n_fields 就是包含二级索引定义的列数 + 主键列数.
-
-
对于二级索引 non-leaf node:
-
- n_fields 就是包含二级索引定义的列数 + 主键列数 + Page Number, 数量为 index->n_fields + 1.
-
使用 dict_index_build_node_ptr() 构建 non-leaf node:
InnoDB 物理 record: rec_t
offsets 数组由 rec_get_offsets(), 数组大小由 n_fields + 1 + REC_OFFS_HEADER_SIZE 决定.
- offsets[0] = n_alloc /* n_alloc 是数组元素个数. */
- offsets[1] = n_fields /* n_fields 是 record 列数. */
- offsets[2] = extra size
- offsets[3.. 3 + n_fields] /* 记录每个 field 的结束偏移. */
rec_t 可以直接通过 cmp_dtuple_rec_with_match_low() 与 dtuple_t 比较:
rec_t 可以通过 offsets 数组分别获取对应的 filed 字段, 再与 ((dfield_t *)tuple->fields + n) 直接进行比较.
B-tree 游标: btr_pcur_t
btr_pcur_t 是在 search 或者 modify 过程中用来定位的游标, 其中记录定位信息, 可以直接通过 store_position() 来保存,通过 restore_position() 可以恢复至上一次保存 record 位置信息.
struct btr_pcur_t {
/** ... */
/* 保存 pcur 记录的信息. */
void store_position(mtr_t *mtr);
/* 恢复出来上一次 pcur 保存的位置. */
bool restore_position(ulint latch_mode, mtr_t *mtr, const char *file,
ulint line);
/** pcur 定位的元信息: index, block, n_fileds ... */
btr_cur_t m_btr_cur;
/** true if old_rec is stored */
bool m_old_stored{false};
/* 保存当前 pcur 指向的 record. */
rec_t *m_old_rec{nullptr};
/* 记录 m_old_rec 的 filed 数量. */
ulint m_old_n_fields{0};
/* 记录数据 page 的 modify clock. */
uint64_t m_modify_clock{0};
/** ... */
}store_position() 保存位置信息:
void btr_pcur_t::store_position(mtr_t *mtr) {
ut_ad(m_pos_state == BTR_PCUR_IS_POSITIONED);
ut_ad(m_latch_mode != BTR_NO_LATCHES);
auto block = get_block();
auto index = btr_cur_get_index(get_btr_cur());
auto page_cursor = get_page_cur();
/* pcur 指向的 record. */
auto rec = page_cur_get_rec(page_cursor);
/* record 所在的 page. */
auto page = page_align(rec);
/* record 在 page 上的 offset. */
auto offs = page_offset(rec);
/* ... */
if (page_rec_is_supremum_low(offs)) {
/* pcur 指向的是一个 supremum record, 则保存前一个 record. */
rec = page_rec_get_prev(rec);
m_rel_pos = BTR_PCUR_AFTER;
} else if (page_rec_is_infimum_low(offs)) {
/* pcur 指向的是一个 infimum record, 则保存后一个 record. */
rec = page_rec_get_next(rec);
m_rel_pos = BTR_PCUR_BEFORE;
} else {
/* pcur 指向的是一个 user record, 直接保存这个 record. */
m_rel_pos = BTR_PCUR_ON;
}
m_old_stored = true;
/* 保存当前指向的 record 至 m_old_rec. */
m_old_rec = dict_index_copy_rec_order_prefix(index, rec, &m_old_n_fields,
&m_old_rec_buf, &m_buf_size);
m_block_when_stored.store(block);
/* Function try to check if block is S/X latch. */
/* 记录 modify clock. */
m_modify_clock = buf_block_get_modify_clock(block);
}-
如果 pcur 指向一个 supremum record, 保存 supremum record 前的一个 record, m_rel_pos 为 BTR_PCUR_AFTER.
-
如果 pcur 指向一个 infimum record, 保存 infimum record 后的一个 record, m_rel_pos 为 BTR_PCUR_BEFORE.
-
如果 pcur 指向一个 user record, 保存 user record, m_rel_pos 为 BTR_PCUR_ON.
restore_position() 先尝试乐观加锁,即直接判断 m_modify_clock 是否变化,假如 b+ tree 发生了 SMO, 需要进行悲观加锁的方式,即通过 btr_cur_search_to_nth_level() 重新 search 加锁:
bool btr_pcur_t::restore_position(ulint latch_mode, mtr_t *mtr,
const char *file, ulint line) {
dtuple_t *tuple;
page_cur_mode_t mode;
ut_ad(mtr->is_active());
ut_ad(m_old_stored);
ut_ad(is_positioned());
auto index = btr_cur_get_index(get_btr_cur());
/* ... */
ut_a(m_old_rec != nullptr);
ut_a(m_old_n_fields > 0);
/* Optimistic latching involves S/X latch not required for
intrinsic table instead we would prefer to search fresh. */
if ((latch_mode == BTR_SEARCH_LEAF || latch_mode == BTR_MODIFY_LEAF ||
latch_mode == BTR_SEARCH_PREV || latch_mode == BTR_MODIFY_PREV) &&
!m_btr_cur.index->table->is_intrinsic()) {
/* Try optimistic restoration. */
/* 乐观恢复. */
if (m_block_when_stored.run_with_hint([&](buf_block_t *hint) {
return hint != nullptr && btr_cur_optimistic_latch_leaves(
hint, m_modify_clock, &latch_mode,
&m_btr_cur, file, line, mtr);
})) {
m_pos_state = BTR_PCUR_IS_POSITIONED;
m_latch_mode = latch_mode;
buf_block_dbg_add_level(get_block(), dict_index_is_ibuf(index)
? SYNC_IBUF_TREE_NODE
: SYNC_TREE_NODE);
if (m_rel_pos == BTR_PCUR_ON) {
#ifdef UNIV_DEBUG
/* ... */
#endif /* UNIV_DEBUG */
return (true);
}
/* This is the same record as stored,
may need to be adjusted for BTR_PCUR_BEFORE/AFTER,
depending on search mode and direction. */
if (is_on_user_rec()) {
m_pos_state = BTR_PCUR_IS_POSITIONED_OPTIMISTIC;
}
return (false);
}
}
/* If optimistic restoration did not succeed, open the cursor anew */
auto heap = mem_heap_create(256);
tuple = dict_index_build_data_tuple(index, m_old_rec, m_old_n_fields, heap);
/* Save the old search mode of the cursor */
auto old_mode = m_search_mode;
/* 根据 store_position() 时记录的 m_rel_pos 采用不同的 search mode. */
switch (m_rel_pos) {
case BTR_PCUR_ON:
mode = PAGE_CUR_LE;
break;
case BTR_PCUR_AFTER:
mode = PAGE_CUR_G;
break;
case BTR_PCUR_BEFORE:
mode = PAGE_CUR_L;
break;
default:
ut_error;
}
/* 乐观恢复 pcur 失败,就要通过 btr_cur_search_to_nth_level 来重新定位 pcur. */
open_no_init(index, tuple, mode, latch_mode, 0, mtr, file, line);
/* Restore the old search mode */
m_search_mode = old_mode;
ut_ad(m_rel_pos == BTR_PCUR_ON || m_rel_pos == BTR_PCUR_BEFORE ||
m_rel_pos == BTR_PCUR_AFTER);
if (m_rel_pos == BTR_PCUR_ON && is_on_user_rec() &&
!cmp_dtuple_rec(
tuple, get_rec(), index,
rec_get_offsets(get_rec(), index, nullptr, ULINT_UNDEFINED, &heap))) {
/* We have to store the NEW value for the modify clock,
since the cursor can now be on a different page!
But we can retain the value of old_rec */
auto block = get_block();
m_block_when_stored.store(block);
m_modify_clock = buf_block_get_modify_clock(block);
m_old_stored = true;
mem_heap_free(heap);
return (true);
}
mem_heap_free(heap);
/* We have to store new position information, modify_clock etc.,
to the cursor because it can now be on a different page, the record
under it may have been removed, etc. */
store_position(mtr);
return (false);
}-
对于 BTR_SEARCH_LEAF,BTR_MODIFY_LEAF,BTR_SEARCH_PREV,BTR_MODIFY_PREV 四种 latch mode, 可以尝试乐观 restore_position().
-
针对悲观 restore_position 的情况:
- 如果
store_position()时记录的 m_rel_pos 为 BTR_PCUR_ON, 则store_position()时为一个 user record, 采用 PAGE_CUR_LE 的 search mode, 恢复至最后一个小于等于 user record(old) 的 record. - 如果
store_position()时记录的 m_rel_pos 为 BTR_PCUR_AFTER, 则store_position()时为一个 supremum record, 采用 PAGE_CUR_G 的 search mode,store_position()时 pcur 保存的是 supremum record 前的 record(old), 所以恢复至大于 record(old) 的 record. (所以可能定位在大于 record(old) 的下一个 user record, 也可能是store_position()当时定位的 supremum record). - 如果
store_position()时记录的 m_rel_pos 为 BTR_PCUR_BEFORE, 则store_position()时为一个 infimum record, 采用 PAGE_CUR_L 的 search mode,store_position()时 pcur 保存的是 infimum record 后的 record(old), 所以恢复至小于 record(old) 的 record. (所以可能定位在小于 record(old) 的上一个 user record, 也可能是store_position()当时定位的 infimum record).
- 如果
store_position() 会记录 buf_block_t, 在乐观恢复中直接通过尝试对 buf_block_t 加锁,当前的 Buffer Pool 支持动态 resize, 这部分的内存可能会被释放, 所以 InnoDB 会首先判断这个 buf_block_t 指针是否存在于 Buffer Pool 的 chunk 中:
void Block_hint::buffer_fix_block_if_still_valid() {
if (m_block != nullptr) {
const buf_pool_t *const pool = buf_pool_get(m_page_id);
rw_lock_t *latch = buf_page_hash_lock_get(pool, m_page_id);
rw_lock_s_lock(latch);
/* If not own buf_pool_mutex, page_hash can be changed. */
latch = buf_page_hash_lock_s_confirm(latch, pool, m_page_id);
if (buf_is_block_in_instance(pool, m_block) &&
m_page_id == m_block->page.id &&
buf_block_get_state(m_block) == BUF_BLOCK_FILE_PAGE) {
buf_block_buf_fix_inc(m_block, __FILE__, __LINE__);
} else {
clear();
}
rw_lock_s_unlock(latch);
}
}游标 cursor 的 up_match 和 low_match 分别代表在 search 阶段与目标 record 相等的 fileds 的数目.
游标 cursor 的搜索模式
-
PAGE_CUR_G: > 查询,查询第一个大于 dtuple 的 rec_t.
-
PAGE_CUR_GE: >=,> 查询,查询第一个大于等于 dtuple 的 rec_t.
- 如果搜索一个存在的 user record, 使用 PAGE_CUR_GE 可能定位在这个 user record 的 previous page 的 supremum record.
-
PAGE_CUR_L: < 查询,查询最后一个小于 dtuple 的 rec_t.
-
PAGE_CUR_LE: <= 查询,查询最后一个小于等于 dtuple 的 rec_t.
- 如果搜索一个不存在的 user record, 使用 PAGE_CUR_LE 返回最后一个小于 dtuple 的 record.