深度 | 解析InnoDB引擎

一、综述

innodb的物理文件包括系统表空间文件ibdata,用户表空间文件ibd,日志文件ib_logfile,临时表空间文件ibtmp,undo独立表空间等。

  • 系统表空间是innodb最重要的文件,它记录包括元数据信息,事务系统信息,ibuf信息,double write等关键信息。

  • 用户表空间文件通常分为两类,一类是当innodb_file_per_table打开时,一个用户表空间对应一个文件,另外一种则是5.7版本引入的所谓General Tablespace,在满足一定约束条件下,可以将多个表创建到同一个文件中。

  • 日志文件主要用于记录redo log。innodb在所有数据变更前,先写redo日志。为保证redo日志原子写入,日志通常以512字节的block单位写入。但由于现代文件系统升级,block_size通常设置到了4k,因此innodb也提供了一个选项支持redo日志以4k为单位写入。

  • 临时表空间文件用于存储所有非压缩的临时表,第1~32个临时表专用的回滚段也存放在该文件中。由于临时表的本身属性,该文件在重启时会重新创建。

  • undo独立表空间是innodb的一个可选项,由innodb_undo_tablespaces配置。默认情况下,该值为0,即undo数据是存储在ibdata中。innodb_undo_tablespaces 设置为非0,可使得undo 回滚段分配到不同的文件中,目前开启undo tablespace 只能在install阶段进行。

上述文件除日志文件外,都具有较为统一的物理结构。所有物理文件由页(page 或 block)构成,在未被压缩情况下,一个页的大小为UNIV_PAGE_SIZE(16384,16K)。不同用途的页具有相同格式的页头(38)和页尾(8),其中记录了页面校验值,页面编号,表空间编号,LSN等通用信息,详见下表。所有page通过一定方式组织起来,下面我们分别从物理结构,逻辑结构,文件管理过程来具体了解innodb的文件结构。

FIL Header / Trailer
checksum 校验值 (4)
offset 页面编号 (4)
previous page 前页编号(4)
next page 后页编号 (4)
lsn for last modification 最后修改的lsn (8)
page type 页面类型 (4)
flush lsn 刷盘lsn,只在page 0保存 (4)
space id 表空间编号 (4)
old-style checksum 页尾校验值 (4)
low 32bits of lsn lsn的低4字节 (4)

二、文件物理结构

2.1 基本物理结构

innodb 的每个数据文件都归属于一个表空间(tablespace),不同的表空间使用一个唯一标识的space id来标记。值得注意的是,系统表空间ibdata虽然包括不同文件ibdata1, ibdata2…,但这些文件逻辑上是相连的,这些文件同属于space_id为0的表空间。

表空间内部,所有页按照区(extent)为物理单元进行划分和管理。extent内所有页面物理相邻。对于不同的page size,对应的extent大小也不同,对应为:

page size extent size
4 KiB 256 pages = 1 MiB
8 KiB 128 pages = 1 MiB
16 KiB 64 pages = 1 MiB
32 KiB 64 pages = 2 MiB
64 KiB 64 pages = 4 MiB

通常情况下,extent由64个物理连续的页组成,表空间可以理解为由一个个物理相邻的extent组成。为了组织起这些extent,每个extent都有一个占40字节的XDES entry。 利用XDES entry,我们可以方便地了解到该extent每页空闲与否,以及其当前状态。其 格式如下:

所有XDES entry都统一放在extent描述页中,一个extent描述页至多存放256个XDES entry,用于管理其随后物理相邻的256个extent(256*64 = 16384 page),如下图所示所示:

 

由图可见,每个XDES entry有严格对应的页面,其对应页面上下界可以描述为:

min_scope = extent 描述页 page_no + xdes 编号 * 64
max_scope =( extent 描述页 page_no + xdes 编号 * 64 )+63

值得注意的是,其中 page 0的extent描述页还记录了与该table space相关的信息(FSP HEADER),其类型为FIL_PAGE_TYPE_FSP_HDR。 其他extent描述页的类型相同,为FIL_PAGE_TYPE_XDES。

2.2 系统数据页

系统表空间(ibdata)不仅存放了SYS_TABLE / SYS_INDEX 等系统表的数据,还存放了回滚信息(undo),插入缓冲索引页(IBUF bitmap),系统事务信息(trx_sys),二次写缓冲(double write)等信息。

innodb中核心的数据都存放在ibdata中的系统数据页中。系统数据页主要包括:FIL_PAGE_TYPE_FSP_HDR, FIL_PAGE_IBUF_BITMAP, FIL_PAGE_TYPE_SYS, IBUF_ROOT_PAGE, FIL_PAGE_TYPE_TRX_SYS, FIL_PAGE_TYPE_SYS, DICT_HDR_PAGE等。

  • FIL_PAGE_TYPE_FSP_HDR/FIL_PAGE_TYPE_XDES
    extent描述页(page 0/16384/32768/… ),上文已述及,故不再展开。

  • FIL_PAGE_IBUF_BITMAP
    ibdata第2个page类型为FIL_PAGE_IBUF_BITMAP,主要用于跟踪随后的每个page的change buffer信息。由于bitmap page的空间有限,同样每隔256个extent Page之后,也会在XDES PAGE之后创建一个ibuf bitmap page。

  • FIL_PAGE_INODE
    ibdata的第3个page的类型为FIL_PAGE_INODE,用于管理数据文件中的segment,每个inode页可以存储FSP_SEG_INODES_PER_PAGE(默认为85)个记录。segment是表空间管理的逻辑单位,每个索引占用2个segment,分别用于管理叶子节点和非叶子节点。关于segment的详细介绍,将在第三节展开。

  • FSP_IBUF_HEADER_PAGE_NO 和 FSP_IBUF_TREE_ROOT_PAGE_NO
    上述两个页分别是Ibdata的第4个page和第5个page。change buffer本质上也是btree结构,其root页固定在第5个page FSP_IBUF_TREE_ROOT_PAGE_NO。由于FSP_IBUF_TREE_ROOT_PAGE_NO中原先用于记录leaf inode entry的字段被用于维护空闲page链表了,因此ibdata需要使用第4页FSP_IBUF_TREE_ROOT_PAGE_NO 来对ibuf进行空间管理。

  • FSP_TRX_SYS_PAGE_NO
    ibdata第6个page的类型为FSP_TRX_SYS_PAGE_NO,记录了innodb重要的事务系统信息,包括持久化的最大事务ID,以及128个rseg(rollback segment)的地址,double write位置等。这128个rseg中,rseg0固定在ibdata中,rseg1-rseg32用于管理临时表,rseg33-rseg128 当未开启undo独立表空间 (innodb undo tablespace = 0)时,仍放在ibdata中,否则放在undo独立表空间中。每个rseg中记录了1024个slot,每个slot也都可对应一个事务,用于管理该事务的undo记录。由于每个slot也需要申请和释放page,因此每个slot也对应一个segment(空间管理逻辑单位)。

  • FSP_DICT_HDR_PAGE_NO
    ibdata第8个page的类型为FSP_DICT_HDR_PAGE_NO,用来存储数据词典表的信息 。该页存储了SYS_TABLES,SYS_TABLE_IDS,SYS_COLUMNS,SYS_INDEXES和SYS_FIELDS的root page,以及当前最大的TABLE_ID/ROW_ID/INDEX_ID/SPACE_ID。当对用户表操作时,需要先从数据字典表中获取到用户表对应的表空间,以及其索引root页的page_no,才能定位到具体数据的位置,对其进行增删改查。(只有拿到数据词典表,才能根据其中存储的表信息,进一步找到其对应的表空间,以及表的聚集索引所在的page no)

  • double write buffer
    innodb使用double write buffer来防止数据页的部分写问题,在写一个数据页之前,总是先写double write buffer,再写数据文件。当崩溃恢复时,如果数据文件中page损坏,会尝试从dblwr中恢复。double write buffer总共128个page,划分为两个block。由于dblwr在安装实例时已经初始化好了,这两个block在Ibdata中具有固定的位置,page64 ~127 划属第一个block,page 128 ~191划属第二个block。

当innodb_file_per_table为off状态时,所有用户表也将和SYS_TABLE / SYS_INDEX 等系统表一样,存储在ibdata中。当开启innodb_file_per_table时,innodb会为每一个用户表建立一个独立的ibd文件。该ibd文件存放了对应用户表的索引数据和插入缓冲bitmap。而该表的回滚数据(undo)仍记录在ibdata中。

三、文件逻辑结构

3.1 基本逻辑结构

innodb为了组织各extent,在表空间的第一个page还维护了三个extent的链表:FSP_FREE、FSP_FREE_FRAG、FSP_FULL_FRAG。分别将extent完全未被使用,部分被使用,完全被使用的Xdes entry串联起来。如下图所示:

段(segment 或称 inode)是用来管理物理文件的逻辑单位,可以向表空间申请分配和释放page或extent,是构成索引,回滚段的基本元素。为节省空间,每个segment都先从表空间FREE_FRAG中分配32个页(FSEG_FRAG_ARR),当这些32个页面不够使用时。按照以下原则进行扩展:如果当前小于1个extent,则扩展到1个extent满;当表空间小于32MB时,每次扩展一个extent;大于32MB时,每次扩展4个extent。

在为segment分配空闲的extent时,如果表空间FSP_FREE上没有空闲的extent,则会为FSP_FREE重新初始化一些空闲extent。extent的分配类似于实现了一套借还机制。segment向表空间租借extent,只有segment退还该空间时,该extent才能重新出现在FSP_FREE/FSP_FULL_FRAG/FSP_FULL中。

segment内部为了管理起这些分配来的extent。也有三个extent链表:FSEG_FREE、FSEG_NOT_FULL、FSEG_FULL,也分别对应extent完全未被使用,部分被使用,完全被使用的Xdes entry。segment的结构如下图所示

inode entry是用于管理segment的结构,一个inode entry对应一个segment。segment的32个页(FSEG_FRAG_ARR),FSEG_FREE、FSEG_NOT_FULL、FSEG_FULL等信息都记录在inode entry中。inode entry的具体结构如下表所示:

inode entry所在的inode page有可能存放满,因此又通过头page(FIL_PAGE_TYPE_FSP_HDR)中维护了两个inode Page链表FSP_SEG_INODES_FULL和FSP_SEG_INODES_FREE。 前者对应没有空闲inode entry的inode page链表,后者对应的至少有一个空闲inode entry的inode page链表,如下图所示:

3.2 索引

ibd文件中真正构建起用户数据的结构是btree。表中的每一个索引对应一个btree。主键(cluster index)对应btree的叶子节点上记录了行的全部列数据(加上transaction id列及rollback ptr)。当表中无主键时,innodb会为该表每一行分配一个唯一的rowID,并基于它构造btree。如果表中存在二级索引(secondary index),那么其btree叶子节点存储了键值加上cluster index索引键值。

每个btree使用两个Segment来管理数据页,一个管理叶子节点(leaf segment),一个管理非叶子节点(non-leaf segment)。这两个segment的inode entry地址记录在btree的root page中。root page分配在non-leaf segment第一个碎片页上(FSEG_FRAG_ARR)。

当对一个表进行增删改查的操作时,我们首先需要从ibdata的第8页FSP_DICT_HDR_PAGE_NO中load改表的元数据信息,从SYS_INDEXES表中获取该表各索引对应的root page no,进而通过root page对这个表的用户数据btree进行操作。表空间的逻辑结构如下图所示:

3.3 索引页数据

索引最基本的页类型为FIL_PAGE_INDEX,其结构如下表所示。Index Header中记录了page所在btree层次,所属index ID,page directory槽数等与页面相关的信息。Fseg Header中记录了该index的leaf-segment和non-leaf segment的inode entry,system records包括infimum和supremum,分别代表该页最小、最大记录虚拟记录。page directory是页内记录的索引。btree只能检索到记录所在的page,page内的检索需要使用到通过page directory构建起的二分查找。

Index Page
FILHeader  (38)
Index Header (36)
Fseg Header (20)
System Records (26)
User Records
Free Space
Page Directory
FIL trailer (8)

innodb按行存放数据。当前MySQL支持等行格式包括antelope(compact和redundant),和barracuda(dynamic和compressed)。barracuda与antelope主要区别在于其处理行外数据等方式,barracuda只存储行外数据等地址指针,不像antelope一样存放768字节的行前缀内容。以compact行格式为例介绍行格式的具体内容,如下图所示,行由变长字段长度列表、NULL标志位、记录头信息、系统列、用户列组成。记录头信息中存放删除标志、列总数、下行相对偏移等信息、系统列包括rowID、transactionID、rollback pointer等组成。

四、文件管理过程

下面用精简后的源码来简单介绍innodb文件的管理过程。

4.1 btree的创建过程

btree的创建过程可以概括为:先创建non_leaf segment,利用non_leaf segment的首页(即32个碎片页中第一页)作为root page;然后创建leaf_segment;最后对root page进行必要的初始化。详细过程请参考以下代码:

btr_create(
    ulint            type,
    ulint            space,
    const page_size_t&    page_size,
    index_id_t        index_id,
    dict_index_t*        index,
    const btr_create_t*    btr_redo_create_info,
    mtr_t*            mtr)
{
    /* index tree 的segment headers 存储于新分配的root page中,ibuf tree的
    segment headers放在独立的ibuf header page中。以下代码屏蔽了ibuf tree的
    创建逻辑,重点介绍index tree的创建过程 */

    /* 局部变量 */
    ...

    /* 创建一个non_leaf segment段,并将段的地址存储到段首页偏移为
    PAGE_HEADER + PAGE_BTR_SEG_TOP的位置,用block记录下non_leaf segment
    段首页page对应的block,该block将作为该btree的root page */
    block = fseg_create(space, 0,
                   PAGE_HEADER + PAGE_BTR_SEG_TOP, mtr);

    if (block == NULL) {

        return(FIL_NULL);
    }

    /* 记录下root page的信息 */
    page_no = block->page.id.page_no();
    frame = buf_block_get_frame(block);

    /* 创建leaf_segment,并将段首存储到root page上偏移为
    PAGE_HEADER + PAGE_BTR_SEG_LEAF的位置 */
    if (!fseg_create(space, page_no,
             PAGE_HEADER + PAGE_BTR_SEG_LEAF, mtr)) {

        /* 没有足够的空间分配新的segment,需要释放掉已分配的root page */
        btr_free_root(block, mtr);
        return(FIL_NULL);
    }

    /* 在root page上做index page的初始化,根据页面压缩与否做不同处理 */
    page_zip = buf_block_get_page_zip(block);
    if (page_zip) {
        /* 其他逻辑 */
        page = page_create_zip(block, index, 0, 0, NULL, mtr);
    } else {
        /* 其他逻辑 */
        page = page_create(block, mtr,
                   dict_table_is_comp(index->table),
                   dict_index_is_spatial(index));
    }

    /* 在root page上设置其所在的index id */
    btr_page_set_index_id(page, page_zip, index_id, mtr);

    /* 将root page的前后页面设置为NULL */
    btr_page_set_next(page, page_zip, FIL_NULL, mtr);
    btr_page_set_prev(page, page_zip, FIL_NULL, mtr);

    /* 其他逻辑 */

    /* 返回root page的页面号 */
    return(page_no);
}

4.2 segment的创建过程

segment的创建过程比较简单:先在inode page中为segment分配一个inode entry,然后再inode entry上进行初始化,更新space header里的最大segment id,即可。需要注意的是:当传入的page 为0 时,意味着要创建一个独立的segment,需要将当前的inode entry地址记录在段首page中,并返回;当传入的page非0时,segment需要在指定的page的指定位置记录下当前的inode entry地址。详细过程请参考代码:

buf_block_t*
fseg_create_general(
/*================*/
    ulint    space_id,/*!< in: space id */
    ulint    page,    /*!< in: page where the segment header is placed: if
            this is != 0, the page must belong to another segment,
            if this is 0, a new page will be allocated and it
            will belong to the created segment */
    ulint    byte_offset, /*!< in: byte offset of the created segment header
            on the page */
    ibool    has_done_reservation, /*!< in: TRUE if the caller has already
            done the reservation for the pages with
            fsp_reserve_free_extents (at least 2 extents: one for
            the inode and the other for the segment) then there is
            no need to do the check for this individual
            operation */
    mtr_t*    mtr)    /*!< in/out: mini-transaction */
{
    /* 局部变量 */
    ...

    /* 如果传入的page是0,则创建一个独立的段,并把segment header的信息
    存储在段首page中。如果传入page是非0,则这是一个非独立段,需要将
    segment header的信息存储在指定page的指定位置上 */

    if (page != 0) {
        /* 获取指定page */
        block = buf_page_get(page_id_t(space_id, page), page_size,
                     RW_SX_LATCH, mtr);

        header = byte_offset + buf_block_get_frame(block);
    }

    /* 其他逻辑 */

    /* 获取space header和inode_entry */
    space_header = fsp_get_space_header(space_id, page_size, mtr);
    inode = fsp_alloc_seg_inode(space_header, mtr);
    if (inode == NULL) {

        goto funct_exit;
    }

    /* 获取当前表空间最大segment id,并更新表空间最大
    segment id */
    seg_id = mach_read_from_8(space_header + FSP_SEG_ID);
    mlog_write_ull(space_header + FSP_SEG_ID, seg_id + 1, mtr);

    /* 初始化inode entry的segment id 和 FSEG_NOT_FULL_N_USED */
    mlog_write_ull(inode + FSEG_ID, seg_id, mtr);
    mlog_write_ulint(inode + FSEG_NOT_FULL_N_USED, 0, MLOG_4BYTES, mtr);

    /* 初始化inode entry的三个extent链表 */
    flst_init(inode + FSEG_FREE, mtr);
    flst_init(inode + FSEG_NOT_FULL, mtr);
    flst_init(inode + FSEG_FULL, mtr);

    /* 初始化innode entry的32个碎片页 */
    mlog_write_ulint(inode + FSEG_MAGIC_N, FSEG_MAGIC_N_VALUE,
             MLOG_4BYTES, mtr);
    for (i = 0; i < FSEG_FRAG_ARR_N_SLOTS; i++) {
        fseg_set_nth_frag_page_no(inode, i, FIL_NULL, mtr);
    }

    /* 如果传入的page是0,则分配一个段首page */
    if (page == 0) {
        block = fseg_alloc_free_page_low(space, page_size,
                         inode, 0, FSP_UP, RW_SX_LATCH,
                         mtr, mtr
#ifdef UNIV_DEBUG
                         , has_done_reservation
#endif /* UNIV_DEBUG */
                         );

        header = byte_offset + buf_block_get_frame(block);
        mlog_write_ulint(buf_block_get_frame(block) + FIL_PAGE_TYPE,
                 FIL_PAGE_TYPE_SYS, MLOG_2BYTES, mtr);
    }

    /* 在page指定位置记录segment header,segment header由
    inode page所在的space id,page no, 以及inode entry的在
    inode page 中的页内偏移组成 */
    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);

funct_exit:

    DBUG_RETURN(block);
}

4.3 extent的分配过程

表空间分配extent的逻辑比较简单,直接查询FSP_FREE上有没有剩余的extent即可,没有的话就为FSP_FREE重新初始化一些extent。详细逻辑如下:

static
xdes_t*
fsp_alloc_free_extent(
    ulint            space_id,
    const page_size_t&    page_size,
    ulint            hint,
    mtr_t*            mtr)
{
    /* 局部变量 */
    ...

    /* 获取space header */
    header = fsp_get_space_header(space_id, page_size, mtr);

    /* 获取hint页所在的xdes entry */
    descr = xdes_get_descriptor_with_space_hdr(
        header, space_id, hint, mtr, false, &desc_block);

    fil_space_t*    space = fil_space_get(space_id);

    /* 当hint页所在的xdes entry的状态是XDES_FREE时,直接将其摘下返回,
    否则尝试从FSP_FREE中为segment分配extent。如果FSP_FREE为空,
    则需要进一步从未初始化的空间中为FSP_FREE新分配一些extent,
    并从新的FSP_FREE中取出第一个extent返回 */
    if (descr && (xdes_get_state(descr, mtr) == XDES_FREE)) {
        /* Ok, we can take this extent */
    } else {
        /* Take the first extent in the free list */
        first = flst_get_first(header + FSP_FREE, mtr);

        if (fil_addr_is_null(first)) {
            fsp_fill_free_list(false, space, header, mtr);

            first = flst_get_first(header + FSP_FREE, mtr);
        }

        /* 分配失败 */
        if (fil_addr_is_null(first)) {

            return(NULL);    /* No free extents left */
        }

        descr = xdes_lst_get_descriptor(
            space_id, page_size, first, mtr);
    }

    /* 将分配到的extent从FSP_FREE中删除 */
    flst_remove(header + FSP_FREE, descr + XDES_FLST_NODE, mtr);
    space->free_len--;

    return(descr);
}

当为segment分配extent时稍微复杂一些:先检查 FSEG_FREE中是否有剩余的extent,如果没有再用fsp_alloc_free_extent从表空间中申请extent。 在第二种情况下,FSEG_F REE中的extent不足,因此还会进一步尝试为FSEG_FREE分配更多extent。详细过程如下:

static
xdes_t*
fseg_alloc_free_extent(
    fseg_inode_t*        inode,
    ulint            space,
    const page_size_t&    page_size,
    mtr_t*            mtr)
{
    /* 局部变量 */
    ...

    /* 如果FSEG_FREE非空,则从其中为segment分配extent,如果FSEG_FREE为空,
    则从调用fsp_alloc_free_extent 为当前segment分配extent */
    if (flst_get_len(inode + FSEG_FREE) > 0) {
        first = flst_get_first(inode + FSEG_FREE, mtr);

        descr = xdes_lst_get_descriptor(space, page_size, first, mtr);
    } else {
        descr = fsp_alloc_free_extent(space, page_size, 0, mtr);

        if (descr == NULL) {

            return(NULL);
        }

        /* 将从space申请到的extent设置为segment私有状态(XDES_FSEG),
        将改extent加入到FSEG_FREE中 */
        seg_id = mach_read_from_8(inode + FSEG_ID);

        xdes_set_state(descr, XDES_FSEG, mtr);
        mlog_write_ull(descr + XDES_ID, seg_id, mtr);
        flst_add_last(inode + FSEG_FREE, descr + XDES_FLST_NODE, mtr);

        /* 当前FSEP_FREE中剩余的extent不多,尝试为当前segment分配更多
        物理相邻的extent */
        fseg_fill_free_list(inode, space, page_size,
                    xdes_get_offset(descr) + FSP_EXTENT_SIZE,
                    mtr);
    }

    return(descr);
}

4.4 page的分配过程

表空间page的分配过程如下:先查看hint_page所在的extent是否适合分配空闲页面,不适合的话,则尝试从FSP_FREE_FRAG链表中寻找空闲页面。如果FSP_FREE_FRAG为空,则新分配一个extent,将其添加到FSP_FREE_FRAG中,并在其中分配空闲页面。

static MY_ATTRIBUTE((warn_unused_result))
buf_block_t*
fsp_alloc_free_page(
    ulint            space,
    const page_size_t&    page_size,
    ulint            hint,
    rw_lock_type_t        rw_latch,
    mtr_t*            mtr,
    mtr_t*            init_mtr)
{
    /* 局部变量 */
    ...

    /* 获取表空间header 和 hint page所在extent的xdes entry */
    header = fsp_get_space_header(space, page_size, mtr);

    descr = xdes_get_descriptor_with_space_hdr(header, space, hint, mtr);

    /* 如果xdes entry的状态是XDES_FREE_FRAG,那就直接从该extent中分配page,
    否则从FSP_FREE_FRAG中去寻找空闲page */
    if (descr && (xdes_get_state(descr, mtr) == XDES_FREE_FRAG)) {
        /* Ok, we can take this extent */
    } else {
        /* Else take the first extent in free_frag list */
        first = flst_get_first(header + FSP_FREE_FRAG, mtr);

        /* 尝试从FSP_FREE_FRAG中寻找空闲页面,当FSP_FREE_FRAG链表为空时,
        需要使用fsp_alloc_free_extent分配一个新的extent,将该extent加入
        FSP_FREE_FRAG,并在其中分配空闲page */
        if (fil_addr_is_null(first)) {
            descr = fsp_alloc_free_extent(space, page_size,
                              hint, mtr);

            if (descr == NULL) {
                /* No free space left */

                return(NULL);
            }

            xdes_set_state(descr, XDES_FREE_FRAG, mtr);
            flst_add_last(header + FSP_FREE_FRAG,
                      descr + XDES_FLST_NODE, mtr);
        } else {
            descr = xdes_lst_get_descriptor(space, page_size,
                            first, mtr);
        }

        /* Reset the hint */
        hint = 0;
    }

    /* 从找到的extent中分配一个空闲页面 */
    free = xdes_find_bit(descr, XDES_FREE_BIT, TRUE,
                 hint % FSP_EXTENT_SIZE, mtr);
    if (free == ULINT_UNDEFINED) {

        ut_print_buf(stderr, ((byte*) descr) - 500, 1000);
        putc('
', stderr);

        ut_error;
    }

    page_no = xdes_get_offset(descr) + free;

    /* 其他逻辑 */

    /* 在fsp_alloc_from_free_frag中设置分配page的XDES_FREE_BIT为false,
    表示被占用;递增头page的FSP_FRAG_N_USED字段;如果该extent被用满了,
    就将其从FSP_FREE_FRAG移除,并加入到FSP_FULL_FRAG链表中,更新FSP_FRAG_N_USED的值 */
    fsp_alloc_from_free_frag(header, descr, free, mtr);

    /* 对Page内容进行初始化后返回 */
    return(fsp_page_create(page_id_t(space, page_no), page_size,
                   rw_latch, mtr, init_mtr));
}

为了能够使得segment内逻辑上相邻的节点在物理上也尽量相邻,尽量提高表空间的利用率,在segment中分配page的逻辑较为复杂。详细过程如下所述:

static
buf_block_t*
fseg_alloc_free_page_low(
    fil_space_t*        space,
    const page_size_t&    page_size,
    fseg_inode_t*        seg_inode,
    ulint            hint,
    byte            direction,
    rw_lock_type_t        rw_latch,
    mtr_t*            mtr,
    mtr_t*            init_mtr
#ifdef UNIV_DEBUG
    , ibool            has_done_reservation
#endif /* UNIV_DEBUG */
)
{
    /* 局部变量 */
    ...

    /* 计算当前segment使用的和占用的page数。前者统计的统计方法为
    累加32个碎片页中已使用的数量,FSEG_FULL/FSEG_NOT_FULL中已使
    用page的数量,后者的统计方法为累加32个碎片页已使用数量,
    FSEG_FULL/FSEG_NOT_FULL/FSEG_FREE三个链表中总page数*/
    reserved = fseg_n_reserved_pages_low(seg_inode, &used, mtr);

    /* 获取表空间header 和 hint page所在extent的xdes entry */
    space_header = fsp_get_space_header(space_id, page_size, mtr);
    descr = xdes_get_descriptor_with_space_hdr(space_header, space_id,
                           hint, mtr);
    if (descr == NULL) {
        /* 说明hint page在free limit之外,将hint page置0,取消hint page的作用*/
        hint = 0;
        descr = xdes_get_descriptor(space_id, hint, page_size, mtr);
    }

    /* In the big if-else below we look for ret_page and ret_descr */
    /*-------------------------------------------------------------*/
    if ((xdes_get_state(descr, mtr) == XDES_FSEG)
        && mach_read_from_8(descr + XDES_ID) == seg_id
        && (xdes_mtr_get_bit(descr, XDES_FREE_BIT,
                 hint % FSP_EXTENT_SIZE, mtr) == TRUE)) {
take_hinted_page:
        /* 1. hint page所在的extent属于当前segment,并且
        hint page也是空闲状态,这是最理想的情况 */
        ret_descr = descr;
        ret_page = hint;

        goto got_hinted_page;
        /*-----------------------------------------------------------*/
    } else if (xdes_get_state(descr, mtr) == XDES_FREE
           && reserved - used < reserved / FSEG_FILLFACTOR
           && used >= FSEG_FRAG_LIMIT) {

        /* 2. segment空间利用率高于临界值(7/8 ,FSEG_FILLFACTOR),
        并且hint page所在的extent处于XDES_FREE状态,直接将该extent从
        FSP_FREE摘下,分配至segment的FSEG_FREE中,返回hint page */

        ret_descr = fsp_alloc_free_extent(
            space_id, page_size, hint, mtr);

        xdes_set_state(ret_descr, XDES_FSEG, mtr);
        mlog_write_ull(ret_descr + XDES_ID, seg_id, mtr);
        flst_add_last(seg_inode + FSEG_FREE,
                  ret_descr + XDES_FLST_NODE, mtr);

        /* 在利用率条件允许的情况下,为segment的FSEG_FREE多分配几个
        物理相邻的extent */
        fseg_fill_free_list(seg_inode, space_id, page_size,
                    hint + FSP_EXTENT_SIZE, mtr);

        goto take_hinted_page;
        /*-----------------------------------------------------------*/
    } else if ((direction != FSP_NO_DIR)
           && ((reserved - used) < reserved / FSEG_FILLFACTOR)
           && (used >= FSEG_FRAG_LIMIT)
           && (!!(ret_descr
              = fseg_alloc_free_extent(
                  seg_inode, space_id, page_size, mtr)))) {

        /* 3. 当利用率小于临界值,不建议分配新的extent,避免空间浪费,
        此时从FSEG_FREE中获取空闲extent,用于分配新的page */
        ret_page = xdes_get_offset(ret_descr);

        if (direction == FSP_DOWN) {
            ret_page += FSP_EXTENT_SIZE - 1;
        }

    } else if ((xdes_get_state(descr, mtr) == XDES_FSEG)
           && mach_read_from_8(descr + XDES_ID) == seg_id
           && (!xdes_is_full(descr, mtr))) {

        /* 4. 当hint page所在的extent属于当前segment时,该extent内如有空闲page,
        将其返回 */

        ret_descr = descr;
        ret_page = xdes_get_offset(ret_descr)
            + xdes_find_bit(ret_descr, XDES_FREE_BIT, TRUE,
                    hint % FSP_EXTENT_SIZE, mtr);

    } else if (reserved - used > 0) {

        /* 5. 如果该segment占用的page数大于实用的page数,说明该segment还有空
        闲的page,则依次先看FSEG_NOT_FULL链表上是否有未满的extent,如果没有,
        再看FSEG_FREE链表上是否有完全空闲的extent */
        fil_addr_t    first;

        if (flst_get_len(seg_inode + FSEG_NOT_FULL) > 0) {
            first = flst_get_first(seg_inode + FSEG_NOT_FULL,
                           mtr);
        } else if (flst_get_len(seg_inode + FSEG_FREE) > 0) {
            first = flst_get_first(seg_inode + FSEG_FREE, mtr);
        } else {
            return(NULL);
        }

        ret_descr = xdes_lst_get_descriptor(space_id, page_size,
                            first, mtr);
        ret_page = xdes_get_offset(ret_descr)
            + xdes_find_bit(ret_descr, XDES_FREE_BIT, TRUE,
                    0, mtr);

    } else if (used < FSEG_FRAG_LIMIT) {

        /* 6. 当前segment的32个碎片页尚未使用完毕,使用fsp_alloc_free_page从
        表空间FSP_FREE_FRAG中分配独立的page,并加入到该inode的frag array page
        数组中 */
        buf_block_t* block = fsp_alloc_free_page(
            space_id, page_size, hint, rw_latch, mtr, init_mtr);

        if (block != NULL) {
            /* Put the page in the fragment page array of the
            segment */
            n = fseg_find_free_frag_page_slot(seg_inode, mtr);

            fseg_set_nth_frag_page_no(
                seg_inode, n, block->page.id.page_no(),
                mtr);
        }

        return(block);

    } else {
        /* 7. 当上述情况都不满足时,直接使用fseg_alloc_free_extent分配一个空闲
        extent,并从其中取一个page返回 */

        ret_descr = fseg_alloc_free_extent(seg_inode,
                           space_id, page_size, mtr);

        if (ret_descr == NULL) {
            ret_page = FIL_NULL;
            ut_ad(!has_done_reservation);
        } else {
            ret_page = xdes_get_offset(ret_descr);
        }
    }

    /* page分配失败 */
    if (ret_page == FIL_NULL) {
        return(NULL);
    }

got_hinted_page:

    /* 将可用的hint page标记为used状态 */
    if (ret_descr != NULL) {
        fseg_mark_page_used(seg_inode, ret_page, ret_descr, mtr);
    }

    /* 对Page内容进行初始化后返回 */
    return(fsp_page_create(page_id_t(space_id, ret_page), page_size,
                   rw_latch, mtr, init_mtr));
}

五、总结

innodb的文件结构由自下而上包括page(页),extent(区),segment(段),tablespace(表空间)等几个层次。page是最基本的物理单位,所有page具有相同的页首和页尾;extent由通常由连续的64个page组成,tablespace由一个个连续的extent组成;段是用来管理物理文件的逻辑单位,可以向表空间申请分配和释放page 或 extent,是构成索引,回滚段的基本元素;表空间是一个宏观概念,当innodb_file_per_table为ON时一个用户表对应一个表空间。

推荐阅读

为传统银行换“心”,腾讯TDSQL成为首款应用于银行传统核心的国产分布式数据库

腾讯新一代企业级云数据库CynosDB商业化正式发布,全面兼容MySQL5.7,秒级升降配,存储自动扩容,按使用量付费,轻松应对业务突发峰值电商秒杀、游戏促销等场景。

必须要赞一个!!

点击优惠购买腾讯自研数据库CynosDB

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章