当前位置: 首页 > 后端技术 > Java

innodb是如何存储数据的?

时间:2023-04-02 01:54:12 Java

如果你用过mysql数据库,一定不会陌生它的存储引擎:innodb。众所周知,在mysql5之前,默认的存储引擎是:myslam。但是在mysql5之后,默认的存储引擎变成了:innodb,这是我们首选的建表存储引擎。那么,问题来了:innodb底层是如何存储数据的?表中有哪些隐藏列?用户记录如何相互关联?如果你想知道以上三个问题的答案,那么请继续往下看。本文主要包括以下内容:1.磁盘还是内存?1.1磁盘数据对系统非常重要,如:用户的身份证、手机号、银行卡号、会员到期时间、积分等,一旦丢失,对用户的影响很大。那么问题来了,如何保证这些重要数据不丢失呢?答:将数据存储在磁盘上。当然有人会说,盘坏了怎么办?那你就需要一个备份,或者说主从。..好了,别说了,今天的重点不是这个。离家更近。大家都知道从磁盘读写数据至少需要两次IO请求才能完成。一种是读IO,一种是写IO。IO请求是很耗时的操作,频繁的IO请求必然会影响数据库的性能。那么,如何解决数据库的性能问题呢?1.2内存是否将数据存储在寄存器中?没错,操作系统从一个寄存器中读取数据是最快的,因为它离CPU最近。但是寄存器有一个很致命的问题:它只能存放极少量的数据,其用途主要是临时存放指令和地址,不能存放大量的用户数据。这样,数据只能保存在内存中。因为内存也能满足我们快速读写数据的需求,性能非常可观,只是比较寄存器稍慢。但有一点很烦人,内存是一种比磁盘更昂贵的资源。通常,500G或1T的磁盘很常见。但是你听说过500G内存吗?别人会认为你疯了。内存大小讨论的数量级一般是16G或者32G。内存可以存储部分用户数据,但不能存储所有用户数据,因为如果数据量太大,可能存储不下。另外,即使用户数据可以只存在内存中,如果未来某一天,数据库服务器或部署节点挂掉或重启,数据会丢失吗?如何做才不会因为异常情况导致数据丢失。同时,数据的读写速度能否得到保证?2.数据页我们可以把一批数据放在一起。写入时,先将数据写入内存中的某一批次,然后一次性将这批次的数据刷入磁盘。如下图所示:读操作时,一次从磁盘中读取一批数据,然后加载到内存中,然后在内存中进行操作。如下图所示:将内存中的数据刷入磁盘,或者将磁盘中的数据加载到内存中,都是批量的。这个批次就是我们常说的:数据页。当然,InnoDB中有很多不同类型的页,数据页只是其中的一种。我们将在这里关注数据页面。那么问题来了,什么是数据页?数据页主要用来存放表中的记录。它与磁盘中的双向链表相连,便于查找,可以非常快速地从一个数据页定位到另一个数据页。很多时候,由于我们表的数据量很大,可能会在磁盘上存储多个数据页。有一天,当我们要根据某个条件查询数据时,需要从一个数据页中找到另一个数据页。这时候,双向链表就派上用场了。磁盘中各个数据页的整体结构如下图所示:[Imageuploadfailed...(image-a761ff-1661219082240)]通常情况下,单个数据页的默认大小为16kb。当然我们也可以通过参数:innodb_page_size重新设置大小。但是,一般来说,它的默认值就足够了。好了,数据页的整体结构已经搞清楚了。那么,单个数据页包含什么呢?从上图可以看出,数据页主要包括以下几个部分:文件头、页头、最大最小记录、用户记录、空闲空间、页目录、文件尾3.用户记录对于一个新申请的数据页,用户记录为空。在插入数据时,InnoDB会分配一部分空闲空间给用户记录。用户记录是innodb的重中之重。我们平时保存在数据库中的数据就存放在里面。那么它包含什么呢?你不好奇吗?实际上,innodb支持的数据行格式有四种:compactrowformatredundantrowformatdynamicrowformatcompressedrowformat我们以compactrowformat为例:一条用户记录主要包含三部分:记录附加信息,包括变量length字段、空值列表和记录头信息。隐藏列,包含行id、事务id和回滚点。包含真实用户数据的真实数据列可以有很多列。让我们一起来看看这些内容吧。3.1附加信息附加信息不是真正的用户数据,它是辅助数据存储。3.1.1变长字段列表有些数据直接存储会有问题。例如,如果一个字段是varchar或text类型,那么它的长度是不固定的,它可以根据存储数据的长度而变化。如果不在一处记录数据的真实长度,InnoDB很可能不知道要分配多少空间。如果按照一定的固定长度分配空间,但实际数据占用的空间并不多,岂不是浪费?因此,需要在变长字段中记录一个变长字段占用的字节数,以便按需分配空间。3.1.2空值列表数据库中某些字段的值允许为空。如果在用户记录中保存每个字段的空值,显然是一种存储空间的浪费。有没有办法简单地标记它而不存储实际的空值?答案:将将为空的字段保存到空值列表中。在列表中使用二进制值1表示该字段允许为空,使用0表示不允许为空。只占用1位来表示一个字符是否为空,确实可以节省大量的存储空间。3.1.3记录头信息记录头信息用于描述一些特殊的属性。主要包括:deleted_flag:删除标志,用于标记记录是否被删除。min_rec_flag:最小目录标志,是非叶子节点中的最小目录标志。n_owned:拥有记录数,记录本组索引记录数。heap_no:堆上的位置,表示当前记录在堆上的位置。record_type:记录类型,其中:0表示普通记录,1表示非叶节点,2表示Infrimum记录,3表示Supreme记录。next_record:下一条记录的位置。3.2隐藏列数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示:目前innodb自动创建的隐藏列有3种:db_row_id,即行id,是一条记录的唯一标识。db_trx_id,交易id,是交易的唯一标识。db_roll_ptr,回滚点,用于事务回滚。如果表中有主键,则直接使用主键作为rowid,无需额外创建。如果表中没有主键,如果有唯一键不为null,就会作为rowid,不需要额外创建。如果表中既没有主键也没有唯一键,数据库会自动创建行ID。也就是说,在InnoDB中,隐藏列中的事务id和回滚点是必须创建的,但是rowid要根据实际情况来确定。3.3真实数据栏真实数据栏存放的是用户的真实数据,它可以包含很多列数据。这个比较简单,就不多说了。3.4用户记录如何链接?通过上面介绍的内容,您应该对用户记录的存储方式有了一定的了解。但是问题来了,一条用户记录是如何连接到另一条用户记录的,innodb怎么知道某条记录的下一条记录是谁呢?答案是:如前所述,记录附加信息>记录头信息>下一条记录的位置。多条用户记录通过下一条记录的位置构成单向链表。这样就可以从前到后查找所有的记录。4.最大和最小记录由上可知,如果一个数据页中有多条用户记录,则通过下一条记录的位置连接起来。但是有一个问题:如果我们能快速找到最大和最小的记录呢?这就需要在保存用户记录的同时保存最大和最小记录。保存到Supreme记录中的最大记录数。最小记录保存在Infimum记录中。保存用户记录时,数据库会自动创建两个附加记录:Supreme和Infimum。它们之间的关系如下图所示:从图中可以看出,用户数据是从最小的一条记录开始,遍历下一条记录的位置,从小到大一步步查找,最后找到最大的记录。5、页目录从上面可以看出,如果我们要查询某条记录,数据库会从最小的记录开始,一条一条地查找所有记录。如果中途发现,则直接返回记录。如果已经找到最大记录而没有找到想要的记录,则返回空。乍一看,没有问题。但如果你仔细想想。效率会不会有点低?这不是要扫描整个页面的用户数据吗?有没有更有效的方法?这需要使用页目录。说白了就是把一页用户记录分成几组,每组最大的一条记录保存在一个地方,就是页目录。每组中最大的记录称为槽。可以看出页目录是由多个槽组成的。如下图所示:假设将一个页的数据分为4组,那么在页目录中,对应4个槽,每个槽存储本组数据的最大值。这样就可以通过二分查找比较槽中记录的大小和需要查找的记录。如果用户需要查找的记录小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录大于当前槽中的记录,则向下查找到下一个槽。这样就可以利用二分查找快速定位到需要查找的记录。soeasy6.文件头和文件尾6.1文件头通过上面介绍的下一条记录所在的位置和行记录中的页目录,innodb可以非常快速的定位到某条记录。但是有一个前提,就是用户记录必须在同一个数据页。如果用户记录很多,我们在第一个数据页上找不到我们想要的数据,如果需要在另一个页面上找怎么办?这时候就需要用到文件头了。它包含很多信息,但我只列出4个最关键的信息:页码上一页下一页页码页面类型顾名思义,innodb使用页码、上一页和下一页来串联不同的数据页.如下图所示:不同数据页之间,由上一页的页码和下一页的页码组成一个双向链表。这样就可以从前到后一页一页地查找所有的数据。另外,页类型也是一个很重要的字段,包含了多种类型,其中比较著名的有:数据页、索引页(目录入口页)、溢出页、undolog页等。6.2At文件的末尾,前面提到过,数据库的数据是以数据页为单位加载到内存中的。如果数据有更新,需要刷新到磁盘。但是如果有一天运气不好,在程序刷入磁盘的过程中出现了异常,比如:进程被杀死,或者服务器重启。此时,数据可能只是部分刷新。如何判断最后一个U盘的数据是否完整?这需要使用文件结尾。它记录了页面的校验和。在将数据刷新到磁盘之前,计算页面的校验和。稍后,如果数据更新,将计算一个新值。这个校验和也记录在文件头中,由于文件头在前,所以会先刷入磁盘。接下来,当用户记录刷新到磁盘时,假设刷新了一部分,恰好程序异常。此时,文件末尾的校验和仍然是一个旧值。数据库会检查。文件末尾的校验和不等于文件头的新值,说明数据页上的数据不完整。7、页眉通过上面介绍的内容,可以很方便地访问到数据页,但是还有一个更重要的问题,就是记录的状态信息。例如,一页数据中保存了多少条记录,或者页目录中使用了多少个槽。这些信息是实时统计的,还是事先统计好的,保存在某个地方的?出于性能的考虑,上面的统计当然是先算出来,保存在一个地方。以后需要数据的时候,读出来会更好。保存统计数据的地方是页眉。当然,页眉并不仅仅保存槽数、记录数等信息。它还记录了:被删除的记录占用的字节数、最后插入的记录的位置、最大事务id索引、id索引级别。综上所述,多个数据页通过页码组成一个双向链表。在每个数据页的行数据之间,由下一条记录的位置组成一个单项链表。整体结构图如下: