- 数据结构
- 体系结构
- 双写机制
- Buffer Pool
数据结构
- InnoDB 的三大特性:双写缓冲区/双写机制;Buffer Pool;自适应 Hash 索引
- 行格式
- 可以在创建或修改表的语句中指定行格式:CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
- COMPACT
- delete_mask 1 标记该记录是否被删除
- min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
- n_owned 4 表示当前记录拥有的记录数
- heap_no 13 表示当前记录在页的位置信息
- record_type 3 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶子节点记录,2 表示最小记录,3 表示最大记录
- next_record 16 表示下一条记录的相对位置
- DB_ROW_ID(row_id):非必须,6 字节,表示行ID,唯一标识一条记录
- DB_TRX_ID:必须,6 字节,表示事务ID
- DB_ROLL_PTR:必须,7 字节,表示回滚指针
- InnoDB 表对主键的生成策略是:
- 优先使用用户自定义主键作为主键
- 如果用户没有定义主键,则选取一个Unique 键作为主键
- 如果表中连Unique 键都没有定义的话,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键
- Redundant MySQL5.0 之前用的一种行格式
- Dynamic & Compressed
- MySQL5.7 的默认行格式就是Dynamic
- Dynamic 和Compressed 行格式和Compact 行格式挺像,只不过在处理行溢出数据时有所不同
- 数据溢出:一个页存放不了一条记录的情况
- Dynamic 和Compressed 行格式,不会在记录的真实数据处存储字段真实数据,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址
- Compressed 行格式和 Dynamic 不同的一点是:Compressed 行格式会采用压缩算法对页面进行压缩,以节省空间
- 索引页格式:一个页的大小一般是16KB
-
- File Header 文件头部, 38 字节, 页的一些通用信息
- Page Header 页面头部, 56 字节, 数据页专有的一些信息
- Infimum + Supremum 最小记录和最大记录, 26 字节, 两个虚拟的行记录
- User Records 用户记录, 大小不确定, 实际存储的行记录内容
- Free Space 空闲空间, 大小不确定, 页中尚未使用的空间
- Page Directory 页面目录, 大小不确定, 页中的某些记录的相对位置
- File Trailer 文件尾部, 8 字节, 校验页是否完整
- 生成页的时候,其实并没有User Records 这个部分,每当插入一条记录,都会从Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records 部分
- 按照主键从小到大的顺序形成了一个单链表,记录被删除,则从这个链表上摘除
- Page Directory 主要是解决记录链表的查找问题:为页中的记录再制作了一个目录
-
- 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组
- 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记录
- 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成 的
- 每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间
- 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录
- 通过记录的next_record 属性遍历该槽所在的组中的各个记录
-
- Page Header:一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等
- File Header:针对各种类型的页都通用,比如页的类型,这个页的编号是多少,它的上一个页、下一个页是谁,页的校验和等
- File Trailer:为了检测一个页是否完整
- 前4 个字节代表页的校验和,这个部分是和File Header 中的校验和相对应的
- 后4 个字节代表页面被最后修改时对应的日志序列位置(LSN),这个也和校验页的完整性有关
-
体系结构
- InnoDB 的体系结构
-
- Insert/Change Buffer 主要是用于对二级索引的写入优化
- Undo 空间则是undo 日志一般放在系统表空间,但是通过参数配置后,也可以用独立表空间存放
- 通用表空间和独立表空间不同,通用表空间是允许多个表存储数据的共享表空间。
-
- InnoDB 的表空间:任何类型的页都有专门的地方保存页属于哪个表空间,同时表空间中的每一个页都对应着一个页号,这个页号由4 个字节组成,也就是32 个比特位,所以一个表空间最多可以拥有232 个页,如果按照页的默认大小16KB 来算,一个表空间最多支持64TB 的数据
- 独立表空间结构
- 区(extent):连续的64 个页就是一个区,也就是说一个区默认占用1MB 空间大小
- 如果是以页为单位来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远
- 可以消除很多的随机I/O
- 组:每256个区又被划分成一个组
- 第一个组最开始的3 个页面的类型是固定的:用来登记整个表空间的一些整体属性以及本组所有的区被称为FSP_HDR,也就是extent 0 ~ extent 255 这256个区,整个表空间只有一个FSP_HDR
- 其余各组最开始的2 个页面的类型是固定的,一个XDES 类型,用来登记本组256 个区的属性,FSP_HDR 类型的页面其实和XDES 类型的页面的作用类似,只不过FSP_HDR 类型的页面还会额外存储一些表空间的属性
- 段(segment):存放叶子节点的区的集合就算是一个段,存放非叶子节点的区的集合也算是一个段
- 也就是说一个索引会生成2 个段,一个叶子节点段,一个非叶子节点段
- 段其实不对应表空间中某一个连续的物理区域,而是一个逻辑上的概念
- 区(extent):连续的64 个页就是一个区,也就是说一个区默认占用1MB 空间大小
- 独立表空间结构
- 系统表空间结构:系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL 进程只有一个系统表空间
- InnoDB 数据字典 (Data Dictionary Header)
- SYS_TABLES 整个InnoDB 存储引擎中所有的表的信息
- SYS_COLUMNS 整个InnoDB 存储引擎中所有的列的信息
- SYS_INDEXES 整个InnoDB 存储引擎中所有的索引的信息
- SYS_FIELDS 整个InnoDB 存储引擎中所有的索引对应的列的信息
- SYS_FOREIGN 整个InnoDB 存储引擎中所有的外键的信息
- SYS_FOREIGN_COLS 整个InnoDB 存储引擎中所有的外键对应列的信息
- SYS_TABLESPACES 整个InnoDB 存储引擎中所有的表空间信息
- SYS_DATAFILES 整个InnoDB 存储引擎中所有的表空间对应文件系统的文件路径信息
- SYS_VIRTUAL 整个InnoDB 存储引擎中所有的虚拟生成列的信息
- 这些表的元数据,用一个固定的页面来记录
- 这个页面就是页号为7 的页面Data Dictionary Header,类型为SYS,记录了数据字典的头部信息
- 整个InnoDB 存储引擎的一些全局属性,比如Row ID:Max Row ID 字段是全局共享的
- 用户是不能直接访问InnoDB 的这些内部系统表的,在information_schema 数据库中的这些以INNODB_SYS 开头的表并不是真正的内部系统表(内部系统表是以SYS 开头的那些表),而是在存储引擎启动时读取这些以SYS 开头的系统表,然后填充到这些以INNODB_SYS 开头的表中
双写机制
- 双写缓冲区/双写机制:一种特殊文件flush 技术,大小是2MB,性能降低了大概5-10%左右
- 在把页写到数据文件之前,InnoDB 先把它们写到一个叫doublewrite buffer(双写缓冲区)的连续区域内,在写doublewrite buffer 完成后,InnoDB 才会把页写到数据文件的适当的位置
- 如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer 中找到完好的page 副本用于恢复
- 为了解决部分页写入问题,当MySQL 将脏数据flush到数据文件的时候, 先使用memcopy 将脏数据复制到内存中的一个区域(也是2M),之后通过这个内存区域再分2 次,每次写入1MB 到系统表空间,然后马上调用fsync 函数,同步到磁盘上。在这个过程中是顺序写,开销并不大,在完成doublewrite 写入后,再将数据写入各数据文件文件,这时是离散写入
Buffer Pool
- Buffer Pool:默认 128M
- 一般分配机器内存的 70%~75%(分配的总空间比指定的缓冲池大小大约大 10%)
show engine innodb status
对于读取多的情况,如果没达到98%以上,都说明buffer 不够
- 缓存页
- 一页 16KB
- 每个缓存页对应的控制信息占用的内存大小是相同的,我们称为控制块
- 控制块和缓存页是一一对应的
- 每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小
- free 链表
- 可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作free 链表(或者说空闲链表)。刚刚完成初始化的Buffer Pool 中所有的缓存页都是空闲的
- 每当需要从磁盘中加载一个页到Buffer Pool 中时就从free 链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了
- flush 链表:再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中
- 缓存页的哈希处理:用表空间号 + 页号作为 key,缓存页作为 value 创建一个哈希表
- 划分区域的LRU 链表
- 预读
- 线性预读
innodb_read_ahead_threshold
(默认56):如果顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool 的请求 - 随机预读
innodb_random_read_ahead
(长期有效性很低,默认关闭):如果Buffer Pool 中已经缓存了某个区的13 个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其他的页面到Buffer Pool 的请求
- 线性预读
- 分区:按照某个比例将LRU 链表分成两半
innodb_old_blocks_pct
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young 区域
- 只有被访问的缓存页位于young 区域的1/4 的后边,才会被移动到LRU 链表头部(在young 区域的缓存页都是热点数据)
- 一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old 区域
- 当磁盘上的某个页面在初次加载到 Buffer Pool 中的某个缓存页时,该缓存页对应的控制块会被放到 old 区域的头部
innodb_old_blocks_time
:在对某个处在old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old 区域移动到young 区域的头部
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young 区域
- 预读
- 刷新脏页到磁盘:后台有专门的线程每隔一段时间负责把脏页刷新到磁盘
- 从 LRU 链表的冷数据中刷新一部分页面到磁盘(innodb_lru_scan_depth):后台线程会定时从LRU 链表尾部开始扫描一些页面
- 从 flush 链表中刷新一部分页面到磁盘
- 用户线程没有可用的缓存页时,会尝试看看LRU 链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将 LRU 链表尾部的一个脏页同步刷新到磁盘
- 系统特别繁忙时,也可能出现用户线程批量的从flush 链表中刷新脏页的情况
- 多个 Buffer Pool 实例(
innodb_buffer_pool_instances
):在多线程环境下,访问 Buffer Pool 中的各种链表都需要加锁处理- innodb_buffer_pool_instances 能设置的最大值是64
- 当innodb_buffer_pool_size(默认128M)的值小于1G 的时候设置多个实例是无效的
- 让每个 Buffer Pool 实例达到 1 个G
- chunk 的大小
innodb_buffer_pool_chunk_size
只能在服务器启动时指定- 一个 Buffer Pool 实例其实是由若干个 chunk 组成的
- 在服务器运行期间调整Buffer Pool的大小时就是以chunk 为单位增加或者删除内存空间
- Buffer Pool 的缓存页除了用来缓存磁盘上的页面以外,还可以存储锁信息、自适应哈希索引等信息
- 查看 Buffer Pool 的状态信息
SHOW ENGINE INNODB STATUS