我敢说,在所有的技术栈里,存储技术算是最核心的一块内容,掌握存储技术的原理真的非常非常重要,无论是高级开发、资深开发,还是众人仰望的架构师,无一不重视这项技术的学习与沉淀。
而我们一般使用的MySQL关系型数据库,更是经典中的经典,虽说MySQL已经非常成熟,但对于MySQL的掌握程度,如果我们只停留在使用层面,不了解它的底层设计,那咱永远只能停留在写SQL上,成为一个彻头彻尾的CRUDBoy。
好了,稍微有点技术追求的你,绝不能错过这次MySQL系列精讲。
本文主讲MySQL的InnoDB存储引擎原理,在拆解的过程中,我不会过多强调概念性术语,咱不讲教科书,相反,它的作用与设计理念才是我们需要关注的地方。大家学习任何技术也都一样,想要好好掌握一门技艺,就要从设计者的角度去俯视它。
话不多说,我们先把MySQL的InnoDB存储引擎“扒光”,然后一件件帮它把“衣服”穿上。
在讲之前,我先抛几个核心问题:
MySQL的记录是怎么存储的?
页内记录到底是怎么维护的?
页内查询过程是怎样的?
来,我们带着问题出发——
MySQL管理数据的一个单位叫Page页面,数据都是存在页面里的。那咱们想要知道数据是怎么存,就需要了解页面长什么样子。
直接爆照:
我们来盘点一下:
页头(Page Header):存一些统计信息,记录页面的控制信息,共占56字节,包括页面空间使用情况、页的左右兄弟页面指针(这个就是双向链表,把左右兄弟页面的指针给拿到了)等。
虚记录:分为最大虚记录与最小虚记录,它俩把这页里面存储的数据的范围框住了。那怎么比较谁大谁小?用的是主键去比较:最大虚记录比页内最大主键还大,最小虚记录比页内最小主键还小。那主键到底是怎么存的呢?InnoDB用的是聚簇索引——数据和主键存到一起、数据和索引存到一起,数据按主键顺序存储。
记录堆:这部分就是存储记录的区域,分为有效记录和已删除记录。已被删除的记录构成一个链表,叫做自由空间链表,如图蓝色已经被删除的数据,用一个链表把它们连起来。
未分配空间:页面未使用的存储空间,除了用了一部分的橙色的区域和已删除的蓝色的数据,剩下的就是未分配空间了,后面有新的数据插入,往里放就行了。
Slog区:这一块对数据检索非常有用,卖个关子,后面详细说。
页尾(Page Tailer):页面的最后部分,占8个字节,主要存储页面的校验信息。这一页如果写坏了,数据不对了,通过校验位可以检查出来。
好了,到这里一个页面咱们了解了,了解数据大概是怎么分布的,那接下来需要考虑哪些点呢?我们接下来研究一下——页面记录是怎么维护的。
刚刚提到了主键顺序这个词,那这个顺序是怎么保证的?这里说的按什么顺序存储,不是说升序、降序这些,这些没有意义,实际上说的是在页里面数据是怎么组织起来的,还有就是插入数据的策略,我怎么插入数据,还有就是页内的查询是怎样的。
我们先来看看聚簇索引:
首先,聚簇索引是一棵B+树,那什么是聚簇呢?
图中下面绿色的部分是咱们的数据区域,数据是基于紫色部分的主键顺序去存储的,数据和主键存到一起没毛病,主键是按照树的顺序去组织的,这个结构就叫聚簇。
我们再看每一个Page,刚刚说到,Page里面有最大虚记录、最小虚记录,最大、最小这数据肯定是有个范围,那这个数据在里边到底是怎么存的?换个说法——数据的顺序是怎么保证的?到底是物理有序,还是逻辑有序?
我们再回顾一下大学的知识——物理有序写入不友好,查询友好;逻辑有序查询不友好,插入友好,两者优缺点互补。
再回到正题,了解了这两种不同存储方式的特性,反观页面是怎么做的。
先看下面这幅图,思考一下插入主键为10,9,8的数据,是按物理有序存储还是逻辑有序存储:
物理有序:一开始先插一个10,9要插进来先移动10;8再想插进来,9和10都要移动把8插进去(流鼻血.gif)
逻辑有序:插入10,再插入9,9的指针指向10,再插入8,8的指针指向9。
注意,这跟有没有索引没有关系,索引是基于它上层的数据结构。
那如果是我们自己去设计一个数据库的话,要怎么优化读写性能呢?
数据插入是写入IO,数据查询是读IO,不管是写还是读,在分析存储的时候,无非是这四种:顺序写、随机写、顺序读、随机读。如果是顺序写,数据会有各种移动,写入性能肯定非常糟糕。但是没办法,优化写入的手段十分有限,不过呢我们却有很多办法优化读。
所以想都不用想,页内数据存储的顺序就是逻辑有序。
重新梳理一下,Page与Page之间由双向链表连接,页内是用小的链表连起来的:
我们再来重新画一下这棵树:
这里要注意,每个Page的索引的每个节点,也就是树的每个节点,它也是一个Page。既然是个Page,也会有页头,也会有双向链表,如图蓝色与紫色相间那部分节点数据。
接下来咱分析一下它的插入策略。
蓝色部分已删除的空间(记录堆)怎么办呢?我们得想办法尽量把它们利用上,这个换谁做数据库设计都要这么设计。
其实,插入策略就是先使用自由空间链表,再使用未使用空间,把数据库“空洞”给补上。不过呢,自由空间链表的空间也不能完全利用上,比如旧的数据占25个字节,新的数据假设都只有20个字节,那剩下这5个字节基本也利用不上,这样一来就会产生越来越多的“碎片”。经过长时间的插入删除插入删除以后,我们就得考虑给数据库做一次收缩,比如通过两次主从表的双向同步,把所有表数据重新插一遍。
我们接下来研究一下——页内查询是怎么做的?
页内的数据是遍历还是二分查找?
无论数据是物理连续还是逻辑有序,都不能二分查找,都得用遍历的办法。如果我们设计一款数据库,通过索引找到数据在哪个Page里面,要是Page这一层通过遍历的方式,那效率实在是太低了,所以数据库肯定不能这样设计。
遍历不行,那就使用二分查找吧,提高一下效率。那MySQL是怎么做的呢?看看这张图:
如图,最小虚记录和最大虚记录之间形成一个链表,这时候Slot区就派上用场了,每个Slot槽指向链表中的某一个位置,每个槽的大小一样,可以理解为一个指针,这样我们只需要用一个算法把每个子链表的长度拆成差不多大小就行了。
在查找的时候,先基于Sn、S0找到指向的最大最小虚记录,在Slot区进行二分:先找到Sn和S0的中间位置,中间找到某个Slot,然后再一步步进行比较,通过几次二分后找到具体的子链表,最后,在子链表内进行遍历找到最终的记录。这样我们借助Slot区实现了一个近似二分查找的方法。这特别像Java里面的跳表结构,一次查找跳一次,再一次查找再跳一次,效率就特别高了。
最后我们来总结一波:一开始我们讲了Page页面的结构是怎样的,有页头页尾、最大最小虚记录、记录堆、未分配空间和自由空间链表、Slot区,知道了他们各自的作用是什么;接着通过聚簇索引这棵B+树把所有Page连接起来,Page与Page之间是逻辑有序的,已删除的记录用自由空间链表把它们连接起来,插入的时候优先把已删除数据的“空洞”给填上;页内的数据通过Slot槽划分一个个子链表,查询的时候比较Slot槽指向的数据,像二分法一步步确定数据在哪个子链表,最后在子链表内进行遍历。
一个成熟的技术框架,往往有些经典算法或核心思想在支撑着,比如提到Redis就能想到Slot,提到Hystrix就能想到滑动窗口算法等等,掌握了技术栈的核心思想,其实就掌握了这个框架的80%的核心内容。
好了,今天的分享到这里,下次我们继续扒。
老规矩,一键三连,日入两千,点赞在看,年薪百万~