mysql底层有多种存储引擎。如果你看它的源码,打开storage目录,可以看到这么多种存储引擎:
下面的表说明了一些主流的存储引擎的比较:
详细可以参考官方文档:https://dev.mysql.com/doc/refman/5.6/en/storage-engines.html
其中innobase最为经典,本文讲的就是innobase存储引擎的底层结构。
InnoDB在主键上建聚簇索引:将主键组织到一棵B+树中,而行数据就储存在叶子节点上
。
在非主键上建辅助索引:第一步在辅助索引B+树中检索属性值,到达其叶子节点获取对应的主键
;第二步使用主键在主索引B+树种再执行一次B+树检索操作
,最终到达叶子节点即可获取整行数据。MyISAM和Innodb是不一样的,在MyISAM中,主索引和辅助索引在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。简单的说,InnoDB是索引组织表,使用的聚簇索引,索引就是数据,顺序存储,因此能缓存索引,也能缓存数据。MyISAM是堆组织表,使用的是非聚簇索引,索引和文件分开,随机存储,只能缓存索引。下面的图表示一张表中InnoDB和MyISAM的主键索引和辅助索引的结构:
再看看innodb的整个架构图:
上部分是逻辑存储结构,下部分是物理存储结构。下面的表空间和缓存交互依靠的是Inno DB Master线程。InnoDB的主要工作都是在一个或者多个单独的Master线程中完成的, Master线程的优先级最高, 分为主循环、后台循环、刷新循环、暂停循环等循环, 最主要的是主循环。主循环每秒执行一次刷新日志缓冲区, 合并插入缓冲, 最多刷新100个脏数据页, 如果当前用户没有活动信息, 切换到后台循环等操作。
存储的最基本结构
,也是Innodb磁盘管理的最小单位
,与数据库相关的所有内容都存储在Page结构里。Page分为几种类型,包括索引页,日志页,inode页等。非聚簇索引
的插入或者更新操作设计了Insert Buffer来进行优化。在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入才能获得较高的插入性能。当一张表中存在非聚簇的且不唯一的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点就不是顺序的了,这时就需要随机读取非聚簇索引页。在插入数据过程中,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引页面是否在缓冲池中,如果在,就把记录直接插入到页中;如果不在,则先放入到一个Insert Buffer中,看似数据库这个非聚集的索引已经插到叶节点,而实际没有,这时存放在另外一个位置。
然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。
为什么通常我们要给表设置一个自增的主键?因为所有记录的节点按大小顺序存放在同一层的叶子节点中,这样就就会形成一个紧凑的索引结构,近似顺序填满,每次插入新数据时减少B+树维护的成本。对于升序插入索引和从尾开始删除记录时,InnoDB将通过算法保证索引中不会产生碎片。如果使用非自增主键,由于每次插入主键的值近乎于随机,分裂会造成了大量的碎片,后续不得不通过OPTIMIZE TABLE来重建并优化填充页面。另外区间读取时,MySQL预读一部分和你当前读数据所在内存相邻的数据块,也能有效减少磁盘I/O次数。
在很多情况下,特别是在那些包括实际的范围查找(限制的或不受限制的)的情况下使用常规索引(B树结构)。但是,如果用户的应用程序查询几乎总是点查询的话,用户也许应当使用哈希索引。
在InnoDB中哈希索引就是把列值作为输入,然后使用一个专用的内部哈希函数计算该行的物理地址(实际是行序号)作为输出。在Oracle中还可以根据键值来自行创建哈希索引而不是内部哈希函数来创建哈希索引。
InnoDB提供了两种查询方式:哈希搜索和二进制搜索。在进行具体搜索操作的过程中,首先尝试哈希搜索,如果失败再尝试二进制搜索。
如果满足下列条件,则在调用二进制搜索之前先调用哈希猜测搜索模块。
1、B树的搜索latch不是被排它锁定。
2、Latch 模式为仅对叶结点操作。
3、上次哈希猜测成功。
4、SQL语句中不含有"column LIKE ‘abc%’ ORDER BY columnDESC"这样的语句。
5、自适应搜索选项已经打开。
如果猜测成功表示根据哈希索引查找成功,否则还需接着进行二进制搜索。
索引对象以双向链表的结构连结,聚簇索引为表对象的第一个索引对象,每添加一个索引对象,就添加在双向链表的表尾。
元信息
的集合。当InnoDB打开一张表,就增加一个对应的对象到数据字典。添加一个索引对象到表对象时,如果表对象中已经有了对应的索引对象就不需要再添加,否则添加。doublewrite的工作流程:
doublewrite由两部分组成,一部分为内存中的doublewrite buffer,其大小为2MB,另一部分是磁盘上共享表空间(ibdata x)中连续的128个页,即2个区(extent),大小也是2M。
1、当数据缓冲池中的脏页刷新时,并不直接写入磁盘数据文件中,而是先拷贝至内存中的doublewrite buffer中;
2、接着从doublewrite buffer分两次写入磁盘共享表空间中(连续存储,顺序写,性能很高),每次写1MB;
3、第二步完成后,再将doublewrite buffer中的脏页数据写入实际的各个表空间文件(离散写);(脏页数据固化后,即进行标记对应doublewrite数据可覆盖)
4、doublewrite的崩溃恢复
如果操作系统在将页写入磁盘的过程中发生崩溃,innodb存储引擎可以从共享表空间的doublewrite中找到该页的一个最近的副本,将其复制到表空间文件,再应用redo log,就完成了恢复过程。
有人问如果保存日志的过程中掉电怎么办?最初始的想法是把一条日志的数据一次性写入硬盘,相当于一个原子操作,然而这并不可行,因为硬盘通常以512字节为单位进行操作,日志数据一超过512字节就不可能一次性写入了。所以实际上是这么做的:给每一条日志设置一个结束符,只有在日志写入成功之后才写结束符,如果一条日志没有对应的结束符就会被视为无效日志,直接丢弃,这样就保证了日志里的数据是完整的。
(参考https://www.cnblogs.com/geaozhang/p/7241744.html)
在数据库中,有lock与latch两种锁:
latch 一般称为闩锁(轻量级的锁) 因为其要求锁定的时间非常短,若迟勋时间长,则应用性能非常差,在InnoDB存储引擎中,latch有可以分为mutex(互斥锁)和rwlock(读写锁)其目的用来保证并发线程操作临界资源的正确性,并且没有死锁检测的机制。
lock的对象是事务,用来锁定的是数据库表、页、行。并且一般lock对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同),此外lock正如大多数数据库中一样,是有死锁机制的。
Lock用来保证数据的逻辑一致性, 由事务所拥有。
Latch用来保证数据的物理一致性,由进程或线程所拥有。
持有Latch的时间相对Lock来说要短得多,对内存中一个数据页的修改通常要先对其加Latch,然后再尝试级或页级的锁定。一个树的latch保护树中的所有非叶结点。树中的每个结点也都有自己的latch (页上的锁)。
在B树上的操作:一般首先获得树的共享latch,然后向下搜索直到达到叶结点,在获得叶结点上的latch之后,释放树上的latch。为了节省CPU时间,在搜索过程中并不对树的非叶结点加latch,这些页仅仅被固定。
如果一个操作需要改变树的结构,这个事务在搜索前会获取树的派它latch。例如,会导致叶结点分裂的操作如下:
1、InnoDB确定叶结点页中的分裂点。
2、分配一个新页。
3、在第一层非叶结点页中插入适当的节点指针。
4、释放树的排它latch。
5、从叶结点页中移动记录到新页中。
空间( filespace)
空间是由文件所组成的链表,空间中的文件有一定的顺序。每个空间用一个空间id来标识。id=0 的空间是系统表空间。空间可以分为三类:表空间,保存的是数据库文件;日志空间, 保存的是日志文件;日志备份空间,保存的是日志文件的备份。
系统表空间(共享表空间)
InnoDB系统表空间由配置文件中的选项参数InnoDB_data_file_path = /ibdata/ibdata1:10M:autoextend(默认)指定的数据文件组成。这些数据文件组成了系统表空间。在新创建的系统表空间中,InnoDB将从第一个数据文件开始分配空间。
InnoDB系统表空间包含InnoDB数据字典,表数据页,表索引页,Undo空间,回滚段,双写缓冲区,插入缓冲等。系统表空间也默认包含任何用户在系统表空间创建的表数据和索引数据。
单表单文件表空间(File-Per-Table Tablespaces)(独立表空间)
为了避免在系统表空间中存储所有表和索引,可以启用innodb_file_per_table配置选项(默认值),单表单文件特性提供了一个更加灵活的选择,每个表的数据都存储在自己的表空间数据文件(.ibd)里。启用了这个参数,每张表的表空间内存放的只是数据、索引和插入缓冲页,其他类型的数据还是存放在共享表空间内。
此外还有通用表空间(General Tablespaces),Undo表空间,临时表空间(The Temporary Tablespace)等等,就不一一讲了。
段(Segment)
段是磁盘上空间分配和回收的申请者,是一个逻辑概念,用来管理物理文件。常见的段有数据段、索引段、回滚段等。段是为了保持叶子节点在磁盘上的连续,可以实现更好的顺序I/O操作,因为这些叶子节点包含实际的表数据。
在InnoDB中会为每个索引分配两个段,一个段对应B+树的非叶子节点(索引段),另一个对应叶子节点(数据段)。
区(Extend)
区是由连续页组成的空间,每个区的大小都是1MB,一个区中有64个连续的页。为了保证区中页的连续性,扩展的时候InnoDB存储引擎一次从磁盘申请4~5个区。
页(Page)
Page是整个InnoDB存储的最基本构件,也是InnoDB磁盘管理的最小单位,与数据库相关的所有内容都存储在这种Page结构里。
下面具体讲关于页。
Page分为几种类型,索引存在数据页(B-tree Node)中。
单个Page的大小是16K(编译宏UNIV_PAGE_SIZE控制)
对于记录,有个最大行长度,这个长度略小于数据库page的一半。例如,对于默认的16KB InnoDB页面大小,最大行长度略小于8KB。因此一个B+Tree叶子节点最少有2个行记录。
当一条新记录被插入到InnoDB clustered index中时,InnoDB预留page的1/16的空间以备将来插入或者更新索引记录。对于顺序写入的索引(无论是递增或是递减,顺序的就行),索引叶点可以达到15/16满。如果是随机的索引写入行为,叶点只会达到1/2到15/16满。当叶点填充在1/2以下满,或是被删除到1/2下满时,Innodb会缩短索引树,试图释放该叶点,该叶点可以被继续写入数据。
只能查到被查找数据行所在的页
,然后数据库通过把页读入内存,再在内存中查找,最后得到结果。如何定位一个Record?
记录堆(record heap):An unordered list is often called a “heap”. 记录堆指上图的橙黄色部分。表示页面已分配的记录空间,也是索引数据的真正存储区域。记录堆分为两种,即有效记录
和已删除记录
。有效记录就是索引正常使用的记录,而已删除记录表示索引已经删除,不在使用的记录,如上图的深蓝色部分。随着记录的更新和删除越来越频繁,记录堆中已删除记录将会越多,即会出现越来越多的空洞(碎片)。这些已删除记录连接起来,就会成为页面的自由空间链表。
未分配空间:指页面未使用的存储空间,随着页面不断使用,未分配空间将会越来越小。当新插入一条记录时,首先尝试从自由空间链表中获得合适的存储位置(空间足够),如果没有满足的,就会在未分配空间中申请。
除了records和free records外,还有一个连续的空间,这个空间是用来做记录分配的,地址偏移从PAGE_HEAP_TOP开始。只有当free records中没有合适的记录空间的时候,才会在这个连续空间上分配记录。空间分配给记录后,需要递增PAGE_N_RECS和PAGE_N_HEAP。
当页空间不足时,通过函数btr_page_reorganize进行页的重新组织,整理碎片空间。
页记录在物理上可能是无序的。PAGE_LAST_INSERT,PAGE_DIRECTION,PAGE_N_DIRECTION的作用是进行页的分裂操作。根据上述值判断插入方向(升序插入还是降序插入,亦或是无序随机插入),采用不同的分裂策略
主索引树非叶节点(绿色)
(1) Cluster Key(主键值)。子节点存储的主键里最小的值(Min Cluster Key on Child),这是B+树必须的,作用是在一个Page里定位到具体的记录的位置。
(2) 指向的节点所在的Page编号。最小值所在的Page编号(Child Page Number),为了定位Record。
主索引树叶子节点(黄色)
(1) Cluster Key(主键值)。Cluster Key Fields
(2) 除去Cluster Key以外的所有列。Non-Key Fields
这里的(1)和(2)两部分加起来就是一个完整的数据行
辅助索引树非叶节点(蓝色)
(1) Secondary Key(辅助索引键值)。子节点里存储的辅助键值里的最小的值(Min Secondary-Key on Child)
(2) Cluster Key(主键值)。Cluster Key Fields 因为辅助索引是可以不唯一的,但是B+树要求键的值必须唯一,所以这里把辅助键的值和主键的值合并起来作为在B+树中的真正键值,保证了唯一性。
但是这也导致在辅助索引B+树中非叶节点反而比叶子节点多了4个字节
(3) 指向的节点所在的Page编号。最小的值所在的Page的编号(Child Page Number)
辅助索引树叶子节点(红色)
(1) Secondary Key(辅助索引键值)
(2) Cluster Key(主键值)
下面是B+树的一部分
在树的每一层中,都定义一个特殊的记录一minimum record,以
任何字母顺序排序,都将它作为最小的记录。minimum record 通过设置用户记录头中的一个标志位来定义。minimum record 的作用是作为一个结点指针的前缀来指向下一层的最左子结点。
注意Page和B+树节点之间并没有一一对应的关系,Page只是作为一个Record的保存容器,它存在的目的是便于对磁盘空间进行批量管理,上图中的编号为47的Page在树形结构上就被拆分成了两个独立节点。
record只是包含键(子节点中最小键)和指向子节点的page的指针,这样自然的就能构造出B+树,B+树逻辑上record是指向子节点中最小键的record。record不是代表节点,几个record一起才是一个节点
Page Directory由槽(slot)构成,每个槽占用2个字节,指向记录在页中的偏移量。槽根据记录主键逆序存放,因此二分法可以快速定位查询记录。
为了提高效率,Page Directory是稀疏的,不是每一个记录都对应一个槽。不在槽中的记录通过链表查询,Record头中的n_owned属性表示槽“拥有“记录数量,以下是各种slot的n_owned描述范围:
如果一个支链的高度超过或不足,会导致相应的支链拆分和合并操作。
下面是一个例子:
从图可以看出,slot指向的Record中的owned代表的是向前有多少个rec属于这个slot管辖,中间被管辖的rec的owned = 0。
由于没有给每个page提供相应的slot,查找结果只能给一个大概的位,槽内记录再根据next指针顺序比较来定位。
通过directory的二分查找只能查到对应记录所属的slot,还需要通过owned内部遍历才能精确定位到对应的记录。
如何定位一个Record,就要用Cursor(游标)
(参考 https://www.jianshu.com/p/0cdd573a8232关于innodb中查询的定位方法)
页内查找的实现在page0cur.c的page_cur_search_with_match函数当中,这个函数除了返回查找的记录以外,还会记录二分查找过程中匹配的字节数和经过的跳数。值得注意的是这个函数支持四种模式的查找
溢出页
有时候如果一条记录过长,就需要存储到溢出页。是否存到溢出页(一般是Uncompressed BLOB Page )主要取决于下面三个因素:
1)大对象的size
CHAR(255)、VARCHAR(65535 )、VARBINARY、BLOB、TEXT等
2)整个行的size(保证一个索引页中存储最少两条记录)
3)innodb 行格式
可变长度列的外部页存储行格式有两种:
COMPACT和REDUNDANT行格式:将前768个字节存储在行中,其余的存储在外部溢出页中,每个这样的列都有自己的溢出页列表,存储一个20字节的值,该值存储列的真实长度,并指向溢出列表中存储该列的剩余部分。
DYNAMIC和COMPRESSED行格式:InnoDB在该行中存储20字节的指针,并指向溢出列表中存储该列的剩余部分。
(参考https://blog.csdn.net/shaochenshuo/article/details/77897379)
page大小可由innodb_page_size设置为4KB,8KB,16KB,32KB或64KB。
对于innodb存储引擎来说,对于二进制和字符串大对象的处理都是一样的。InnoDB早期的文件格式(页格式)为Antelope,可以定义两种行记录格式,分别是Compact和Redundant
Named File Format为了解决不同版本下页结构的兼容性,在Barracuda可以定义两种新的行记录格式Compressed和Dynamic
varchar列值长度大于某数N时也会存溢出页,在latin1字符集下N值可以这样计算:innodb的块大小默认为16kb,由于innodb存储引擎表为索引组织表,树底层的叶子节点为一双向链表,因此每个页中至少应该有两行记录,这就决定了innodb在存储一行数据的时候不能够超过8k,减去其它列值所占字节数,约等于N。
外部存储页类型:
File Header字段
数据页(B-tree Node)
undo页(undo Log Page)
系统页(System Page)
事务数据页(Transaction system Page)
插入缓冲位图页(Insert Buffer Bitmap)
插入缓冲空闲列表页(Insert Buffer Free List)
未压缩的二进制大对象页(Uncompressed BLOB Page)
压缩的二进制大对象页(compressed BLOB Page)
Page Header字段
管理释放的记录:
PAGE_FREE指向第一个被删除的rec记录的页内偏移量
PAGE_GARBAGE来管理其空间大小,这个值表示所有删除的记录占用空间字节总和,以便删除的记录可以重复被使用,提高空间的使用率
Record字段
通过记录头中的next record可以串联得到一个可用空间链表。
Heap no仅代表物理存储顺序,不代表键值顺序。