在 MYSQL 8.0 以前,默认的存储引擎是:MylSAM。但MYSQL 8.0 之后,默认的存储引擎已经变成了:InnoDB,它是我们建表的首选存储引擎
MYSQL 的数据最终会持久化到磁盘中。
写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示:
读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示:
将内存中的数据刷到磁盘,或者将磁盘中的数据加载到内存,都是以批次为单位,这个批次就是我们常说的:数据页
什么是数据页?
数据页:主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页(表中的数据比较多,在磁盘中可能存放在多个数据页)
要根据某个条件查询数据时,需要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体结构如下图所示:
通常情况下,单个数据页默认的大小是 16kb
。当然,我们也可以通过参数:innodb_page_size
,来重新设置大小。不过,一般情况下,用它的默认值就够了
从上图中可以看出,数据页主要包含如下几个部分:
对于新申请的数据页,用户记录是空的。当插入数据时,innodb 会将一部分空闲空间分配给用户记录
用户记录是 innodb 的重中之重,我们平时保存到数据库中的数据,就存储在它里面
在 innodb 支持的数据行格式有四种:
以 compact 行格式为例:
一条用户记录主要包含三部分内容:
额外信息并非真正的用户数据,它是为了辅助存数据用的
有些数据如果直接存会有问题,比如:如果某个字段是 varchar 或 text 类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化
如果不在一个地方记录数据真正的长度,innodb 很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?
所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间
数据库中有些字段的值允许为 null,如果把每个字段的 null 值,都保存到用户记录中,显然有些浪费存储空间
有没有办法只简单的标记一下,不存储实际的 null 值呢?
答案:将为 null 的字段保存到 null 值列表
在列表中用二进制的值 1,表示该字段允许为 null,用 0 表示不允许为 null。它只占用了 1 位,就能表示某个字符是否为 null,确实可以节省很多存储空间
记录头信息用于描述一些特殊的属性
它主要包含:
数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:
目前 innodb 自动创建的隐藏列有三种:
真正的数据列中存储了用户的真实数据,它可以包含很多列的数据
通过上面介绍的内容,大家对一条用户记录是如何存储的,应该有了一定的认识
但问题来了,一条用户记录和另一条用户记录是如何相连的,innodb 是怎么知道,某条记录的下一条记录是谁?
答案是:用前面提到过的:记录额外信息 ==>
记录头信息 ==>
下一条记录的位置
多条用户记录之间通过下一条记录的位置,组成了一个单向链表。这样就能从前往后,找到所有的记录了
从上面可以得知,在一个数据页当中,如果存在多条用户记录,它们是通过下一条记录的位置相连的
不过有个问题:如果才能快速找到最大的记录和最小的记录呢?
这就需要在保存用户记录的同时,也保存最大和最小记录了
在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示:
从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止
如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空
但如果仔细想想,效率有点低(要对整页用户数据进行扫描)
更高效的方法:使用 页目录:把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是 页目录
。每一组的最大记录叫做槽
假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值
这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。
如此一来,就能通过二分查找,快速的定位需要查找的记录了
通过前面介绍的行记录中下一条记录的位置和页目录,innodb 能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中
如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?
这时就需要使用 文件头部 了。
它里面包含了多个信息,但我只列出了其中4个最关键的信息:
innodb 是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示:
不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。
此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo 日志页等
数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。
如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?
这就需要用到文件尾部,它里面记录了页面的校验和
在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。
接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的
通过上面介绍的内容,数据页之间能够轻松访问了,但剩下还有个比较重要的问题,就是记录的状态信息
比如:一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?
为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是页头部
页头部记录信息:
多个数据页之间通过页号构成了双向链表。而每一个数据页的行数据之间,又通过下一条记录的位置构成了单项链表。整体架构图如下: