如果单纯的在内存中创建一个B+树,是比较简单的事,因为内存可以较快速度的进行随机存储。树的数据结构又像是多分支的无环链表,所以对树的调整就是对各节点的引用修改。
而数据库mysql是工作在磁盘上的,磁盘的随机存储性能非常低,所以mysql不能像内存中那样去创建一个树。
磁盘的顺序写速度接近内存的随机写,所以mysql充分的利用了磁盘顺序写的特性进行优化。
mysql构建了段、区的概念用于在顺序读写的前提下创建B+树。
mysql创建B+树分为了三大部分:段的创建、区的分配、页的分配。
注:创建与分配在mysql中的区别
创建:凭空产生一个
分配:不同于创建,从无到有,是本身就有,划分出去。
为什么叫分配,而不是创建? 不同于内存操作,mysql在初始化空间时,即使没用到,也会提前初始化,只是这些对象都是0值,也就是空闲对象。
比如mysql从磁盘上划分了一块空间,刚好是10个页,但现在用不上都是空的,就先都初始化0值,这片空间虽然有页的结构,但其实都是空的,当有数据要写如时,mysql就从这10个空闲页中分配一个去写数据。
段叫创建,是因为段不是物理空间概念,只是逻辑概念,并不会有一个段的连续空间,有需要创建时,才会创建。
区和页,对应的是一个连续空间,这些空间会提前申请好,然后设为0值,等待写入真正的数据。
源码入口
在源码storage/innobase/btr目录 btr0btr.cc文件中
btr_create方法就是B+树的创建入口
入口方法内十分简单,主要就是创建了两个段。
一个是非叶子节点段,
一个是叶子节点段。
通过两个段,mysql将聚集索引和数据节点分离了。
在新创建的树中,mysql为了节约磁盘空间,两个段的操作都在一个页中,这个页也就是根页。
根页是mysql最重要的概念,mysql将根页固定为pageNo=3(也就是第四个页,pageNo从0开始),这么做也是为了方便后续操作,能快速查找到根页。
如果根页满了,会将数据拷贝到新页,根页始终是B+树的根节点。
注:mysql的命名习惯
fsp_xx开头的方法,表示从space表空间进行操作
fseg_xxx开头的方法,表示从segment段内进行操作,如从段内分配页,分配区...
cpp
第一个段创建,索引段,参数PAGE_BTR_SEG_TOP为非叶子节点段
返回了段头被分配的页block, 在mysql源码中,block通常表示在内存中的页, 磁盘中的页称为page
block = fseg_create(space, 0,
PAGE_HEADER + PAGE_BTR_SEG_TOP, mtr);
第二个创建段,PAGE_BTR_SEG_LEAF表示为叶子节点段。
源码没有接受返回的block,是因为分配到了同一个页(根页)就没必要创建重复变量了。
fseg_create(space, page_no,
PAGE_HEADER + PAGE_BTR_SEG_LEAF, mtr)
页的创建, 从block 转变为 page, 对页初始化并写到了磁盘
page = page_create(block, mtr,
dict_table_is_comp(index->table),
dict_index_is_spatial(index));
设置页的上下指针(维护链表)
btr_page_set_next(page, page_zip, FIL_NULL, mtr);
btr_page_set_prev(page, page_zip, FIL_NULL, mtr);
段的创建
fseg_create为段的创建方法,实际调用为fseg_create_general
这个方法会有个page参数,外部可指定page,如果指定了,就在这个页上创建段,如果没指定程序会通过fseg_alloc_free_page_low分配一个空闲页。
mysql对段的定义就是inode对象。
创建段,就等于是创建inode对象。
cpp
# fseg_create_general
如果外部指定了页,就将页加载出来到内存, 如果没有,就从段中分配一个空闲页
if (page != 0) {
block = buf_page_get(page_id_t(space_id, page), page_size,
RW_SX_LATCH, mtr);
}else{
block = fseg_alloc_free_page_low(space, page_size,
inode, 0, FSP_UP, RW_SX_LATCH,
mtr, mtr)
}
获取表空间头
space_header = fsp_get_space_header(space_id, page_size, mtr);
从表空间中分配一个空闲的inode对象,也就是inode页中。
inode = fsp_alloc_seg_inode(space_header, mtr);
获取最新的段id,作为自己的id
seg_id = mach_read_from_8(space_header + FSP_SEG_ID);
然后更新最新的段id,给下个段用
mlog_write_ull(space_header + FSP_SEG_ID, seg_id + 1, mtr);
mlog_write_ull是mysql写入数据的方法,分别对inode对象写入段id和其他属性
mlog_write_ull(inode + FSEG_ID, seg_id, mtr);
mlog_write_ulint(inode + FSEG_NOT_FULL_N_USED, 0, MLOG_4BYTES, mtr);
....
在这个页的头部,写入inode地址(页号+偏移量),方面后续查找到inode段对象。
mlog_write_ulint(header + FSEG_HDR_OFFSET,
page_offset(inode), MLOG_2BYTES, mtr);
mlog_write_ulint(header + FSEG_HDR_PAGE_NO,
page_get_page_no(page_align(inode)),
MLOG_4BYTES, mtr);
mlog_write_ulint(header + FSEG_HDR_SPACE, space_id, MLOG_4BYTES, mtr);
区的分配
区的分配有两个方法,一个是从表空间分配,另一个是从段内。
从表空间分配
cpp
fsp_alloc_free_extent
从表空间分配一个空闲区
header = fsp_get_space_header(space_id, page_size, mtr); 获取表空间头
descr = xdes_get_descriptor_with_space_hdr(
header, space_id, hint, mtr, false, &desc_block); 在表空间头中根据页号获取对应的xdes对象
如果xdes对象非空闲 !=XDES_FREE
first = flst_get_first(header + FSP_FREE, mtr); 获取空闲列表头节点
fsp_fill_free_list(false, space, header, mtr); 创建新区,放到FSP_FREE列表中
first = flst_get_first(header + FSP_FREE, mtr); 再次获取头节点,这次就必定是空闲区了
descr = xdes_lst_get_descriptor(
space_id, page_size, first, mtr); 拿到xdes对象
移除FSP_FREE的一个节点,空闲长度减1
flst_remove(header + FSP_FREE, descr + XDES_FLST_NODE, mtr);
space->free_len--;
返回xdes对象
从段分配
cpp
fseg_alloc_free_extent
从段中分配一个空闲区
若段内的区空闲列表不为空
flst_get_len(inode + FSEG_FREE) > 0
first = flst_get_first(inode + FSEG_FREE, mtr); 从FSEG_FREE空闲列表取首节点
descr = xdes_lst_get_descriptor(space, page_size, first, mtr); 获取首节点的xdes对象
FSEG_FREE空闲列表,如果空闲列表不为空,就从空闲列表分配区,
如果空闲列表为空,就从表空间分配一个区
若段内的区空闲列表为空
descr = fsp_alloc_free_extent(space, page_size, 0, mtr); 从表空间内分配一个区
获取段id(seg_id),初始化xdes属性,
将区加入到FSEG_FREE列表
fseg_fill_free_list(inode, space, page_size,
xdes_get_offset(descr) + FSP_EXTENT_SIZE,
mtr); 判断段内空间是否不足,如果不足给段内填充新的空闲列表(4个)
返回xdes对象
区扩容
fsp_fill_free_list
创建新区
fsp_try_extend_data_file(space, header, mtr) 扩展ibd文件,如果当前区的页数少于64页,则补全页
如果当前区是256区组里的第一个区,需要将第一页初始化未xdes页
如果不是,找到区对应的xdes对象,初始化该xdes对象。
将区加入到FSP_FREE链表
页的分配
从表空间分配页
fsp_alloc_free_page 从表空间分配一个空闲页,表在刚开始时,没有直接初始化段和区,而是直接在表空间分配页,当页超过32页时,就开始初始化段和区。
cpp
获取表空间头
header = fsp_get_space_header(space, page_size, mtr);
1、获取区信息
判断入参是否有指定page的位置,如果有,就从指定hit从获取区descr
first = flst_get_first(header + FSP_FREE_FRAG, mtr); 获取第一个区(空闲页的碎片区)
如果获取失败,就分配 一个区descr = fsp_alloc_free_extent(space, page_size, hint, mtr);
descr = xdes_lst_get_descriptor(space, page_size,first, mtr); 获取first节点信息
2、从区中获取空闲页
free = xdes_find_bit(descr, XDES_FREE_BIT, TRUE, hint % FSP_EXTENT_SIZE, mtr);
遍历区中所有页,查找一个空闲页
先从hit~end遍历,然后再从0~hit遍历,如果外部有指定地址hit,则从hit开始向后查找,再从0到hit查找剩余页
3、初始化页
page_no = xdes_get_offset(descr) + free; 获得pageNo页号 区地址+页地址
fsp_alloc_from_free_frag(header, descr, free, mtr); 更新xdes区对象中的链表信息
fsp_page_create 将page写入磁盘
从段中分配页
上面用到了一个从段中分配空闲页的方法fseg_alloc_free_page_low
从段中分配一个空闲页,这是一个智能化的分配接口,会尽可能的减少碎片化页
cpp
# fseg_alloc_free_page_low
获取段id
seg_id = mach_read_from_8(seg_inode + FSEG_ID);
计算段的剩余空间, reserved:段中总页, used已使用的页。 空闲页数 = reserved - used
reserved = fseg_n_reserved_pages_low(seg_inode, &used, mtr);
获取表空间头
space_header = fsp_get_space_header(space_id, page_size, mtr);
这里是先去找到一个区XDES, descr就是XDES对象
descr = xdes_get_descriptor_with_space_hdr(space_header, space_id,
hint, mtr);
没找到就从这里继续找
descr = xdes_get_descriptor(space_id, hint, page_size, mtr);
下面就是一堆条件分支了,总结起来:
1、通过hit找到的区刚好属于这个段,刚好空闲
2、找到的区是一个空闲区; 同时本段内的空闲空间已不足1/8; 段内已使用空间已经大于32页(FSEG_FRAG_LIMIT)
将这个区分配到该段
3、direction不为FSP_NO_DIR; 同时本段内的空闲空间已不足1/8; 段内已使用空间已经大于32页(FSEG_FRAG_LIMIT)
fseg_alloc_free_extent(seg_inode, space_id, page_size, mtr) 从段中分配一个空闲区
FSEG_FREE空闲列表,如果空闲列表不为空,就从空闲列表分配区,
如果空闲列表为空,就从表空间分配一个区
4、找到的区未满,该区页刚好属于该段
直接从这个区分配一个空闲页
5、reserved - used > 0 段中有空闲空间
先找空闲区
first = flst_get_first(seg_inode + FSEG_NOT_FULL, mtr); 从未满的链表找
first = flst_get_first(seg_inode + FSEG_FREE, mtr); 不满足再从空闲链表找
再找空闲页
ret_descr = xdes_lst_get_descriptor(space_id, page_size, first, mtr); 获取xdes信息
ret_page = xdes_get_offset(ret_descr)
+ xdes_find_bit(ret_descr, XDES_FREE_BIT, TRUE,
0, mtr);
在区中的bitmap遍历空闲页
6、used < FSEG_FRAG_LIMIT 已使用空间小于32页
从表空间分配一个页 fsp_alloc_free_page
将页写入到段的slot(最大32个槽位)
7、else
进入else,说明段空间已经不足,需要分配新的区
申请一个新的区,然后返回第一个页
ret_descr = fseg_alloc_free_extent
ret_page = xdes_get_offset(ret_descr);
fsp_page_create 将page写入磁盘