开始阅读本文之前,先思考三个问题:
带着你的答案,请继续往下面看。
本文主要包含如下内容:
1.磁盘or内存?
1.1 磁盘
如何解决数据的安全问题?
答案:把数据存在磁盘上。
只要磁盘不坏,数据就可以永久保存。
如果每次都从磁盘中读取数据,数据固然是安全了,但频繁的进行IO请求势必会影响数据库的性能。
那么,如何才能解决数据库的性能问题呢?
1.2 内存
把数据存在内存中。
内存能满足我们快速读取和写入数据的需求。
内存可以存储一些用户数据,但如果数据量过大可能无法存储所有的用户数据,此外,万一数据库服务器或者部署节点挂了,或者重启了,数据也有丢失的风险。
怎么做,才能不会因为异常情况,而丢数据。同时,又能保证数据的读写速度呢?
2.数据页
页是mysql中磁盘和内存交换的基本单位,也是mysql管理存储空间的基本单位。
同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为 16KB,当然也可以通过改变 innodb_page_size 选项对默认大小进行修改,需要注意的是不同的页大小最终也会导致区大小的不同。
一次最少从磁盘读取16KB内容到内存中,一次最少把内存中16KB内容刷新到磁盘中。
写操作时,如下图所示:
读操作时,如下图所示:
数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。
很多时候,由于我们表中的数据比较多,在磁盘中可能存放在多个数据页当中。
假如我们要根据某个条件查询数据时,需要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体结构如下图所示:
单个数据页包含哪些内容呢?
从上图中可以看出,数据页主要包含如下几个部分:
- 文件头部
- 页头部
- 最大和最小记录
- 用户记录
- 空闲空间
- 页目录
- 文件尾部
3.用户记录
对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分空闲空间
分配给用户记录。
用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。那么,它里面又包含哪些内容呢?
其实在innodb支持的数据行格式有四种:
我们以compact行格式为例:
一条用户记录主要包含三部分内容:
下面让我们一起了解一下这些内容。
3.1 额外信息
额外信息并非真正的用户数据,它是为了辅助存数据用的。
3.1.1 变长字段列表
有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。
如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?
所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。
3.1.2 null值列表
数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。
有没有办法只简单的标记一下,不存储实际的null值呢?
答案:将为null的字段保存到null值列表。
在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。
3.1.3 记录头信息
记录头信息用于描述一些特殊的属性。
它主要包含:
- deleted_flag:即删除标记,用于标记该记录是否被删除了。
- min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记。
- n_owned:即拥有的记录数,记录该组索引记录的条数。
- heap_no:即堆上的位置,它表示当前记录在堆上的位置。
- record_type:即记录类型,其中:0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录。
- next_record:即下一条记录的位置。
3.2 隐藏列
数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
目前innodb自动创建的隐藏列有三种:
- db_row_id,即行id,它是一条记录的唯一标识。
- db_trx_id,即事务id,它是事务的唯一标识。
- db_roll_ptr,即回滚点,它用于事务回滚。
如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。
如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。
也就是说在innodb中,隐藏列中事务id
和回滚点
是一定会被创建的,但行id要根据实际情况决定。
3.3 真正数据列
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。这个比较简单,没有什么好多说的。
3.4 用户记录是如何相连的?
通过上面介绍的内容,大家对一条用户记录是如何存储的,应该有了一定的认识。
但问题来了,一条用户记录和另一条用户记录是如何相连的,innodb是怎么知道,某条记录的下一条记录是谁?
答案是:用前面提到过的, 记录额外信息 》 记录头信息 》下一条记录的位置。
多条用户记录之间通过下一条记录的位置
,组成了一个单向链表。这样就能从前往后,找到所有的记录了。
4.最大和最小记录
从上面可以得知,在一个数据页当中,如果存在多条用户记录,它们是通过下一条记录的位置
相连的。
不过有个问题:如果才能快速找到最大的记录和最小的记录呢?
这就需要在保存用户记录的同时,也保存最大和最小记录了。
最大记录保存到Supremum记录中。
最小记录保存在Infimum记录中。
在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示:
从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。
5.页目录
InnoDB为了提高对整页用户数据扫描的效率,制作了一个类似书目录一样的页目录。
页目录的过程:
由此可见,页目录是有多个槽组成的。所下图所示:
如此一来,就能通过二分查找,快速的定位需要查找的记录了。
注意:
对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。
6.文件头部和尾部
6.1 文件头部
通过前面介绍的行记录中下一条记录的位置
和页目录
,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?
这时就需要使用文件头部
了。
文件头部包含页的一些通用信息,占固定的38字节。由下面这些内容组成的:
innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示:
不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。
此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。
6.2 文件尾部
之前提过,数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。
如果程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?
这就需要用到文件尾部
。
它里面记录了页面的校验和
。
在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。
接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。
7.页头部
通过上面介绍的内容,数据页之间能够轻松访问了,但剩下还有个比较重要的问题,就是记录的状态信息。
为了性能考虑,数据页的状态信息实现统计好,保存在页头部
。
页头部是由14个部分组成,共占56个字节 。由下面这些内容组成的:
总结
多个数据页之间通过页号
构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置
构成了单项链表。整体架构图如下:
来源:数据与人