5 mysql源码中B+树的构建

如果单纯的在内存中创建一个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写入磁盘
相关推荐
侯喵喵3 小时前
Jetson orin agx配置ultralytics 使用docker或conda
yolo·docker·1024程序员节·ultralytics
星火科技探索实验室3 小时前
【实战经验】飞牛云 如何使用 SSD 缓存加速?
1024程序员节
千百元3 小时前
java 程序Apache log4j JDBCAppender SQL注入漏洞(CVE-2022-23305)
1024程序员节
开心-开心急了3 小时前
Kivy 乒乓游戏教程 基于Minconda或Anconda 运行
python·conda·1024程序员节·kivy
望获linux5 小时前
【Linux基础知识系列:第一百五十九篇】磁盘健康监测:smartctl
linux·前端·数据库·chrome·python·操作系统·软件
西部风情5 小时前
聊聊并发、在线、TPS
android·java·数据库
小彭律师5 小时前
Docker/K8s部署MySQL的创新实践与优化技巧大纲
mysql·docker·kubernetes
诗句藏于尽头6 小时前
自动签到之实现掘金模拟签到
python·1024程序员节
csdn_aspnet8 小时前
在 MacOS 中安装 MySQL 8
mysql·macos