MySQL存储引擎是通过插件实现的,所以在源码中分为两层:服务器层和存储引擎层。服务器层负责解析SQL、选择执行计划、条件过滤、排序、分组等逻辑。存储引擎层做的事情比较简单,负责写入数据和读取数据。写数据是将MySQL传递给存储引擎的数据存储在一个磁盘文件或内存中(对于Memory引擎来说就是存储在内存中),读数据是从磁盘或内存中读取数据返回给服务器层。服务器层和引擎层是两个相对独立的模块。如果他们合作完成工作,就会有一个数据交互的过程。今天我们将使用服务器层从存储引擎层读取数据。这起着关键作用。数据交互过程。一、原理在源码中,数据库中的每张表对应一个TABLE类的实例。实例中有一个record属性,record属性是一个有2个元素的数组。服务器层每次调用引擎层的方法读取。取数据时,第一个元素的地址会以table->record[0]的形式传给引擎层。引擎层从磁盘或内存中读取数据后,将引擎层的数据格式转换成服务器层的数据格式,然后写入到该地址对应的内存空间中,服务器层就可以使用这个数据做各种事情(例如:WHERE条件过滤、分组、排序等)。整个交互过程就这么简单。既然这么简单,是否值得单独写一篇文章来谈这个呢?当然是值得的。台上一分钟,台下十年功这句话大家应该都不陌生了。这个交互过程之所以这么简单,是因为server层在前期做了足够多的准备工作,让这个过程看起来和百度的搜索框一样简单。为了找出答案,是时候让我们回到过去的准备工作(又名前戏阶段)了。2.前戏阶段建表时,记录中各字段的Offset(也就是我们常说的行)和一条记录的最大长度(包括存储该长度所需的字节数)可变长度字段)将被计算)。当我们第一次查询一张表时,MySQL会从frm文件中读取字段、索引等信息,以及刚才提到的字段Offset和一条记录的最大长度。接下来会根据记录的最大长度为第1节中提到的TABLE类实例的记录属性申请内存。record数组的两个元素record[0]和record[1]占用的字节数等于record的最大长度。在源码中,每个字段对应Field子类的一个实例,实例中有一个ptr属性,指向record[0]中每个字段对应的内存地址。对于变长字段,Field子类实例也会存储内容长度占用的字节数。存储引擎从磁盘或内存中读取一条记录的某个字段后,会判断该字段的类型。如果是定长字段,则将字段的内容转换成相应的格式写入ptr指向的内存空间。如果是变长字段,则先将内容长度写入ptr指向的内存空间,然后将字段内容经过相应的格式转换后写入内容长度后的内存空间。这是抽象的东西的结束。下面我以一个实际的表为例,通过一张图来展示record[0]的内存布局,让大家有个直观的认识。3.实例分析这是一个示例表:CREATETABLE`t_recbuf`(`id`int(10)unsignedNOTNULLAUTO_INCREMENT,`i1`int(10)unsignedDEFAULT'0',`str1`varchar(32)DEFAULT'',`str2`varchar(255)DEFAULT'',`c1`char(11)DEFAULT'',`e1`enum('北京','上海','广州','深圳')DEFAULT'北京',`s1`set('eat','drink','play','music')DEFAULT'',`bit1`bit(8)DEFAULTb'0',`bit2`bit(17)DEFAULTb'0',`blob1`blob,`d1`decimal(10,2)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8;这是record[0]的内存布局:示例表和内存布局图准备好了,接下来我们根据图来分析各个字段对应的内存空间:字段NULL值标记区这个区域是用来标记具体一条记录的,定义表结构时没有指定NOTNULL字段。实际内容是否为NULL,若为NULL则本区对应位置置1,若不为NULL则本区对应位置置0,各字段NULL标志占1位.该字段在record[0]的开头,所以其Offset=0。由于示例表中有10个字段没有指定NOTNULL,所以一共需要10位来存储NULL标志,总共占用2个字节。存储引擎在读取每个字段时,如果该字段在字段NULL值标记区有位置,则在其对应位置设置一个值(0或1)。idid字段类型为int,定长字段,占用4个字节,Offset=该字段的NULL值,标记区占用的字节数=2,ptr属性指向Offset2。存储引擎读取id字段的内容,经过大小端存储模式转换后写入ptr属性指向的内存空间。由于在InnoDB中,内容是以big-endian方式存储的(内容的高位在前,低位在后),而server层的读取是little-endian方式,所以在写入内容之前record[0]的整型字段,会进行大小端存储方式的转换。i1i1字段类型为int,定长字段,占用4个字节,Offset=idOffset(2)+idlength(4)=6,ptr属性指向Offset6,存储引擎读取内容i1字段的内容,经过大小端存储模式转换后,将内容写入ptr属性指向的内存空间。str1str1字段类型为varchar,变长字段,Offset=i1Offset(6)+i1Length(4)=10,ptr属性指向Offset10。定义str1字段时指定存储32个字符。表的字符集是utf8。每个字符最多占用3个字节,32个字符最多占用96个字节。96<255,只需要1个字节就可以存储str1内容的长度,所以str1len区占1个字节。str1字段内容紧跟在str1len之后,由于str1len占1个字节,所以str1content的Offset=10+1=11。存储引擎在读取str1字段内容的时候,也会读取str1的内容长度.它首先将内容长度写入ptr属性指向的内存空间,然后在其旁边写入str1的内容。str2str2字段的类型也是varchar,变长字段,Offset=str1偏移量(10)+str1的内容长度(1)+内容占用的最大字节数(96)=107,ptr属性指向Offset107。定义str2字段时,指定存储255个字符,最多会占用255*3=765字节。存储str2的内容长度需要2个字节,所以str2的len区占2个字节。str2字段内容紧挨着str2len存放,由于str2len占2个字节,所以str2content的Offset=107+2=109。存储引擎读取到str2字段的内容后,会先将内容长度写入ptr属性指向的内存空间,然后在其旁边写入str2的内容。c1c1字段的类型为char,定长字段,Offset=str2偏移量(107)+str2的内容长度占用的字节数(2)+内容占用的最大字节数(765)=874,ptr属性指向Offset874。定义c1字段时,指定存放11个字符,最多会占用11*3=33字节。存储引擎读取c1字段的内容后,会将内容写入ptr属性指向的内存空间。如果c1字段的实际内容长度小于该字段内容的最大字节数,则会在刚刚写入的内容旁边写入一定数量的空格。例如:实际内容长度为11字节,但字段内容最大字节数为33,则在实际内容后写入22个空格。e1e1字段类型为enum,定长字段,只有4个选项,占用1个字节,Offset=c1Offset(874)+content最大长度占用的字节数(33)=907.enum类型在存储引擎中存储为整数。存储引擎读取到e1字段的内容后,将内容进行大小端转换,并将转换后的内容写入ptr属性指向的内部空间。设置了s1s1字段类型,定长字段,只有4个选项,占用1个字节,Offset=e1Offset(907)+e1Length(1)=908。设置的类型在存储中也存储为整数引擎。存储引擎读取s1字段的内容后,还需要将内容进行字节序转换,并将转换后的内容写入ptr属性指向的内存空间。set字段是用enum实现的,最多占用8个字节,共64位,每个选项用1位表示,所以一个set字段总共可以有64个选项。解释所需的枚举和集合字段长度。如果建表时定义的选项个数不同,字段长度也可能不同(1~8字节),但字段长度在建表时就已经确定了,所以也是定长字段.bit1bit1字段的类型为bit,定长字段。创建表时定义的长度表示位,而不是字节数。偏移量=s1偏移量(908)+s1长度(1)=909。定义bit1字段时指定了bit(8),表示该字段的长度为8位,即1个字节。bit类型的字段在存储引擎中存储为char。存储引擎读取bit1字段的内容后,将内容写入ptr属性指向的内存空间。这里的char是指C/C++中的char,不是MySQL的char类型。bit2bit2字段的类型也是bit,定长字段,建表时定义为bit(17),占用3个字节,Offset=bit1Offset(909)+bit1length(1)=910。对于bit类型的字段,如果建表时指定的位数不是8的整数倍,存储引擎在向磁盘或内存插入数据时会在前面加0,比如bit(17),占用3个字节,内容为00010000010010011时,前面加七个0,变成000000000010000010010011,读出来还是一样。之所以定义两个位域是为了测试位类型的域。当定义的位数不是8的整数倍时,是否会将多出来的位存入字段值NULL标记区?后来发现只有MyISAM和NDB存储引擎会处理这个。InnoDB中的位字段存储为char。当位数不是8的整数倍时,多出的位仍需占用1个字节。例如:bit(17)需要占用3个字节。字节。blob1lenblob1字段的类型是blob,变长字段,Offset=bit2Offset(910)+bit2length(3)=913。一个blob类型的字段最多可以存储2^16=65536bytes=64K。存储引擎读取到blob1字段的内容后,会分配一块可以容纳blob1字段内容的内存空间,并将读取到的内容写入内存空间。然后将blob1字段的内容长度写入ptr属性指向的内存空间,占用2个字节,然后紧挨着刚刚分配的内存空间的首地址写入,占用8个字节。注意:只向record[0]写入了blob1字段内容的首地址,并没有写入blob1字段的全部内容。示例中只使用了blob类型字段,实际的blob类型分为4种类型:tinyblob、blob、mediumblob和longblob,这4种类型的内容长度分别占用1到4个字节。另外需要说明的一点是:tinytext、text、mediumtext、longtext也是用上面对应的blob类型实现的,json类型是用longblob类型实现的。d1d1字段的类型为十进制,定长字段,Offset=blob1偏移量(913)+blob1长度占用的字节数(2)+blob1内容首地址占用的字节数(8)=923。十进制类型的字段在存储引擎中以二进制存储。创建表时,会计算存储需要多少字节。存储引擎读取d1字段的内容后,将内容写入ptr属性指向的内存空间。本文转载自微信公众号“一树一溪”,可通过以下二维码关注。转载本文请联系艺书艺熙公众号。
