当前位置:首页 > 公众号精选 > 程序员小灰
[导读]跟着官方文档 源码 源文件三件套自己探索

mysql 的一行记录,最终肯定是存储在磁盘上,也就是肉眼可见的文件上,今天我们的目标很简单,就是看看它到底是怎么存的。当然还有一个更重要的目标,就是以这个为引子,带大家完全通过一手资料,来揭秘这个问题的答案。所以大家阅读时,不要完全奔着寻找这个答案去,如果很少通过一手资料去研究问题,那么相信完整读过本文,你会有收获和启发的。我们先准备好三样东西。1. 准备好一个 mysql 5.7 并将其启动。2. 准备好 mysql 的官方文档放在旁边:
https://dev.mysql.com
3. 准备好 mysql 的源码,万一要用呢,别怕:
https://dev.mysql.com/downloads/mysql/5.7.html
一手资料,就是官方文档  源码  二进制文件,其中二进制文件是我们自己去磁盘中找的,一会就知道了。Let's Go!mysql 会把文件存在哪里呢?先找到他。mysql> SHOW VARIABLES LIKE 'datadir';
--------------- ---------------------------------------------
| Variable_name | Value                                       |
--------------- ---------------------------------------------
| datadir       | C:\ProgramData\MySQL\MySQL Server 5.7\Data\ |
--------------- ---------------------------------------------
1 row in set, 1 warning (0.00 sec) 我是 windows,就在这里了,进入这个目录。

这些是啥先不管,盯着它看就好了,我们继续。
第一步:创建数据库

mysql> create database flash; 盯着刚刚的文件夹看,此时会多出一个文件夹

同时这个文件夹里会多出一个文件,叫:|-- flash
|--
db.opt
|-- flash
|-- performance_schema

看一眼它里面的内容,就知道他是干嘛的了。default-character-set=latin1
default-collation=latin1_swedish_ci
default-character-set 是默认字符集,default-collation 是默认字符序。字符集大家都了解,就不展开了。字符序就是字符的排序和比较规则,一般以 _ci 结尾的表示大小写不敏感,_cs 结尾的表示大小写敏感,_bin 结尾的表示用编码值进行比较。含义知道了,那我们重新设置它应该会有所变化,我们把这个数据库设置为开发时常用的 utf8mb4 格式。ALTER SCHEMA `flash` DEFAULT CHARACTER SET utf8mb4;
再看 db.opt 文件,内容已经发生了变化。default-character-set=utf8mb4
default-collation=utf8mb4_general_ci OK,那我们现在对这个文件有了个初步认识,创建一个新的数据库时,首先会多出一个以数据库名为名称的文件夹,然后文件夹里面会多出一个描述数据库配置的 db.opt 文件,我们继续!

第二步:创建表

创建一张 student 表,三列,其中 id 是主键。CREATE TABLE `flash`.`student` (
`id` INT NOT NULL,
`name` VARCHAR(10) NOT NULL,
`age` INT NULL,
PRIMARY KEY (`id`)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4;

此时 flash 文件夹中,多出了两个文件|-- flash
|-- db.opt
|-- student.frm
|-- student.ibd
|-- flash
|-- performance_schema
为了严谨,我们先看下 db.opt 文件有没有变化,发现没有任何变化,说明创建表对这个 db.opt 配置信息文件,没有影响。

再点开 student.frm,坏了,乱码了。

说明这个文件不是文本文件,用二进制方式打开它。

我把一些关键的地方都标上了含义,那这个文件的作用大家就一目了然了,就是记录表结构嘛,具体的格式可以看 frm 文件结构的官方文档(写得太复杂了...我反正是没看):https://dev.mysql.com/doc/internals/en/frm-file-format.htmldb.opt 记录了数据库信息,student.frm 记录了表结构信息,那重头戏自然就在那个 student.ibd 文件了,这里一定存着具体的数据呀,索引呀等信息吧。打开它!果不其然还是乱码,那还是二进制打开它!截取了中间信息较为丰富的某部分。

发现一点也看不懂。
第三步:插入数据

我们加几条数据看看。INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('1', 'dibingfa2', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('2', 'dibingfa2', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('3', 'dibingfa3', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('4', 'dibingfa4', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('5', 'dibingfa5', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('6', 'dibingfa6', '2');
INSERT INTO `flash`.`student` (`id`, `name`, `age`) VALUES ('7', 'dibingfa7', '2'); 再二进制打开它!

发现有些东西我们可以看出点端倪了!猜测下这部分就是每一行的记录信息吧。我们插入了七条数据,我发现这些二进制串有一段可以分割成七对,我把他单独拿出来,并且按行分割。

我们将第一行记录拆解,第一行记录的表数据是这样的。1 dibingfa 2在 ibd 文件中是这样的。08 00 00 00 10 00 24 80 00 00 01 00 00 00 00 0A 07 A7 00 00 01 1B 01 10 64 69 62 69 6E 67 66 61 80 00 00 02这串数据代表啥意思呢?由于本文只能参考官方文档,我们看这里,即 Innodb 行格式。https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html看这部分(我们的行格式是 DYNAMIC 类型,不过参考 COMPACT 类型描述也行,因为几乎一样,之后再说):

不要看这么一大长串就害怕,我们一点点来,别着急。这个文档,我可能没资格评价,但我个人觉得写的很烂,一大堆废话也没说明白格式是什么样子,每个字节表示什么。不过也可能作用并非如此。好的官方文档应该是能把每个字节和每一位都解释清楚的,无奈这个文档不行,那我们就去找更接近一手资料的源码。我找到了源码,还是很清晰的,注释上就写明了每一行记录的磁盘数据格式,太好了,不用看代码了。

再贴上刚刚的第一行记录。08 00 00 00 10 00 24 80 00 00 01 00 00 00 00 0A 07 A7 00 00 01 1B 01 10 64 69 62 69 6E 67 66 61 80 00 00 02来一点点看,第一部分。| length of the last non-null variable-length field of data ... ...|

...
| length of first variable-length field of data |
这部分是变长字段长度列表,就是依次记录所有变长字段的长度,由于我们只有一个变长字段 varchar(10) 的 name,所以就是 08,我们存储的 "dibingfa" 刚好是 8 个字节,对上了。那如果是多个,很显然,就这样存。

错!应该是这样存,也就是逆序存放,具体为啥后面说。

OK,这就是第一个字节 08 所表示的含义。再往下。| SQL-null flags (1 bit per nullable field), padded to full bytes |第二个结构叫 NUll 值列表,用 1 位表示一个 NULL 值,要填充满一个字节,那往下的一个字节是 00,一看我们的记录中也确实没有 NULL 值,对上了。具体来说,同样也是逆序存放的。

继续。| 4 bits used to delete mark a record, and mark a predefined

minimum record in alphabetical order |
| 4 bits giving the number of records owned by this record
(this term is explained in page0page.h) |
| 13 bits giving the order number of this record in the
heap of the index page |
| 3 bits record type: 000=conventional, 001=node pointer (inside B-tree),
010=infimum, 011=supremum, 1xx=reserved |
| two bytes giving a relative pointer to the next record in the page |
ORIGIN of the record
这五个字节很乱,放在一块叫记录头信息00 00 10 00 24,其表示删除状态,记录类型,下一条记录的相对位置等。

这一大坨先放一放,因为涉及到好多额外的知识。继续往下看。| first field of data |

...
| last field of data |
剩下全都是具体的列数据了,从第一列到最后一列。第一列是 ID 列,是 INT 类型的 1,占四个字节 80 00 00 01。开头的 80 是因为,正数要以 1 开头,这是 mysql 规定的,0x80 的二进制就是 1000 0000,所以这也对上了。第二列是 name 列,是 "dibingfa" 这样一个 varchar 类型的字符串。可是与后面怎么也对应不上,这是咋回事呢?还记不记得,mysql 每行记录会有几个隐藏列,rowid,事务 ID,回滚指针?没错,就是他们。其中,因为有主键,所以 rowid 就不存在了,也可以说第一列要么是 mysql 为我们生成的 6 字节的 rowid,要么是用户定义的主键或其他 Unique 键,优先以用户定义的键为准。下面我们一块看一下这五个列。(三个隐藏列,两个我们定义的列)主键 ID:80 00 00 01
事务 ID:00 00 00 00 0A 07
回滚指针:A7 00 00 01 1B 01 10
name 列(dibingfa):64 69 62 69 6E 67 66 61
age 列(2):80 00 00 02
其中 age 列同刚刚说的一样,mysql 会为正数的前面,加一个 1,所以 age 为 2,在磁盘上存储的就是 80 00 00 02事务 ID 和回滚指针就涉及到事务、隔离级别和 MVCC 这一大坨八股文的知识点,这里不做展开。
行记录格式整体结构

总结下,整个一行记录的格式,叫做 mysql 的行记录格式ROW_FORMAT这个 ROW_FORMAT 可以有不同的值,代表存储这一行记录的不同数据结构,其枚举记录在 remOtypes.h 文件中。/** Innodb row types are a subset of the MySQL global enum row_type.
They are made into their own enum so that switch statements can account
for each of them. */

enum rec_format_enum {
REC_FORMAT_REDUNDANT = 0, /*!< REDUNDANT row format */
REC_FORMAT_COMPACT = 1, /*!< COMPACT row format */
REC_FORMAT_COMPRESSED = 2, /*!< COMPRESSED row format */
REC_FORMAT_DYNAMIC = 3 /*!< DYNAMIC row format */
};
我电脑上用的是 mysql 5.7,其默认的行记录格式是 DYNAMIC,这个在源码中也可以找到答案,在 ha_innodb.cc 中。static ulong innodb_default_row_format = DEFAULT_ROW_FORMAT_DYNAMIC; 当然,可以用如下命令查询你的行格式。show table status from flash like 'student';所以我们今天以上讲述的格式,都是 DYNAMIC 格式的结构,总结起来如下:
记录源信息变长字段列表

NULL 值列表
记录头信息

具体记录的各列信息rowid 或 主键(隐藏列)
事务 ID隐藏列
回滚指针隐藏列列 1
列 2...列 n刚刚那七条记录,整体分析下,就如下图。

然后多个行,一次紧密地排列,通过记录头中的下一条记录的相对位置指针信息,可以快速找到下一条记录的起始位置。再宏观一点看,整个 ibd 文件,划分了很多个块,每个块 16 KB,我们这几行记录信息,在第四个块的某个区域内。

具体为什么是这个区域呢?因为这里是用户记录部分,前面还有文件头、页面头等信息,共占用 120 个字节,咱们今天讲的行记录部分,就从第 121 个字节开始。慢慢的,我再和大家一起把其他部分搞清楚,那 mysql 这块我们就从最原始的磁盘数据入手,将 data 文件夹下的所有文件都搞清楚了。
最原始的数据都搞清楚了,原理还担心么?
再聊几句

其实,不要被行记录格式这种名词吓到,它只是个协议规定罢了。就是 mysql 规定了一种将一行记录存储在磁盘中的格式,以便于 mysql 自己的程序可以根据这个结构认识这一行记录。
所以这种协议,首先要满足让 mysql 知道全部想知道的信息,比如 mysql 现在能仅仅通过 ibd 文件里的这些二进制数,知道每个字段的值都是什么吗?不能,因为它不知道表结构是什么样子,也就没法知道两个字段值之间的界限在哪里。
所以不难想到,它一定利用了 frm 文件中存储的表结构信息。
其次,要让 mysql 在知道这些信息的同时,还能更方便地利用这个结构,占用更少的存储空间,以及提升程序的便利性
拿占用更少存储空间这块来讲,NULL 值完全可以当做普通列,也存储在后面,然后规定一个 NULL 值的二进制标识符即可。但 DYNAMIC 行记录格式规定前面放一个 NULL 值列表的结构,并且仅仅用 1 位来表示一个 NULL 值记录,这样就极大节省了空间再说便利性这块,上面说了变长字段长度列表和 NULL 值列表,都是逆序存储的,看似很别扭,其实就是为了程序的便利性,这里留给大家自己探索吧。顺便提一下,记得很久之前还是用 sqlyog 去连 mysql 的,其实 mysql 下载并安装后,自带的 workbench 就很好用了。

所以非常方便本机做实验。大家方便的时候也自己玩一玩哦,我们下期见。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

TWSC 2985系列SD6.0存储芯片 国内首颗支持4K LDPC纠错技术 增强纠错、耐久可靠、性能升级

关键字: 德明利 半导体 存储 芯片 国产存储企业

为了满足日益增长的数据处理需求,铁威马NAS推出了全新的性能巅峰2024年旗舰之作F4-424 Pro,并搭载了最新的操作系统--TOS 6。这款高效办公神器的问世,无疑将为企业和专业人士带来前所未有的便捷与效率。

关键字: 存储 Linux 服务器

今日凌晨,中国台湾东部的花莲县连续发生地震,最高强度为6.3级,震源深度10公里,据中国地震台网分析,本次地震均为4月3日台湾花莲县海域发生的7.3级地震的余震。中国台湾地区在全球半导体供应链中扮演者重要角色,其10nm...

关键字: 固态硬盘 芯片 存储

最近总是出现iPhone16和ipad的概念新闻,以前为了跟风也会更换手机,后来是因为职业原因拍摄大量视频照片占据手机空间而不得已换了一部又一部手机。但慢慢地我发现,想要查找几年前的视频总得翻找之前的手机,流程繁杂又拖沓...

关键字: NAS 互联网 存储

4月12日消息,西部数据展出了业内首款4TB SD卡——闪迪Extreme Pro SDUC 4TB存储卡。

关键字: 西部数据 存储

5G、人工智能、物联网、云计算技术快速升级,存储容量和性能需求也快速增长,全球数据量也进而呈现出爆炸式提升的状态。在海量应用场景下依靠单一的存储方式已经无法满足大规模非结构数据,如何在可控成本内高效、弹性地应对每一次数据...

关键字: SSD 存储 物联网

北京——2024年4月9日 越来越多的企业将关键性的工作负载放到云上,如何确保云上业务的连续性即云的韧性对企业来说就越来越重要。在亚马逊云科技,我们从一开始就在基础设施、服务设计与部署、运营模式和机制中将韧性考虑其中。例...

关键字: 服务器 存储 数据中心

RS触发器是一种常见的数字电路元件,通常用于存储和传输二进制信息。它可以采用不同的结构和实现方法,但无论采用何种方式,RS触发器都有一些约束条件,以确保其正常工作和可靠性。

关键字: rs触发器 存储 传输

美光坚持多元、平等、包容的企业文化,携手社区推行公益

关键字: 内存 存储 美光

e络盟现可向全球客户供应Alliance Memory产品

关键字: 存储 eMMC 闪存IC
关闭
关闭