乐趣区

关于mysql:innodb是如何存数据的yyds

前言

如果你应用过 mysql 数据库,对它的存储引擎:innodb,肯定不会感到生疏。

家喻户晓,在 mysql8 以前,默认的存储引擎是:myslam。但 mysql8 之后,默认的存储引擎曾经变成了:innodb,它是咱们建表的首选存储引擎。

那么,问题来了:

  1. innodb 的底层是如何存储数据的?
  2. 表中有哪些暗藏列?
  3. 用户记录之间是如何关联起来的?

如果你想晓得下面三个问题的答案,那么,请持续往下面看。

本文次要蕴含如下内容:

1. 磁盘 or 内存?

1.1 磁盘

数据对系统来说是十分重要的货色,比方:用户的身份证、手机号、银行号、会员过期工夫、积分等等。一旦失落,会对用户造成很大的影响。

那么问题来了,如何能力保障这些重要的数据不丢呢?

答案:把数据存在磁盘上。

当然有人会说,如果磁盘坏了怎么办?

那就须要备份,或者做主从了。。。

好了,打住,这不是明天的重点。

言归正传。

大家都晓得,从磁盘上读写数据,至多须要两次 IO 申请能力实现。一次是读 IO,另一次是写 IO。

而 IO 申请是比拟耗时的操作,如果频繁的进行 IO 申请势必会影响数据库的性能。

那么,如何能力解决数据库的性能问题呢?

1.2 内存

把数据存在寄存器?

没错,操作系统从寄存器中读取数据是最快的,因为它离 CPU 最近。

然而寄存器有个十分致命的问题是:它只能存储十分大量的数据,设计它的目标次要是用来暂存指令和地址,并非存储大量用户数据的。

这样看来,只能把数据存在内存中了。

因为内存同样能满足咱们,疾速读取和写入数据的需要,而且性能是十分可观的,只是比拟寄存器稍稍慢了一丢丢而已。

不过有个让人厌恶的中央是,内存绝对于磁盘来说,是更加低廉的资源。通常状况下,500G 或者 1T 的磁盘,是很常见的。但你有据说过有 500G 的内存吗?他人会认为你疯了。内存大小探讨的数量级个别是 16G 或 32G。

内存能够存储一些用户数据,但无奈存储所有的用户数据,因为如果数据量太大了,它可能还是存不下。

此外,即便用户数据能刚好存在内存,当前万一有一天,数据库服务器或者部署节点挂了,或者重启了,数据不就丢了?

怎么做,能力不会因为异常情况,而丢数据。同时,又能保证数据的读写速度呢?

2. 数据页

咱们能够把一批数据放在一起。

写操作时,先将数据写到内存的某个批次中,而后再将该批次的数据一次性刷到磁盘上。如下图所示:

读操作时,从磁盘上一次读一批数据,而后加载到内存当中,当前就在内存中操作。如下图所示:

将内存中的数据刷到磁盘,或者将磁盘中的数据加载到内存,都是以批次为单位,这个批次就是咱们常说的:数据页

当然 innodb 中存在多种不同类型的页,数据页只是其中一种,咱们在这里重点介绍一下数据页。

那么问题来了,什么是数据页?

数据页次要是用来存储表中记录的,它在磁盘中是用双向链表相连的,不便查找,可能十分疾速得从一个数据页,定位到另一个数据页。

很多时候,因为咱们表中的数据比拟多,在磁盘中可能寄存在多个数据页当中。

有一天,咱们要依据某个条件查问数据时,须要从一个数据页找到另一个数据页,这时候的双向链表就派上大用场了。磁盘中各数据页的整体构造如下图所示:

通常状况下,单个数据页默认的大小是16kb。当然,咱们也能够通过参数:innodb_page_size,来从新设置大小。不过,个别状况下,用它的默认值就够了。

好吧,数据页的整体构造曾经搞明确了。

那么,单个数据页蕴含哪些内容呢?


从上图中能够看出,数据页次要蕴含如下几个局部:

  • 文件头部
  • 页头部
  • 最大和最小记录
  • 用户记录
  • 闲暇空间
  • 页目录
  • 文件尾部

3. 用户记录

对于新申请的数据页,用户记录是空的。当插入数据时,innodb 会将一部分 闲暇空间 调配给用户记录。

用户记录是 innodb 的重中之重,咱们平时保留到数据库中的数据,就存储在它外面。那么,它外面又蕴含哪些内容呢?你不好奇吗?

其实在 innodb 反对的数据行格局有四种:

  1. compact 行格局
  2. redundant 行格局
  3. dynamic 行格局
  4. compressed 行格局

咱们以 compact 行格局为例:

一条用户记录次要蕴含三局部内容:

  1. 记录额定信息,它蕴含了变长字段、null 值列表和记录头信息。
  2. 暗藏列,它蕴含了行 id、事务 id 和回滚点。
  3. 真正的数据列,蕴含真正的用户数据,能够有很多列。

上面让咱们一起理解一下这些内容。

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. 页目录

从下面能够看出,如果咱们要查问某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则间接返回该记录。如果始终找到最大记录,还没有找到想要的记录,则返回空。

咋一看,没有问题。

但如果认真想想。

效率会不会有点低?

这不是要对整页用户数据进行扫描吗?

有没有更高效的办法?

这就须要应用 页目录 了。

说白了,就是把一页用户记录分为若干组,每一组的最大记录都保留到一个中央,这个中央就是 页目录 。每一组的最大记录叫做

由此可见,页目录是有多个槽组成的。所下图所示:


假如一页的数据分为 4 组,这样在页目录中,就对应了 4 个槽,每个槽中都保留了该组数据的最大值。

这样就能通过二分查找,比拟槽中的记录跟须要找到的记录的大小。如果用户须要查找的记录,小于以后槽中的记录,则向上查找上一个槽。如果用户须要查找的记录,大于以后槽中的记录,则向下查找下一个槽。

如此一来,就能通过二分查找,疾速的定位须要查找的记录了。

so easy

6. 文件头部和尾部

6.1 文件头部

通过后面介绍的行记录中 下一条记录的地位 页目录,innodb 能十分疾速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。

如果用户记录十分多,在第一个数据页找不到咱们想要的数据,须要到另外一页找该怎么办呢?

这时就须要应用 文件头部 了。

它外面蕴含了多个信息,但我只列出了其中 4 个最要害的信息:

  1. 页号
  2. 上一页页号
  3. 下一页页号
  4. 页类型

顾名思义,innodb 是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示:

不同的数据页之间,通过上一页页号和下一页页号形成了双向链表。这样就能从前向后,一页页查找所有的数据了。

此外,页类型也是一个十分重要的字段,它蕴含了多种类型,其中比拟闻名的有:数据页、索引页(目录项页)、溢出页、undo 日志页等。

6.2 文件尾部

我之前提过,数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,须要刷新到磁盘上。

但如果某一天比拟晦气,程序在刷新到磁盘的过程中,呈现了异样,比方:过程被 kill 掉了,或者服务器被重启了。

这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是残缺的呢?

这就须要用到 文件尾部

它外面记录了页面的 校验和

在数据刷新到磁盘之前,会先计算一个页面的校验和。前面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,因为文件头部在后面,会先被刷新到磁盘上。

接下来,刷新用户记录到磁盘的时候,假如刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,阐明该数据页的数据是不残缺的。

7. 页头部

通过下面介绍的内容,数据页之间可能轻松拜访了,但剩下还有个比拟重要的问题,就是记录的状态信息。

比方一页数据到底保留了多条记录,或者页目录到底应用了多个槽等。这些信息是实时统计,还是当时统计好了,保留到某个中央?

为了性能思考,下面的这些统计数据,当然是先统计好,保留到一个中央。前面须要用到该数据时,再读取进去会更好。这个保留统计数据的中央,就是 页头部

当然页头部不仅仅只保留:槽的数量、记录条数等信息。

它还记录了:

  • 已删除记录所占的字节数
  • 最初插入记录的地位
  • 最大事务 id
  • 索引 id
  • 索引层级

其实还有很多,在这里就不一一列举了,有趣味的敌人能够找我私聊。

总结

多个数据页之间通过 页号 形成了双向链表。而每一个数据页的行数据之间,又通过 下一条记录的地位 形成了单项链表。整体架构图如下:


好了,本文内容先到这里。如果小伙伴们有任何疑难的话,欢送找我私聊。

顺便预报一下,在 innodb 的存储构造中,还有一个十分重要的内容没讲,它就是:索引。敬请期待,咱们下期见。

最初说一句(求关注,别白嫖我)

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多 BAT 大厂的前辈交换和学习。

集体公众号

退出移动版