zoukankan      html  css  js  c++  java
  • MySQL源码:深度解析Innodb记录格式源码

    很长时间又不写东西了,之前已经看过了innodb格式,但现在想研究一下其它方面的东西,突然发现这个又忘了,索性再看看把它写下来。

    可以通过一个最普遍的插入操作来跟踪Innodb的记录格式,因为在插入时,系统得到的是公共的mysql记录格式record,现在它没有涉及到任何的存储引擎,那么这里不管当前这个表对应的存储引擎是什么,记录格式是一样的,对于插入,mysql函数对应的是ha_write_row,具体到Innodb存储引擎,实际调用的函数是ha_innobase::write_row函数,那么在这里,Innodb首先会将接收到的record记录转换为它自己的一个元组tuple,这其实是与record对应的innodb的表示方式,它是一个内存的记录,逻辑的记录,那么在系统将其真正的写入到页面之前,这条记录的存在方式都是这个tuple,那么下面主要是从源码的角度研究Innodb是如何将一个tuple转换为它的物理的存储记录的,主要研究代码的实现逻辑及记录的格式。

    这里只介绍格式为Compact类型的记录格式。

    实现在某一个页面插入一个元组(一条记录)操作的函数是page_cur_tuple_insert,它的参数就是一个dtuple_t*类型的tuple,在这里,它首先要分配一片空间来存储将要转换过来的物理记录,所以这里需要先计算空间的大小,计算方法如下:

    1. 首先每条记录都要包括下面2个部分:REC_N_NEW_EXTRA_BYTES + UT_BITS_IN_BYTES(n_null),前面表示的是这种格式的固定长度的extra部分,这部分用来存储什么内容后面会给出,后面表示的是所有字段中哪些字段的值是null,当然这里只存储那些nullable属于的字段,如果创建表的时候指定是not null的话,这里就不会被存储,那么这里是用一个位来表示一个字段的null属性。那么上面这部分被系统代码命名为extra_size变量值。

    2. 统计每一个列中数据的长度,在统计这个信息的时候,又有多种情况,主要分定长字段和变长字段,对于定长字段,它的长度直接就是数据类型的长度,比如int类型的那就是4个字节,rowid列就是6个字节等,没有其它附加长度。对于变长字段而言,除了数据内容本身的长度外,还需要计算其数据长度的存储空间,如果字段的字义长度大于255个字节,或者字段的数据类型为BLOB的,那么需要用2个字节来存储这个字段的长度;如果定义长度小于128个字节,或者小于256个字节,但类型不是BLOB类型的,那么这个字段的数据长度用一个字节来存储,除上面2种情况之外,都用2个字节来存储。那么在这一部分中,用来存储变长字段数据的长度的空间的长度也是被Innodb计算为extra_size的。

    所以现在可以知道,一个innodb的记录包括2个部分,一部分是extra_size,另一部分是数据内容,那么这2部分的总长度就是上面计算出来的结果,这里把它定义为record_size。

    接下来,申请空间,进行元组到记录的转换工作。

    转换函数为rec_convert_dtuple_to_rec_new,参数有申请好的记录空间buf,元组和索引的内存结构。

    首先这里有一个操作是rec = buf + extra_size,变量rec表示的是数据内容的存储开始位置。extra_size就是上面计算出来的2个数据部分。

    那么真正执行转换的是接下来调用的rec_convert_dtuple_to_rec_comp函数,下面是其原型:

     1 void
     2 
     3 rec_convert_dtuple_to_rec_comp(
     4 
     5 /*===========================*/
     6 
     7          rec_t*                          rec,   /*!< in: origin of record */
     8 
     9          ulint                    extra,        /*!< in: number of bytes to
    10 
    11                                                reserve between the record
    12 
    13                                                header and the data payload
    14 
    15                                                (normally REC_N_NEW_EXTRA_BYTES) */
    16 
    17          const dict_index_t*  index,        /*!< in: record descriptor */
    18 
    19          ulint                    status,       /*!< in: status bits of the record */
    20 
    21          const dfield_t*          fields,        /*!< in: array of data fields */
    22 
    23          ulint                    n_fields)/*!< in: number of data fields */

    rec表示的是刚才上面计算出来的rec变量,extra表示的是固定长度的REC_N_NEW_EXTRA_BYTES。

     1          end = rec;
     2 
     3          nulls = rec - (extra + 1);
     4 
     5          n_null = index->n_nullable;
     6 
     7          lens = nulls - UT_BITS_IN_BYTES(n_null);
     8 
     9          /* clear the SQL-null flags */
    10 
    11          memset(lens + 1, 0, nulls - lens);

    在这里,这段代码一下子很难看明白,那么首先这里画一下记录存储格式:

             |---------------------extra_size-----------------------------------------|---------fields_data------------|

             |--columns_lens---|---null lens----|------fixed_extrasize(5)------|--col1---|---col2---|---col2----|

    那么语句nulls = rec - (extra + 1);得到的结果是什么呢?想干什么?因为extra表示的是REC_N_NEW_EXTRA_BYTES,固定长度的fixed_extrasize,rec表示的是图中col1的开始位置,那么现在可以知道这条语句的结果就是使得nulls指向了前面nulllens的后一个字节的开始位置。那现在我们知道nulls是一个或者多个字节,用来存储每一个nullable字段的空标志的,那现在为什么要指向这个数组的后一个字节的开始位置呢?一下子很难想明白,不过从后面的代码中可以知道,写入nulls是从后面向前面写的,所以这也理解了为什么指向了后面一个字节的位置了。

    那接下来的一个语句lens = nulls - UT_BITS_IN_BYTES(n_null);道理也是一样的,因为columns_lens正好是在nulllens的前面,那么如果向前跳过null标志的所有空间,则指向的位置lens就是columns_lens的后面一个字节的位置了。在写入值的时候也是从后面向前面写。

    那最后一个语句memset(lens + 1, 0, nulls - lens);表示的意思就很明白了,因为lens指向的是columns_lens的最后一个字节的开始位置,那么加1就指向了nulls空间的开始位置,nulls – lens表示的是nulls空间的长度。这里是将nulls空间清零。

    上面有两个部分都是从后面向前面填写数据,那是不是担心在写入的时候会不会向前面越界呢?其实是不会的,因为这些都是在前面计算好的,extrasize已经是固定的,包括了nulls和columns_lens的长度的。

    上面算是初始化工作,下面就是根据每一个字段来填写record记录了,下面一段代码是处理null信息的,对于每一个字段,都会做下面的处理:

             1       if (!(dtype_get_prtype(type) & DATA_NOT_NULL)) {
    
             2                /* nullable field */
    
             3                ut_ad(n_null--);
    
             4                if (UNIV_UNLIKELY(!(byte) null_mask)) {
    
             5                          nulls--;
    
             6                          null_mask = 1;
    
             7                }
    
             8                ut_ad(*nulls < null_mask);
    
             9                if (dfield_is_null(field)) {
    
             10                       *nulls |= null_mask;
    
             11                       null_mask <<= 1;
    
             12                       continue;
    
             13              }
    
             14              null_mask <<= 1;
    
             15     }

    从第一行可以看出,要处理这个的条件首先必须是没有定义not null属性,所以nulls空间只存储这些字段的信息。

    第4行表示的是如果(byte) null_mask)为0时,nulls向前退一个字节,并且将null_mask恢复为1的初值,因为这个值初始值就是1的,可以猜到,如果这个条件满足了,则说明已经写入了8个nullable列了,那么需要移向前一个字节继续写null信息了,但发现null_mask是int类型的,而nulls是一个字节一个字节的填的,不匹配啊,不过仔细看,判断条件是(byte) null_mask),所以只要写入8个之后,这个值就为0了。因为对于每一个字段,都是执行null_mask向左移1个位的,所以移8次之后,低8位就都是0了。

    第9行表示的是如果这个列的数据就是null值,那么需要将这个null反映到nulls数组中去,因为null_mask当前的值(其实是1的位置)其实表示的是当前nulls这个字节中正在处理的字段的对应关系,也就是说,如果当前的字段的值为null,那么像第10行所示的,将null_mask或到nulls字节上去,如果不为null,就不管,对应的位的值为0。

    所以从这里可以看出,整个nulls空间中的位图是以从后面向前面的顺序来表示所有nullable列的null信息的。

             1       if (fixed_len) {
    
             2       } else if (dfield_is_ext(field)) {
    
             3                *lens-- = (byte) (len >> 8) | 0xc0;
    
             4                *lens-- = (byte) len;
    
             5       } else {
    
             6                if (len < 128 || (dtype_get_len(type) < 256 && dtype_get_mtype(type) != DATA_BLOB)) {
    
             7                          *lens-- = (byte) len;
    
             8                } else {
    
             9                          *lens-- = (byte) (len >> 8) | 0x80;
    
             10                       *lens-- = (byte) len;
    
             11              }
    
             12     }
    
             13     memcpy(end, dfield_get_data(field), len);
    
             14     end += len;

    从第一行可以看出,对于定长数据,只需要将其数据写入到记录里面即可,主要处理的是变长数据,第2行表示的是如果长度大于256个字节,或者数据类型为BLOB,则用两个字节来存储其长度,低字节存储(len >> 8) | 0xc0,高字节存储(byte) len(被截断)。其它可以直接看出来。

    到13行,是直接将数据拷到数据存储空间,用end来表示,存储完一个字段接着下一个字段,是按照索引定义的顺序存储的。

    到这里,一条记录的逻辑到物理的转换就完成了,从中也知道了Innodb是如何实现其物理记录的存储的。

    总结:看innodb的代码,可以说它的代码非常优美,非常精练的,所以有些地方很难一下子看懂,需要揣测,体会才能深入的理解。同时有很多地方是直接硬编码的,这样导致更加难理解,最好的方式是通过宏将其命名,有助于理解。

  • 相关阅读:
    Windows Server 2012配置开机启动项
    Windows Server 2019 SSH Server
    NOIP2017 senior A 模拟赛 7.7 T1 棋盘
    Noip 2015 senior 复赛 Day2 子串
    Noip 2015 senior复赛 题解
    Noip 2014 senior Day2 解方程(equation)
    Noip 2014 senior Day2 寻找道路(road)
    Noip 2014 senior Day2 无线网络发射器选址(wireless)
    Noip2014senior复赛 飞扬的小鸟
    Noip 2014 senior 复赛 联合权值(link)
  • 原文地址:https://www.cnblogs.com/bamboos/p/2943160.html
Copyright © 2011-2022 走看看