当前位置:首页 > 公众号精选 > 程序员小灰
[导读]mysql的一行记录,最终肯定是存储在磁盘上,也就是肉眼可见的文件上,今天我们的目标很简单,就是看看它到底是怎么存的。当然还有一个更重要的目标,就是以这个为引子,带大家完全通过一手资料,来揭秘这个问题的答案。所以大家阅读时,不要完全奔着寻找这个答案去,如果很少通过一手资料去研究问...

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 set1 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(10NOT 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 就很好用了。
所以非常方便本机做实验。大家方便的时候也自己玩一玩哦,我们下期见。

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

来源:https://juejin.cn/post/6860252224930070536| 背景日志是 mysql 数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查...

关键字: mysql

跟着官方文档 源码 源文件三件套自己探索

关键字: mysql 存储

什么Mysql的事务?事务的四大特性?事务带来的什么问题?你详细了解过MVCC吗?它是怎么工作的?

关键字: mysql 嵌入式

想进大厂,mysql不会那可不行,来接受mysql面试挑战吧,看看你能坚持到哪里?

关键字: mysql 嵌入式

最近,在开发一个分库分表中间件,由于功能需求,需要分析MySQL协议,发现网上对于MySQL协议分析的文章大部分都过时了,原因是分析的MySQL版本太低了。怎么办?于是乎,我便硬着头皮开始啃MySQL源码,经过两个多月的...

关键字: mysql 嵌入式

想进大厂,Mysql不会那可不行,来接受Mysql面试挑战吧,看看你能坚持到哪里?

关键字: mysql 嵌入式

Explain被称为执行计划,在语句之前增加explain关键字,MySQL会在查询上设置一个标记,模拟MySQL优化器来执行SQL语句,执行查询时,会返回执行计划的信息,并不执行这条SQL。

关键字: mysql 嵌入式

什么是事务?事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。

关键字: mysql 嵌入式

此文不会事无巨细的从select的用法开始讲解mysql,主要针对的是开发人员需要知道的一些MySQL的知识点,主要包括索引、事务、优化等方面,以在面试中高频的问句形式给出答案。

关键字: mysql 嵌入式

MySQL死锁异常是我们经常会遇到的线上异常类别,一旦线上业务日间复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常。这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,并且分析解决时还需要了解MySQL...

关键字: mysql 嵌入式
关闭
关闭