降妖除魔 | mysql 的一行记录是怎么存储的?
The following article is from 低并发编程 Author 闪客
作者 | 闪客sun
出品 | 公众号:低并发编程(ID:dibingfa)
https://dev.mysql.com3. 准备好 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 文件,我们继续!
第二步:创建表
第二步:创建表
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,坏了,乱码了。第三步:插入数据
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');
| 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 个字节,对上了。那如果是多个,很显然,就这样存。| 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 这一大坨八股文的知识点,这里不做展开。行记录格式整体结构
/** 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 值列表
记录头信息
事务 ID(隐藏列)
回滚指针(隐藏列)列 1
列 2...列 n刚刚那七条记录,整体分析下,就如下图。
最原始的数据都搞清楚了,原理还担心么?
再聊几句
所以这种协议,首先要满足让 mysql 知道全部想知道的信息,比如 mysql 现在能仅仅通过 ibd 文件里的这些二进制数,知道每个字段的值都是什么吗?不能,因为它不知道表结构是什么样子,也就没法知道两个字段值之间的界限在哪里。
所以不难想到,它一定利用了 frm 文件中存储的表结构信息。
其次,要让 mysql 在知道这些信息的同时,还能更方便地利用这个结构,占用更少的存储空间,以及提升程序的便利性。
拿占用更少存储空间这块来讲,NULL 值完全可以当做普通列,也存储在后面,然后规定一个 NULL 值的二进制标识符即可。但 DYNAMIC 行记录格式规定前面放一个 NULL 值列表的结构,并且仅仅用 1 位来表示一个 NULL 值记录,这样就极大节省了空间。再说便利性这块,上面说了变长字段长度列表和 NULL 值列表,都是逆序存储的,看似很别扭,其实就是为了程序的便利性,这里留给大家自己探索吧。
2、33张图剖析ReentrantReadWriteLock源码
点分享
点点赞
点在看