浅谈Mysql数据存储

Mysql以其还不错的性能,简单易用、免费开源的优点深受开发人员喜好。网上有不少关于Mysql的优秀文章,其中有不少主题值得深刻讨论,好比:索引、事物、锁、数据表拆分等等。在这些主题背后都有不少知识理论,笔者认为这些知识理论都和Mysql的数据的存储有关系,理解Mysql的数据存储很是重要,是未来深刻学习探索Mysql的高级特性的基础,因此整理了这篇文章,笔者能力有限,有理解不到位的地方,欢迎你们留言指正。mysql

1.磁盘的存取原理

为了实现数据的持久化存储,mysql将数据存储到了磁盘上,因此客户端对数据库中数据进行查询,mysql须要将磁盘上的相关数据加载到内存中(这就是所谓的磁盘IO),根据查询sql筛选出数据返回给客户端。操做系统读写磁盘的基本单位是扇区,而文件系统的基本单位是(不明白“扇区”和“簇”概念的读者不要着急,下边会进行介绍的),也就是说操做系统将磁盘上的数据文件加载到内存中是一整块一整块的读取的,即使仅须要查询一块数据中的一个字节,也须要将这块数据全加载到内存中来。mysql数据库5.5版本以后,默认以InnoDB做为存储引擎,InnoDB就是以数据页来存储数据的,数据页的大小默认为16KB,操做系统将数据库中的数据加载到内存也是以数据页为基本单位。sql

下面咱们来看一下磁盘是怎样存取数据的,下面是一个磁盘的物理结构示意图:数据库

能够看出磁盘有多个盘面套在心轴上,每一个盘的正反面有一个磁头用于读写数据,磁盘在工做时,盘面的高速旋转引发空气动力,使得磁头悬浮在盘面上,与盘面距离不到1微米,能够在极短的时间内精肯定位到计算机指令指定的磁道上。下面咱们来了解一下磁道、扇区、柱面和簇的概念。缓存

  • 每一个盘片的每一个盘面被划分红多个狭窄的同心圆环,数据就是存储在这样的同心圆环上,咱们将这样的圆环称为磁道(Track),每一个盘面能够划分多个磁道。在每一个盘面的最外圈,离盘心最远的地方是“0”磁道,向盘心方向依次增加为1磁道,2磁道,等等。硬盘数据的存放就是从最外圈开始。磁头只能沿着盘面的径向移动,移动到要读取数据的磁道上方,这段时间称为寻道时间bash

  • 磁盘的盘面上磁道数有成千上万个。每一个磁道上能够存储数KB的数据,但计算机并不须要一次读写这么多数据,基于此又把每一个磁道划分红若干弧段,每段称为一个扇区(Sector)。扇区是硬盘上存储的物理单位(从DOS时代起,每一个扇区存储512字节的数据,已经成为业界不成文的规定)。扇区的编号是从1开始的,而不是0。磁头到达指定磁道后,盘片经过旋转,使得要读取的扇区转到读写磁头的下方,这段时间称为旋转延迟时间(rotational latencytime)性能

  • 柱面实际上是咱们抽象出来的一个逻辑概念,前面说过,离盘心最远的磁道为0磁道,依此往里为1磁道,2磁道,3磁道....,不一样盘面上相同磁道编号则组成了一个圆柱面,即所称的柱面(Cylinder)。磁盘数据的读写都是按照柱面进行的,即磁头读写数据时首先在同一柱面内从0磁头开始进行操做,依次向下在同一柱面的不一样盘面(即磁头上)进行操做,只有在同一柱面全部的磁头所有读写完毕后磁头才转移到下一柱面,由于选取磁头只需经过电子切换便可,而选取柱面则必须经过机械切换。电子切换比从在机械上磁头向邻近磁道移动快得多。所以,数据的读写按柱面进行,而不按盘面进行。 读写数据都是按照这种方式进行,尽量提升了硬盘读写效率。学习

  • 物理相邻的若干个扇区称为了一个,文件系统的基本单位是簇(Cluster)。 簇通常有这几类大小 4K,8K,16K,32K,64K等。簇越大存储性能越好,但空间浪费严重。簇越小性能相对越低,但空间利用率高。ui

从上边对磁盘结构分析咱们知道:磁盘读取数据时,磁头经过盘面径向移动到磁道上,而后盘面旋转到目标扇区的时候开始读取数据,若是数据都存在相邻的扇区,相比于数据存储在不一样的磁道的扇区上来说,磁盘读取数据的效率就会高不少(不须要寻道时间,只须要不多的旋转时间),这就是顺序I/O性能优于随机I/O的缘由。编码

2. InnoDB引擎数据存储

Mysql数据库查询数据效率的瓶颈在于磁盘I/O(这是一件很耗时的工做)。数据库中数据存储的基本单位是一张表中的一条记录,记录是按照行为单位存储的,可是数据库加载磁盘数据到内存并非以行为单位(这样的话每读取一条记录都须要一次磁盘I/O,效率很是低。),前边说过,Mysql新版本默认存储引擎是InnoDB,InnoDB按照页来存储数据,页的大小默认为16KB。(可经过下面命令行查看:)spa

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.02 sec)
复制代码

所以数据库加载数据到内存,不论读一条记录,仍是读多条记录,都是将这些记录所在的页进行加载。也便是说,数据库管理存储空间的基本单位是页(Page)。

2.1 记录行的存储

咱们平时向Mysql中插入数据以“记录”为基本单位,记录在磁盘上的存储方式被称为“行格式”或“记录格式”。InnoDB存储引擎支持不一样的行格式(Compact、Redundant、Dynamic和Compressed),它们的原理基本上是相同的。咱们以Compact行格式为例,下边是其存储示意图:

Compact行记录格式示意图

一条记录数据的存储能够分为两部分:“额外信息”和“真实数据”。 额外信息用来描述记录,分为变长字段列表、NULL值列表和记录头信息,这部分信息很是重要,咱们分别来看一下:

  • 变长字段列表: MySQL支持的一些变长的数据类型,好比VARCHAR(M)、TEXT等。这些变长字段中存储的字节数量是不固定的,存储这个数据的真实字节数颇有必要,这在解析数据的时候,数据库就知道从真实数据区域取出哪部分数据了。全部变长字段的真实数据占用的字节长度都存放在记录的开头部位,造成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放。

咱们在项目使用到char类型,好比使用char(32)来存储用户的密码,那么使用char类型的字段会被添加到变长字段列表中吗?答案是有可能会!由于项目中数据表使用的字符集是不肯定的,最经常使用的utf8mb4使用1~4个变长字节来编码数据,中文和英文所占用的字节数是不一样的。

对于项目中绝大多数使用存储量小的字段,好比说varchar(32),tinyint(4)等,假设使用utf8字符集,这种字段存储的真实数据长度必定不会超过255,使用一个字节表示就能够了。可是若是字段的真实数据可能会超过了255该怎么表示呢?分为两种状况:当真实数据字节数小于127的时候,用1个字节表示,大于127的时候使用2个字节表示,也就是说字节的最高位表示该字节表示的是一个变长数据的一部分仍是所有。

  • NULL值列表:数据表中的某些列可能存储NULL值,把这些NULL值都放到记录的真实数据中存储很浪费存储空间,因此Compact行格式把这些值为NULL的列统一管理起来,存储到NULL值列表中。在表中,主键列和使用NOT NULL修改过的列不容许存储NULL值,其余的列若是也没有NULL值,那么NULL值列表也就不存在了,也就是说NULL值列表并不老是存在的,上边说到的变长字段列表也是同样。标示一条记录中的数据是否为NULL使用一个二进制位就能够搞定了,1为NULL,0为非NULL,NULL值列表标示也是按照记录列的顺序逆序存放的。

问:变长字段长度列表、NULL值列表中的信息之因此按照列的顺序逆序存放?答:这样可使记录中位置靠前的字段和它们对应的字段长度信息加载到内存中时,位置距离更近,可能会提升高速缓存的命中率。

  • 记录头信息:记录头信息由5个固定的字节组成,5个字节是40个二进制位,这40个二进制位描述了记录的不一样属性信息,这些信息很是重要!

下面是对记录头标志位信息的详细描述:

名称 占用bit数 描述
预留位1 1 暂时未使用到
预留位2 1 暂时未使用到
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 当前槽拥有的记录数
heap_no 13 当前记录在记录堆的位置
record_type 3 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录
next_record 16 下一条记录的相对位置

记录头信息很是多,可是这些信息在数据查找和存储的过程当中很是有用,就好比说next_record记录着当前记录下一条记录的相对位置,对于数据查找很是方便。delete_mark标示记录是否被删除,由此咱们能够知道,数据库的物理删除并非直接从磁盘上物理删除,而是经过一个标志位标示,等未来要用到这一部分磁盘空间的时候再释放。其余的标志位信息也比较重要,咱们下边用到的时候会详细说明。

咱们再来看记录的真实数据部分,记录的真实数据中除了咱们数据表中自定义的一些字段外,数据库会额外的添加一些隐藏列用于完成数据快速查找、事物提交、回滚等操做。隐藏列信息以下:

列名 真实名称 是否必须 占用空间 描述
roll_id DB_ROW_ID 6字节 惟一标示一条记录的行ID
transaction_id DB_TRX_ID 6字节 事物ID
roll_pointer DB_ROLL_PTR 7字节 回滚指针

这些隐藏列的信息很是重要,咱们来逐一说明一下:

  • DB_ROW_ID:咱们知道它是无关紧要的,这跟Innodb主键的生成策略有关。Innodb优先使用用户自定义主键做为主键,若是用户没有定义主键,则选取一个Unique键做为主键,若是表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列做为主键。为何InnoDB非要生成主键呢?由于数据库为了更好的进行范围查找和数据匹配,老是将记录按照主键从小到大存储的,有序对于数据查找很是重要,记录存储有序了,就能高效使用二分查找策略了。

  • DB_TRX_ID:咱们知道InnoDB和MySIAM一个重要的区别是,InnoDB支持事物,事物要保证数据操做的ACID,InnoDB为数据表中的每条记录都生成一个transaction_id列,当记录被事物使用到时,使用它来存储事物id。

InnoDB支持行锁,不少人认为事物必需要配合InnoDB的行锁才能完成工做,这种观点是错误的!事物自己有本身的隔离级别,隔离级别不一样,对数据一致性的要求也不一样,事物自己有本身的一套MCVV(版本链控制),不必定使用到行锁。

  • DB_ROLL_PTR:上边咱们提到了事物有本身的MVCC,事物失败后,须要回滚。而具体回滚到什么状态,或者回滚到事物执行的哪一个节点(Mysql的事物支持使用savepoint进行打点,未来能够回滚到指定节点),这些工做须要有记录有一个字段标示,这就是回滚指针列的做用。

真实数据存储,还有一点很是重要,Innodb将记录存在默认大小为16KB的数据页中,而咱们真实存储的数据记录,好比text类型,超过16KB也是有可能的,前边咱们也提到,记录存储自己还有一些额外的信息须要存储,这样咱们一个数据页中每每会存不下这些大记录。这个时候就会发生页分裂,多出来的部分数据会被存储到溢出页中。

InnoDB默认采用Dynamic的行格式储存记录,dynamic行格式中列存储是否放到off-page页(溢出页),主要取决于行大小,它会把行中最长的那一列放到off-page页,直到数据页能存放下两行。

以上就是Innodb引擎关于行记录的介绍,下边咱们再来看一直强调的数据页的结构,以及记录是怎样组织起来存储到数据页中的。

2.2 Innodb页结构和记录存储

Innodb为了实现不一样的目的设置了不少种页,咱们上边提到的储存记录数据的页叫作数据页,此外还有一些其余的页来完成不一样的工做:好比存放INODE信息的页,存放undo日志信息的页等等,本节咱们主要探讨数据页的存储结构,其余类型的页在“表空间”中会简单提到,不做为重点。

为了实现数据的快速检索和存储的科学性,16KB大小的数据页又被大体分红了7个部分:

数据页双向链表

先简单介绍一下File Header、FileTailer,而后再重点详细说明行记录是怎样组织存储到数据页中的。File Header 和 File Tailer 是全部类型的页都通用的结构。Mysql是以页为单位将磁盘数据加载到内存中,而后进行查询、修改,以后再以页为单位,将修改过的数据刷到磁盘上。为了保证数据的完整性,InnoDB在每一个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,存储页面的校验和(4字节)、日志序列位置(LSN)(4字节),同FileHeader中的校验和、LSN相对应,用来校验文件数据同步的完整性。FileHeader还存储了另一些页的通用信息,好比页属于哪一个表空间(下一节咱们会说什么是表空间),页的上一页、下一页信息,这是很是重要的,数据页基于File Header中记录的上一页、下一页信息构成一个双向链表:

数据页之间经过双向链表链接

数据页之间经过双向链表链接意味着在物理空间存储上,数据页之间并不必定是连续的,可是Mysql尽量的会去保证数据页在物理空间上的连续,由于Mysql常常进行范围查找,物理空间连续意味着可能更多的使用到顺序IO,更详细的细节咱们下一节会具体讲到。

数据记录会存储到User Records部分,一开始数据页中User Records部分并不存在,不占据任何存储空间。随着插入数据的增多,User Records存储的用户记录愈来愈多,Free Space的空间愈来愈少(像海绵同样自由压缩),直到没有了Free Space,用户数据记录插入时,申请新的数据页进行插入。咱们前边提到,Innodb会为每一条记录生成主键,若是定义的数据表中没有主键,就会生成一个隐藏的row_id列,在User Records存储区存储的记录是按照由小到大的顺序排列的,为了更好的管理数据记录,InnoDB定义了两条伪记录:最小记录与最大记录(infimun+suprenum)。这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成,一共占用26个字节。上述描述的数据插入过程以下图所示:

数据页插入记录的过程

咱们再回过头来讲说记录行格式中的头部信息。记录行的头部信息40个字节是固定的表示记录的属性信息,咱们上边提到的最小记录和最大记录的record_type分别为二、3,而咱们用户字节插入的数据记录record_type为0。咱们还知道next_record表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。 这块咱们要重点理解一下,next_record存储的是当前记录到下一条真实数据的偏移量而不是到到下一条记录的偏移量,由于记录还包含有额外的信息。记录在数据页的User Records区按照主键大小排序储存,这样经过记录的next_record信息,数据页中的数据记录之间就造成了一条单向链表:

最小记录和最大记录的头部信息中heap_no是最小的0和1,在数据页中是排在最前边的,最大记录的next_record值为0,从最小记录到最大记录经过next_record存储的偏移量构成了逻辑上的单向链表(图中将每条记录分开表示是为了展现记录之间的关联,真实数据存储中,next_record存储的是偏移量,数据记录在物理空间存储上是连续的)。记录中的delete_mask都是0,表示记录并无被删除。假如用户记录2被删除,会发生什么事情呢?mysql会将用户记录的记录头部信息的delete_mask置为1(并无立刻清理存储空间),同时将指向它的上一条记录的next_record改成指向它下一条记录数据起始的偏移量,依旧维护着一条单向链表:

数据页删除用户记录

记录头标志中的n_owned存储的是当前槽拥有的记录数,是什么?它和数据页部分的Page Directory又有什么样的关系呢?为了弄明白这两个问题,咱们先来看看在一个数据页中查找一条用户记录是怎样实现的。咱们知道用户记录在数据页中按照主键大小顺序存储,根据主键id查找记录,可使用二分查找提升效率。二分查找的次数和记录数量有关系,数据页中存储的用户记录可能成百上千条,有没有什么办法能加快二分查找的速度呢?另外咱们知道二分查找在数据量大的状况下,很是的高效,在用户记录只有比较少(0-8)个的状况下,也许并无顺序查找来的简单高效。因此InnoDB在数据页中为了加快数据查找速度,将用户记录作了分组,每一个组中最后一条记录(也就是组中最大用户记录)的头信息中n_owned标志位记录着当前组拥有的记录数量;同时,将每一个组中最后一条记录的地址偏移量单独提取出来存储到Page Directory处,造成了所谓的页目录。页目录中这些地址偏移量被称为槽。正如刚刚所说的,组中分配记录数量的多少是有考量的。InnoDB有这样的规则:最小记录的分组只能有1条记录(就是它本身,最小记录),最大分组拥有的记录数是1~8条(包括用户记录和它自己),其余分组中记录的条数在4~8条。咱们来分析下随着用户记录的插入,分组和“槽”变化的过程:

  • 初始状况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
  • 每插入一条记录,都会从页目录中找到主键值比该记录的主键值大而且差值最小的槽,而后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
  • 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分红两个组,一个组中4条记录,另外一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。

数据页中的槽位

有了数据页的目录部分(Page Directory),咱们再来梳理一下在一个数据页中查找一条记录的过程。分为两步:第一步:首先经过二分法肯定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录。第二步:经过记录的next_record属性遍历该槽所在的组中的各个记录。到这里咱们就清楚了InnoDB数据页的结构,也明白了怎样在一个数据页中查找一条指定的记录了,下面咱们从更大维度,更广阔的视角来看一看Mysql的数据存储的设计。

2.3 简单理解Innodb表空间

为了更好的管理数据页,Innodb引入了表空间的概念。(Oracle的存储结构是按照表空间进行管理的,Innodb模仿了Oracle的数据存储方式)。

咱们先来区分一下Mysql的安装目录和数据目录的区别:在操做系统上安装完Mysql以后,咱们就获得了Mysql的安装目录,其中安装目录下的bin文件夹下有管理Mysql的可执行文件,例如mysql、mysqld、mysqld_safe等等。而Mysql在运行期间产生的数据倒是在数据目录下存放的,能够经过命令查看mysql的数据目录:

mysql> show variables like 'datadir';
+---------------+------------------------+
| Variable_name | Value                  |
+---------------+------------------------+
| datadir       | /usr/local/mysql/data/ |
+---------------+------------------------+
1 row in set (0.04 sec)
复制代码

咱们再来看看数据库和数据表在操做系统上是如何表示的:每一个数据库对应mysql数据目录下的一个同名文件。在咱们使用create database建立数据库的时候,若是使用Innodb引擎,会在数据库同名文件下建立一个名为db.opt的文件,该文件包含了数据库的字集、比较规则等属性信息,它是一个二进制文件,笔者使用cat file查看信息时是一些乱码信息。和数据库同样,数据表的存储也须要一个文件存储表的属性信息,例如:表中的列、每一个列类型(int,varchar之类的)、字符集、使用了哪些索引等等。**在Innodb引擎中,建立表的时候会生成一个.frm的数据表同名文件来表示数据表结构。**咱们知道Innodb以页为单位存储数据,为了更好的管理数据,Innodb引擎将数据表中的数据存储到表空间下,表空间是一个抽象的概念,它对应着文件系统上的不少文件,表空间下有许多许多页,咱们的表数据就存储在这些页中。Mysql为Innodb引擎设计了不少表空间,例如:系统表空间、独立表空间、通用表空间(general tablespace)、undo表空间(undo tablespace)、临时表空间(temporary tablespace)等等。咱们就来简单介绍一下系统表空间和独立表空间:

  • 系统表空间:默认状况下,Innodb会在数据目录下建立一个名为ibdata1,大小为12M的文件,系统表空间只有一份,随着存储数据量的增大自增加。MySQL5.5.7到MySQL5.6.6之间的各个版本中,咱们表中的数据都会被默认存储到这个 系统表空间。
  • 独立表空间:MySQL5.6.6以后的版本中,Mysql会把使用了Innodb引擎的数据表存储到数据表对应的独立表空间中去,数据表的独立表空间文件的名字和数据表名相同,拓展名为.ibd,也就是咱们在建立一个数据表的时候,会生成两个文件:表.frm用来存储数据表的属性信息,表.opt用来存储数据表的数据和索引信息

和Innodb不一样的是,MyISAM并无表空间,表数据都放在对应的数据库子目录下,使用MyISAM建立数据表以后会生成三个文件:表.frm、表.MYD、表.MYI,其中表.frm和Innodb同样,存储数据表的属性信息,表.MYI存储表的索引文件,表.MYD存储表的数据文件。这点和Innodb不一样,咱们说Innodb将表数据和索引都存储到表空间下,由于Innodb索引的设计和MyISAM是不一样的,Innodb中数据即索引,索引即数据。

下边对表空间的存储结构作简单介绍:

数据页的结构咱们已经很清楚了,为何好端端的又提出了一个区的概念呢?咱们前边说过,Mysql尽量的将数据顺序储存来尽量高的使用顺序IO提升磁盘加载速度,一个页默认大小16KB,是能够存放更多记录,这些记录已经顺序存储了,还不够,因此表空间又添加了区的概念,一个区默认由64个数据页组成,一个数据页默认大小是16KB,一个区也就是1M。这样1M的数据顺序存储了,在插入大量数据的时候,Innodb还能够申请多个连续的区来存放用户数据,这样,多个区就又顺序存储了,这些细节的考虑对于Mysql磁盘IO加载数据性能的提高是不可忽略的。

区存储数据的粒度仍是太粗了,在数据量比较少的状况下,区中每每有不少空间不能被有效利用,基于此,又将区分为了四种:空闲的区、有剩余空间的碎片区、没有剩余空间的碎片区、附属于某个段的区。这样在数据表存储数据比较少的状况下,就可使用有剩余空间的碎片区存储数据了。

那段呢?段又是什么?Innodb引擎是将数据和索引都存储成一个B+树结构,也就是索引和数据是放在一块儿的(这是聚簇索引的概念),存储索引的页和存储数据的页是不一样类型的,在表空间中,索引和数据也是分开存储的,另外还有关于事物的处理,也有不一样的段用来存储数据。为了更好的区分这些,Innodb使用段来将不一样的数据作区分,存储用户数据记录的叫数据段,存储索引的叫索引段。一个段由不少附属于它的区和一些其余区的零散的页组成。(附属于一个段的区存储的数据都会这个段的相关数据),这样就是实现了对特定数据的区分管理。

3.总结与回顾

本节绝大部份内容都是在探讨Innodb引擎的数据存储的,从记录的存储到数据页的设计,后边简单谈了一下表空间概念,按部就班的帮助你们了解了Mysql数据存储的概貌。文中提到了不少细节,好比数据记录都有哪些隐藏列,数据页的Page Directoy区是怎样设计的等等,理解这些很是重要,Mysql的索引、事物有不少原理都是基于这些基础的数据存储结构设计实现的,但愿本文能给想要深刻学习Mysql的读者带来一些收获,谢谢你们!

相关文章
相关标签/搜索