操作系统存储与访问二进制流
磁盘存储的一些基本概念:扇区、簇(block 块)、磁道、柱面
扇区是磁盘中保存数据的最小单位,大小为512Bytes
,而簇块是相连的扇区,簇是操作系统进行数据交换的基本单位,在操作系统内存空间中与簇相对应的概念为页;
程序通过具体的扇区号访问
在原始的生磁盘(raw) 中,我们的程序要存储数据时,我们需要向磁盘管理器中传入 盘面(磁头号)、磁道、扇区号 ,这样磁盘管理器才可以对应到一个具体的扇区并写入数据;
程序通过block号访问
这样的使用方式很明显我们的程序与磁盘的访问方式高度耦合在一起(即盘面、磁道与扇区号的组合),因此我们需要增加一层抽象 - 数据库驱动程序, 数据库驱动程序负责屏蔽磁盘数据访问的细节,并对程序提供以block(数据块)
作为磁盘数据访问的基本单位的驱动程序使用方式,驱动程序负责解析程序中block
对应的盘面、磁道与扇区号,在Linux0.11
中,一个block
对应两个扇区,现代操作系统可以根据当前主机的磁盘大小、内存大小等初始化在该主机中block
对应磁盘中几个扇区,而对应的扇区越多,磁盘I/O的效率越高(因为一次性读写的数据量变多了,I/O的次数减少了),但是也因此造成空间的浪费,因为一个block
作为操作系统分配的最小磁盘数据单元,block
越大,自然写不满的概率就越大,浪费空间的概率也越大。
程序通过fd(文件描述符)访问
虽然通过block号
的访问方式,已经屏蔽了大部分磁盘访问的细节,但是我们要存储在磁盘上的数据流可能会对应很多个block
,那么我们的程序需要自己去管理与维护这个 block列表,因此我们还是要在我们的程序与block
块之间增加一层抽象-文件,在操作系统中,我们可以通过fd(文件描述符)
对文件进行访问,而每个fd会对应一个block链表(FAT
表中会维护该列表),这样每个文件可以通过对应的文件描述符找到该文件的对应的磁盘块列表以实现对数据的访问。
索引优化
不过链表的长度比较长的时候,其查询效率会退化成接近o(n2),所以使用链表大多会在链表长度超过一定长度时做一些优化(如java8对于HashMap的优化
),而Linux 0.11
对长度超过6的链表会添加索引表,一级索引即为该文件所对应的block
的 聚簇索引,<blockId, blockPoniter>,如果增加了一级索引之后仍然存在 则增加二级索引(稀疏索引),以便可以很快的定位到具体的要访问的数据块。
数据库管理二进制流
依托操作系统的文件抽象进行管理
我们存储在数据库的一个个表对于操作系统而言仍然是一个个二进制流,操作系统也会通过上述管理block
的方式管理一个个存储着记录的文件对应的磁盘空间;
但是又不是完全一致,数据库管理系统(DBMS
,常见的为Mysql等)依托操作系统管理文件的基础上提出了自己新的抽象,即表与表空间,而表与表空间对于操作系统而言是不可见的,即操作系统并不知道哪些文件是属于一个表空间的,哪个文件中存储着哪些表的记录,比如说Mysql中如果没有定义自己的表空间,并且没有开启innodb_file_per_table
,此时就会将该表的记录存放在公共表空间中,而这个表空间对应了公共表文件(存放表记录的文件),因此这个表文件中可能会存在多个表的记录,而这对于操作系统是完全无感知的。
并且数据库为了更好的以不同的粒度管理磁盘空间,又增加了 盘区、段的概念,和操作系统因为页的存储空间比较小导致维护成本比较高从而推出的段页式大同小异;其中一个页中有一到多个记录,一个盘区中会有多个页,一个段中会有多个区。
在MySQL
中,如果我们开启了innodb_file_per_table
,那么每个表都会有属于自己的表空间,如果没有开启则会放在公共的表空间中,而一个表空间可以对应一个或多个操作系统文件;下图为Mysql中对上述概念的实现简图:
即,所谓表本质上还是存储在磁盘上的二进制数据,并且数据库作为运行在操作系统上的进程,依据操作系统管理文件的方式对自己的表文件进行管理。
二进制数据是如何映射成表的呢?
上述主要讲述了表文件是什么,如何存储的,数据库又是如何依赖操作系统访问存储在磁盘上的表文件中的二进制流的。
不过读取完二进制流后,就像是解析HTTP
报文一样,我们需要通过对二进制流得按照相应规则进行转换才可以表达含义;对于数据库而言,则最基础的是如何从这个二进制流中解析出一个个记录行。
数据行
一个表是由表的模式(schema)
和表的数据
构成的; 比如说Person
表由人名、年龄、地址三个字段和三个字段的类型等属性构成了Person
表的模式(DDL),而表的数据即为[小白, 18, 掘金], [小黑, 19, 掘金]
这样的字段构成。
人名 | 年龄 | 地址 |
---|---|---|
小白 | 18 | 掘金 |
小黑 | 19 | 掘金 |
所以要将存储在簇中的二进制流映射成逻辑概念上的表,我们(Mysql存储引擎)需要将二进制流按照相应的表模式进行识别与管理。
上面这种根据表模式进行直接映射是比较值观的,但存在很多问题,比如说一个磁盘块中已经存储了一个数据行的数据,而剩下的空间又不够新的数据行存储时,或者说我需要修改一个char
类型的数据时,并且增加char字段的字符数增加后当前数据页无法继续容纳该数据行时应该如何处理呢?存储引擎又应该遵循什么规则呢?
因此我们需要考虑具体DBMS
在数据页中存储数据行的方法,我们先来看Mysql的Innodb存储引擎比较常用的数据行格式:Compact
:
Compact
MySQL
中通过Compact
行记录的格式来存储数据行(Redundant
数据行格式已经不再使用,之后会分析为什么会被废弃): 其中变长字段即varchar()
,存储改行数据中数据类型为varchar字段的长度,相比之下char
为定长,如char(10),表示该字段长度为10字节,如果我们插入的为bb
,按照ASCII的规则,他只占用了两个字节,剩下的8个字节会通过0x20
进行填充,而使用varchar()
,则会在变长字段长度列表 表明该字段长度为2字节,不会再占用多余的空间。
但是当我们设置表的编码格式为多字节编码格式如:GBK
、UTF-8
时, char字段
也视为变长字段进行存储,与 varchar()
无异。
NULL标志位
也是如此,出于节省存储空间的目的;比如当第二列的字段值为空,则我们只需要在NULL标志位字段中的第二位置1
即可,不再会存储列值。
RowHeader
中存储的信息主要是为了方便在页 的层面去管理数据行,存储了在页中下一条数据行的相对位置,记录的类型,索引堆中该记录的排序记录。
而事务ID、回滚指针与MySQL
的事务特性与锁机制息息相关;
最后的row_id
则是在表模式
中没有显示定义主键时,会自动选择首个非NULL无重复的列作为主键,并将主键的值冗余到该列中,因此为了避免不必要的开销,还是需要显示定义主键值。
数据页
数据页即为一条条行数据组成,MySQL
中页的默认大小为16KB
,因此我们的问题从如何在页中管理数据行转变为如何在区
中管理页。
因此我们需要谈到B+树
这个数据结构,从二叉查找树到平衡树再到B+树,MySQL最终选择了B+树。 原因主要是因为二叉查找树在面对排好序的数据时,树深会急剧增加,并且查询的时间复杂度会退化到链表级别;而二叉平衡树则需要花费比较高的代价维护二叉树的平衡性;而B+树作为一个实现多级索引的数据结构,通过自己特殊的分裂与合并算法使得二叉树的树身得到有效控制且无需花费比较大的代价维护平衡性。
页按照其存储内容可分为数据页、索引页 ,上图中上方索引页为稀疏索引,主索引,下面四个索引页为稠密索引、聚集索引。
稠密索引:每个存储在数据库中的记录行都会对应一个索引记录;
聚集索引:索引按照当前索引的索引键顺序排列,并且每个数据行都会有一个索引记录,但一个表只可以有一个聚集索引;
稀疏索引:并不是每个存在在数据库中的数据行都会对应一个索引记录;
主索引:数据库中每个数据块(页)对应一个索引,即索引的维度由数据行变为数据页,降低了索引的成本的同时并不会过多降低查询的效率,因为操作系统内存与磁盘是以页作为最小交换单位的。
数据页如何管理其中的数据行
Mysql
中通过pageHeader
存储当前数据行的一些信息
数据库索引与操作系统文件索引的关系:
我们知道,数据库存储引擎是依赖操作系统管理自己的表文件的,但是操作系统中FAT索引是<文件描述符, 首个磁盘块>的映射关系,而这远不能满足数据库,因此DBMS需要依据操作系统的文件索引建立自己的一套索引体系:
待更新