MySql 笔记(五)InnoDB引擎页分裂与页合并的原理

  • 一、文件表(File-Table)结构
  • 二、根,分支与叶子
  • 三、页的内部原理
  • 四、页合并的原理
  • 五、页分裂的原理
  • 六、主键的使用
  • 七、总结
  • 八、致谢

这篇文章是在知乎上见过的关于页分裂和页合并原理的很好的文章,有助于帮助我们了解MySQL及InnoDB的底层实现原理,搬运过来记录一下,也以便于以后的复习和回顾。

原文标题:InnoDB Page Merging and Page Splitting
原文链接:https://www.percona.com/blog/2017/04/10/innodb-page-merging-and-page-splitting/
翻译地址:https://blog.2014bduck.com/archives/260


如果你找过任何一位MySQL顾问,问他对你的语句和/或数据库设计的建议,我保证他会跟你讲主键设计的重要性。特别是在使用InnoDB引擎的情景,他们肯定会给你解释索引合并和页分裂这些。这两个方面与性能息息相关,你应该在任何设计索引(不止是主键索引)的时候都将他们考虑在内。

你可能觉得这些听起来挺莫名其妙,没准你也没错。这不是容易的事,特别是讲到关于内部实现的时候。通常你都不会需要处理这些事情,并且你也不想去着手他们。

但是有时候这些问题又是必须搞清楚的。如果有这种情况,那这篇文章正适合你。

我尝试用这篇文章将一些最不清晰、InnoDB内部的操作解释清楚:索引页的创建、页合并和页分裂。

在InnoDB中,数据即索引(译注:索引组织数据)。你可能听过这种说法,但它具体是什么样的?

一、文件表(File-Table)结构

假设你已经装好了MySQL最新的5.7版本(译注:文章发布于17年4月),并且你创建了一个windmills库(schema)和wmills表。在文件目录(通常是/var/lib/mysql/)你会看到以下内容:

data/windmills/wmills.ibdwmills.frm

这是因为从MySQL 5.6版本开始innodb_file_per_table参数默认设置为1。该配置下你的每一个表都会单独作为一个文件存储(如果有分区也可能有多个文件)。

目录下要注意的是这个叫wmills.ibd的文件。这个文件由多个段(segments)组成,每个段和一个索引相关。

文件的结构是不会随着数据行的删除而变化的,但段则会跟着构成它的更小一级单位——区的变化而变化。区仅存在于段内,并且每个区都是固定的1MB大小(页体积默认的情况下)。页则是区的下一级构成单位,默认体积为16KB。

按这样算,一个区可以容纳最多64个页,一个页可以容纳2-N个行。行的数量取决于它的大小,由你的表结构定义。InnoDB要求页至少要有两个行,因此可以算出行的大小最多为8000 bytes。

听起来就像俄罗斯娃娃(Matryoshka dolls)一样是么,没错!下面这张图能帮助你理解:

二、根,分支与叶子

每个页(逻辑上讲即叶子节点)是包含了2-N行数据,根据主键排列。树有着特殊的页区管理不同的分支,即内部节点(INodes)。

上图仅为示例,后文才是真实的结构描述。

具体来看一下:

ROOT NODE #3: 4 records, 68 bytesNODE POINTER RECORD ≥ (id=2) → #197INTERNAL NODE #197: 464 records, 7888 bytesNODE POINTER RECORD ≥ (id=2) → #5LEAF NODE #5: 57 records, 7524 bytesRECORD: (id=2) → (uuid="884e471c-0e82-11e7-8bf6-08002734ed50", millid=139, kwatts_s=1956, date="2017-05-01", location="For beauty's pattern to succeeding men.Yet do thy", active=1, time="2017-03-21 22:05:45", strrecordtype="Wit")

下面是表结构:

CREATE TABLE `wmills` (`id` bigint(11) NOT NULL AUTO_INCREMENT,`uuid` char(36) COLLATE utf8_bin NOT NULL,`millid` smallint(6) NOT NULL,`kwatts_s` int(11) NOT NULL,`date` date NOT NULL,`location` varchar(50) COLLATE utf8_bin DEFAULT NULL,`active` tinyint(2) NOT NULL DEFAULT '1',`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`strrecordtype` char(3) COLLATE utf8_bin NOT NULL,PRIMARY KEY (`id`),KEY `IDX_millid` (`millid`)
) ENGINE=InnoDB;

所有的B树都有着一个入口,也就是根节点,在上图中#3就是根节点。根节点(页)包含了如索引ID、INodes数量等信息。INode页包含了关于页本身的信息、值的范围等。最后还有叶子节点,也就是我们数据实际所在的位置。在示例中,我们可以看到叶子节点#5有57行记录,共7524 bytes。在这行信息后是具体的记录,可以看到数据行内容。

这里想引出的概念是当你使用InnoDB管理表和行,InnoDB会将他们会以分支、页和记录的形式组织起来。InnoDB不是按行的来操作的,它可操作的最小粒度是页,页加载进内存后才会通过扫描页来获取行/记录。

现在页的结构清楚了吗?好,我们继续。

三、页的内部原理

页可以空或者填充满(100%),行记录会按照主键顺序来排列。例如在使用AUTO_INCREMENT时,你会有顺序的ID 1、2、3、4等。

页还有另一个重要的属性:MERGE_THRESHOLD。该参数的默认值是50%页的大小,它在InnoDB的合并操作中扮演了很重要的角色。

当你插入数据时,如果数据(大小)能够放的进页中的话,那他们是按顺序将页填满的。

若当前页满,则下一行记录会被插入下一页(NEXT)中。

根据B树的特性,它可以自顶向下遍历,但也可以在各叶子节点水平遍历。因为每个叶子节点都有着一个指向包含下一条(顺序)记录的页的指针。

例如,页#5有指向页#6的指针,页#6有指向前一页(#5)的指针和后一页(#7)的指针。

这种机制下可以做到快速的顺序扫描(如范围扫描)。之前提到过,这就是当你基于自增主键进行插入的情况。但如果你不仅插入还进行删除呢?

四、页合并的原理

当你删了一行记录时,实际上记录并没有被物理删除,记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。

当页中删除的记录达到MERGE_THRESHOLD(默认页体积的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。

在示例中,页#6使用了不到一半的空间,页#5又有足够的删除数量,现在同样处于50%使用以下。从InnoDB的角度来看,它们能够进行合并。

合并操作使得页#5保留它之前的数据,并且容纳来自页#6的数据。页#6变成一个空页,可以接纳新数据。

如果我们在UPDATE操作中让页中数据体积达到类似的阈值点,InnoDB也会进行一样的操作。

规则就是:页合并发生在删除或更新操作中,关联到当前页的相邻页。如果页合并成功,在INFOMATION_SCHEMA.INNODB_METRICS中的index_page_merge_successful将会增加。

五、页分裂的原理

前面提到,页可能填充至100%,在页填满了之后,下一页会继续接管新的记录。但如果有下面这种情况呢?

页#10没有足够空间去容纳新(或更新)的记录。根据“下一页”的逻辑,记录应该由页#11负责。然而:

页#11也同样满了,数据也不可能不按顺序地插入。怎么办?

还记得之前说的链表吗(译注:指B+树的每一层都是双向链表)?页#10有指向页#9和页#11的指针。

InnoDB的做法是(简化版):

创建新页
判断当前页(页#10)可以从哪里进行分裂(记录行层面)
移动记录行
重新定义页之间的关系

新的页#12被创建:

页#11保持原样,只有页之间的关系发生了改变:

页#10相邻的前一页为页#9,后一页为页#12
页#12相邻的前一页为页#10,后一页为页#11
页#11相邻的前一页为页#10,后一页为页#13
(译注:页#13可能本来就有,这里意思为页#10与页#11之间插入了页#12)

这样B树水平方向的一致性仍然满足,因为满足原定的顺序排列逻辑。然而从物理存储上讲页是乱序的,而且大概率会落到不同的区。

规律总结:页分裂会发生在插入或更新,并且造成页的错位(dislocation,落入不同的区)

InnoDB用INFORMATION_SCHEMA.INNODB_METRICS表来跟踪页的分裂数。可以查看其中的index_page_splits和index_page_reorg_attempts/successful统计。

一旦创建分裂的页,唯一(译注:实则仍有其他方法,见下文)将原先顺序恢复的办法就是新分裂出来的页因为低于合并阈值(merge threshold)被删掉。这时候InnoDB用页合并将数据合并回来。

另一种方式就是用OPTIMIZE重新整理表。这可能是个很重量级和耗时的过程,但可能是唯一将大量分布在不同区的页理顺的方法。

另一方面,要记住在合并和分裂的过程,InnoDB会在索引树上加写锁(x-latch)。在操作频繁的系统中这可能会是个隐患。它可能会导致索引的锁争用(index latch contention)。如果表中没有合并和分裂(也就是写操作)的操作,称为“乐观”更新,只需要使用读锁(S)。带有合并也分裂操作则称为“悲观”更新,使用写锁(X)。

六、主键的使用

好的主键不仅对于数据查找很重要,而且也影响写操作时数据在区上的分布(也就是与页分裂和页合并操作相关)。

在第一个测试中我使用的是是自增主键,第二个测试主键是基于一个1-200的ID与自增值的,第三个测试也是1-200的ID不过与UUID联合。

插入操作时,InnoDB需要增加页,视为“分裂”操作:

表现因不同主键而异。

在头两种情况中数据的分布更为紧凑,也就是说他们拥有更好的空间利用率。对比半随机(semi-random)特性的UUID会导致明显的页稀疏分布(页数量更多,相关分裂操作更多)。

在页合并的情况中,尝试合并的次数因主键类型的不同而表现得更加不一致。

在插入-更新-删除操作中,自增主键有更少的合并尝试次数,成功比例比其他两种类型低9.45%。UUID型主键(图表的右一侧)有更多的合并尝试,但是合并成功率明显更高,达22.34%,因为数据稀疏分布让很多页都有部分空闲空间。

在辅助索引与上面主键索引相似的情况下,测试的表现也是类似的。

七、总结

MySQL/InnoDB不断地进行这些操作,你可能只能了解到很少的信息。但他们可能给你造成伤害,特别是比起用SSD,你还在用传统的机械存储(spindle storage)的时候(顺便提一下SSD会有另外的问题)。

坏消息就是我们用什么参数或者魔法去改变服务端。但好消息是我们可以在设计的时候做很多(有帮助)的事。

恰当地使用主键和设计辅助索引,并且记住不要滥用(索引)。如果你已经预计到会有很多插入/删除/更新操作,规划一个合适的时间窗来管理(整理)表。

有个很重要的点,InnoDB中你不会有断断续续的行记录,但是你会在页-区的维度上遇到这些问题。忽略表的管理工作会导致需要在IO层面、内存层面和InnoDB缓冲池层面做更多工作。

你必须不时(at regular intervals)重建一些表。可以采用一些技巧,比如分区和外部的工具(pt-osc)。不要让表变得过大和过于碎片化(fragmented)。

磁盘空间浪费?需要读多个表去获取需要的数据而不是一次搞定?每次搜索导致明显更多的读操作?那是你的锅,不要找借口!

祝大家都能愉快地使用MySQL !

八、致谢

Laurynas Biveinis: 感谢花时间向我解释一些内部实现。

Jeremy Cole: 感谢他的项目InnoDB_ruby (我经常用上)。

MySql 笔记(五)InnoDB引擎页分裂与页合并的原理相关推荐

  1. Mysql技术内幕innodb引擎笔记

    第2章 InnoDB存储引擎 2.3 InnoDB体系架构 InnoDB存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作: 维护所有进程/线程需要访问的多个内部数据结构. 缓 ...

  2. mysql分页tmp_count,MySQL MyISAM和InnoDB引擎的写入速度优化比较,分页速度优化

    以下的文章主要介绍的是MySQL MyISAM的引擎和InnoDB引擎的实际性能的比较,我们首先是通过MySQL数据库的表结构来出MySQL MyISAM的引擎和InnoDB引擎的实际性能的实际操作. ...

  3. MySQL数据库的InnoDB引擎TableSpaceExists问题解决

    文章中所有操作均是在 MySQL 5.7 版本下进行的 引入问题 MySQL 使用过程中如果出现如下报错: ERROR 1813 (HY000): Tablespace '`库名`.`表名`' exi ...

  4. MySQL探秘(五):InnoDB锁的类型和状态查询

    锁是数据库系统区分于文件系统的一个关键特性.数据库使用锁来支持对共享资源进行并发访问,提供数据的完整性和一致性.此外,数据库事务的隔离性也是通过锁实现的.InnoDB在此方面一直优于其他数据库引擎.I ...

  5. mysql引擎机制_关于MySQL中的InnoDB引擎的MVCC机制的理解

    目前在阅读<High Performance MysqL Second Edition>,读到Multiversion Concurrency Control章节时对InnoDB实现MVC ...

  6. MySQL笔记-解决InnoDB: Could not find a valid tablespace file for 'mysql/innodb_index_stats'

    这里主要是./mysql_install_db时会出现的. 如下的问题: 这里主要的问题是:my.cnf权限问题,这里文件的权限为777.这是不安全的. 主要是因为my.cnf中的[mysqld]中的 ...

  7. mysql innodb count_MySQL下INNODB引擎的SELECT COUNT(*)性能优化及思考

    正 文: MySQL下INNODB引擎的SELECT COUNT(*)性能优化及思考 最近有项目有高并发需求,服务器采用负载均衡,数据库采用阿里云的RDS MYSQL,16核64G内存,连接数:160 ...

  8. mysql 页分裂_InnoDB中的页合并与分裂

    如果你找过任何一位MySQL顾问,问他对你的语句和/或数据库设计的建议,我保证他会跟你讲主键设计的重要性.特别是在使用InnoDB引擎的情景,他们肯定会给你解释索引合并和页分裂这些.这两个方面与性能息 ...

  9. mysql 页分裂_mysql聚簇索引的页分裂原理实例分析

    本文实例讲述了mysql聚簇索引的页分裂.分享给大家供大家参考,具体如下: 在MySQL中,MyISAM采用的是非聚簇索引的,InnoDB存储引擎是采用聚簇索引的. 聚簇结构的特点: 根据主键查询条目 ...

最新文章

  1. 每天一个python 小案例——if else
  2. toj 2870 理解dijstra
  3. Hive:hive is not allowed to impersonate anonymous
  4. Java-WAS的Hello world
  5. ggplot2中显示坐标轴_ggplot2作图:修改图中一切文本的外观
  6. 马库斯:DeepMind新出的机器心智网络不错,但有误导性
  7. LintCode_514 Paint Fence
  8. 数据结构上机实践第九周项目1 - 二叉树算法库
  9. android 支付宝 6002,IOS支付宝支付出现6002问题的解决办法
  10. 从五方面探究智慧城市顶层设计失败原因
  11. python 分析qq聊天记录
  12. Android 读取bin文件方法
  13. 2012年8月编程语言排行榜
  14. 白糖详细 制造工艺、等级划分、国家标准号和注意事项
  15. 【“探索者”小车】机器人大赛“探索者”全地形小车参赛经验分享
  16. 九寨沟7.0级大地震,希望大家都能够平安
  17. 口红的html前端显示,初学者怎么涂口红自然
  18. MybatisPlus自定义SQL日志打印
  19. 第13天 面向对象
  20. 盘点那些必问的数据结构算法题之链表

热门文章

  1. 11.1 身在大学,不追浮云——《逆袭大学》连载
  2. 年度成绩大赏|2021,StreamNative 扬帆起航
  3. java中数组拼接成字符串中间用逗号隔开
  4. java连接mongo数据库,并执行db.runCommand命令
  5. 【stm32f4 C++与C混合开发】建立keil5的ARM Compiler 6(AC6)标准库开发工程
  6. 移动web JavaScript,事件(touchstart,touchmove,touchend)
  7. 神州优车上云之路:如何在效率、质量和成本三方面达到平衡?
  8. 使用计算机能佩戴老花镜,青岛版六年级科学第四单元检测题及答案
  9. 国庆星期日 1949年的国庆节(10月1日)是星期六。 今年(2012)的国庆节是星期一。
  10. 我应该拿什么来拯救你,我的游戏?