深度解析Innodb记录格式源码

数据库 MySQL 数据库运维
笔者很长时间又不写东西了,之前已经看过了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的代码,可以说它的代码非常优美,非常精练的,所以有些地方很难一下子看懂,需要揣测,体会才能深入的理解。同时有很多地方是直接硬编码的,这样导致更加难理解,最好的方式是通过宏将其命名,有助于理解。

原文链接:http://www.cnblogs.com/bamboos/archive/2013/03/04/2943160.html

【编辑推荐】

  1. 适合初学者的MySQL学习笔记之库操作示例
  2. 适合初学者的MySQL学习笔记之表操作示例
  3. 适合初学者的MySQL学习笔记之MySQL管理心得
  4. 适合初学者的MySQL学习笔记之MySQL查询示例
  5. 适合初学者的MySQL学习笔记之管理员常用操作总结

责任编辑:彭凡 来源: 博客园
相关推荐

2016-12-15 09:44:31

框架Caffe源码

2022-12-07 08:02:43

Spring流程IOC

2024-09-11 09:25:03

Tomcat组件PREP

2023-05-08 08:11:49

@Component使用场景时序图

2022-08-17 07:52:31

Spring循环依赖单例池

2021-05-26 11:30:24

Java线程池代码

2024-01-11 12:14:31

Async线程池任务

2022-09-27 18:56:28

ArrayList数组源代码

2021-07-03 08:51:30

源码Netty选择器

2023-03-27 08:12:40

源码场景案例

2023-10-10 11:02:00

LSM Tree数据库

2013-12-09 10:34:12

2023-03-13 08:12:25

@DependsOn源码场景

2023-03-06 11:13:20

Spring注解加载

2009-09-14 16:12:57

LINQ删除记录

2019-03-06 09:55:54

Python 开发编程语言

2022-05-25 08:01:37

WHERESQL 语句

2023-11-02 09:54:21

ODT文件

2015-09-16 09:10:27

Java源码解析

2024-07-11 08:50:05

Go语言errors
点赞
收藏

51CTO技术栈公众号