数据库系统核心就是存储引擎,前面的文章我写过日志类型的LSM-Tree存储引擎,本篇文章将介绍基于B+树的 InnoDB 存储引擎实现细节,存储引擎一般都是有不同专业领域人员开发的,目前在MySQL5.7之后使用的默认存储引擎就为 InnoDB ,下面为笔者整理的 InnoDB 引擎相关的实现细节笔记总结。
配置设计
MySQL 是如何对配置选项进行设计的?设置这个可能大部分软件都有手机上大部分是 GUI 图形化设置界面。而大部分的基础软件或者运行在 Linux 平台上的软件都是通过命令行或者指定的配置文件进行设置的,MySQL 作为一个数据库基础软件也提供很多种配置参数的方式,分别为命令行参数、环境变量、配置文件。
权限最高的就是命令行方式,如果在启动数据库的时候再命令行上传入参数那么可以覆盖其他方式配置的环境参数,例如下面的:
mysqld --skip-networking
# 等价于
mysqld --skip_networking
上面这种方式通过做参数标记,这种没有具体的参数值,而另外一种命令行的方式就是有默认值可以通过命令行修改其值的,方式如下:
# 修改默认的存储引擎
mysqld --default-storage-engine=MyISAM
如果是传入参数值时不容许有空格必须用等号划分,多个参数值可以通过空格分割传递起来;上面这些都为长参数配置选项,还有一种就是 MySQL 客户端所使用的默认短参数,主要短参数是区分大小写的,例如 -p
代表密码,而 -P
表示的是端口号,如下图:
mysql -h127.0.01 -uroot -p
Enter password:
上面的方式每次都要通过命令行参数传入其配置参数值,如果要配置很多的话需要写多个参数集合,这使得很多的参数需要一起写到命令行中并且每次都要这么传入,非常麻烦,但是命令行在所有配置方式中命令行上传入的参数优先级最高。另外一种方式是通过配置文件的方式设置其参数值,采用的是 ini
的配置文件格式,例如上面的命令行方式配置的参数可以编写配置文件的格式:
[client]
port=3306
[mysql]
default-character-set=utf-8
[mysqld]
# mysql服务端默认监听(listen on)的TCP/IP端口
port=3306
# mysql数据库文件所在目录
datadir="xxxx/Data"
# 服务端使用的字符集,默认为Latin,安装时一般选择utf-8
character-set-server=utf-8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
像上面这种方式称之为配置组的方式,每个组的都有自己对应的配置值,而组名大部分是对应的bin
下面的可执行二进制名称,同一个配置文件中的配置出现多次,将以出现在最后一组的参数值为准;如果是命令行传入参数那么最终使用的是命令行参数,命令行参数级别最高;当然部分配置可以在使用 DDL 语言进行修改操作,例如数据库的字符集和表使用的默认存储引擎。
MySQL的可配置信息远不止这么多,这些配置参数一部分是系统变量的成员,可以使用 SHOW VARIABLES
来查看相关的系统变量信息,设置系统环境变量的方式就是上面我所整理的命令行的方式和配置文件的方式,当数据库启动的时候这些被设置的参数就会被设置为其对应的值,如果没有配置则有默认的值。
对应某些环境变量在程序运行的是无法动态修改的,且部分变量例如存储引擎都有各自作用域,例如字符串和字符串排序规则有数据级别、表级别、行级别,这时设计是因为可以合理的使用存储空间提高数据库性能。另外一些变量分为 GLOBAL 和 SESSION 级别的变量,服务器启动的时候会为分配初始化一些全局的变量,而会话级别则是针对于当前连接到服务器的某个客户端的环境变量设置,例如 max_connections
为全局变量,而 insert_id
客户端表示自增 ID 的初始化启始值,还有 default_storage_engine
则为服务器端和会话端共有的值。
存储单位
在 InnoDB 中数据表中的每条数据记录采用页存储,以页作为存储磁盘上的基本单位,默认的页大小为 16KB
,如果需要处理数据记录也就是要把页从磁盘中加载到内存中再操作,一次性批量操作 16KB 大小的页。当然这个页面大小页可以通过 innodb_page_size
系统环境来设置,当数据库启动之后这个值就不能被再次修改,运行过程中是不可变的系统变量。InnoDB 一些设计细节运用一些巧妙的设计来提高空间利用率,官方将存储数据的记录的页称之为 INDEX PAGE ,下面就为页布局图:
这里主要关注的是 User Records 部分,数据库里面的记录都是存储在 User Records 里面,而 User Records 是从Free Space 里面申请的。Free Space 会在页面初始化时创建完成其实并没有 User Records 部分,当数据库开始工作的时候会从 Free Space 申请,当把整个 Free Space 使用完成之后等于整个页面被使用完,当使用完成之后就会申请新的页面。
数据记录
平时我们使用 INSERT
语句传入的数据记录在 InnoDB 底层存储也是一条数据记录,也称之为行格式, InnoDB 的作者在设计的时候提供了4种数据记录存储格式分别为:
- Compact
- Redundant
- Dynamic
- Compressed
其中 Redundant 是以前使用的旧格式,为了兼容性还一直保留;自 MySQL 5.1 开始,默认的行记录格式为 Compact,而从 MySQL 5.7 开始,默认的行记录格式为 Dynamic,使用 SHOW TABLE STATUS LIKE 'table_name'
可以查看指定表的行格式。
目前最常用的还是 Compact 和 Dynamic 的方式进行存储的,这取决于所使用的数据库版本。在创建表或者创建列记录时可以使用 ROW_FORMAT=COMPACT
关键字来指定对应格式,如果默认没有指定存储格式会使用 Compact 格式,Compact 格式如下图:
页中的数据是按行进行存放的,每页中存放的行记录最少为 2 行,最多为 16 KB / 2 - 200 行,也就是 7992 行。所谓的行就是上面的 Compact 数据格式,Compact 行记录格式是在 MySQL 5.0 引入的,设计目标是高效的存储数据,一个页中存放的行数据越多,其性能就越高。
变长字段长度列表里有 3 个重要的参数为 M、W、L ,这3个参数分别代表着:M 最大占用字符数, W 一个字符集里面一个字符占用的字节数,L 可变长字段实际存储的数据字节长度。
变长字段长度列表是存储一些不定长的字符串类型,例如 VARCHAR(M)、VARBINARY(M) 各种 TEXT 类型,例如 BLOB 类型的二进制格式,因为是这些类型不定长所以在存储记录会将其字节数也存起来。其中的 M 表示某一个字符集表示一个字符最多需要的字节数,例如 UTF-8mb3 中 W 就等于 3,例如 VARCHAR(M) 来说 M 则表示最多能存储 M 个字符,可以通过 SHOW CHARSET
命令查看对应的 Maxlen 的值;如果变长字节数占用的比较多,可能就要使用 2 个字节来描述其占用大小,至于用多少有一套默认的算法:
- 如果 M * W <= 255 ,那么使用 1 个字节来表示长度
- 如果 M * W > 255,并且 L <= 127,那么使用 1 个字节来表示长度
- 如果 M * W > 255,并且 L > 127,那么使用 2 个字节来表示长度
在 Compact 格式下的变长字段只允许存储非 NULL 的列的值,如果一个列中有 NULL 值都会放到一个统一的地方管理,首先会统计行记录中允许存储为 NULL 的列表有哪些(主键列以及使用 NOT NULL 修饰的列都不满足条件),则其他满足 NULL 条件的列被映射成一个二进制位图,如果二进制位上为 1 时代表该列的值为 NULL,存放顺序为小端逆序排列。
其记录头信息都是一些特殊的标记二进制位,这里不经常使用并且维护者是存储引擎自己,主要看列值存储部分,且列值的头3个值为每条记录一些附加信息,分别为 ROW_ID、TRX_ID、ROLL_POINTER,这3个字段是存储引擎自动生成的,ROW_ID 为一个隐藏的主键,如果在定义列的时候没有声明主键或者 UNIQUE 键就会默认添加一个 ROW_ID作为主键,而 TRX_ID 为事务处理的唯一标识,ROLL_POINTER 则为回滚的指针。
Redundant 格式是MySQL 5.0之前使用的格式,其最大差异就是没有变长字段长度,这就意味着会通过占用字节数来通过偏移量访问列值,主要的特点就是通过两个相邻的偏移量的差值来计算各个列的长度;也没有 NULL 专属列,而是将每个列的第一个二进制位来判断,如果是 1 则该列为空,如果真是数据长度大于 127 就采用 2 个字节来表示一个列对应的偏移量。
Dynamic 和 Compressed 行格式是在MySQL 5.7之后所使用的行格式,和 Compact 格式很相近,只是在出来溢出数据记录的时候方式不一样。Compact 和 Redundant 在处理溢出数据记录的时候就会有所差异,例如如果定义一个列为 VARCHAR(65532) 此时就会发数据记录溢出情况,上面说过 InnoDB 采用的页存储数据记录一个页大小为 16KB 也就是 16384 字节,而 65532 字节显然超过这个范围就会发生溢出情况。在 Compact 和 Redundant 中如果发生这种情况会,就会只存储 768 字节的数据,而另外 20 字节存储指向其他页的地址信息。Dynamic 和 Compressed 行格式不会存储 768 字节的数据,而是直接将其溢出的数据存储在多个溢出页中,方便通过压缩算法压缩空间。