第二章 InnoDB存储引擎

413 阅读9分钟

引言:

之前面试的时候,被问:MySQL同样有内存,那和redis有什么区别?那这个时候为什么还是选择redis,不用MySQL呢?确实,对于中间件的选择是要对业务进行评估后才进行选择的,那就需要我们掌握扎实的业务评估,同时也要对各种中间件的特性和性能做好相应的分析。

后来又遇到一个面试官,跟我说了很久联合索引的选取,这样就需要好好了解一下InnoDB。

在我们进行这章节的分析之前,需要注意几个数据:

机械硬盘:顺序平均读取速度能达到84.0MB/s顺序平均写入速度能达到79.0MB/s随机数据块为512字节时平均读取速度仅为0.033MB/s,数据块大小为4KB时,读取速度仅为0.226MB/s随机在数据块为512字节时平均写入速度仅为0.083MB/s数据块大小为4KB时,平均写入速度仅为0.576MB/s机械硬盘:\\ 顺序平均读取速度能达到84.0MB/s\\ 顺序平均写入速度能达到79.0MB/s\\ 随机数据块为512字节时平均读取速度仅为0.033MB/s,\\数据块大小为4KB时,读取速度仅为0.226MB/s。\\ 随机在数据块为512字节时平均写入速度仅为0.083MB/s,\\数据块大小为4KB时,平均写入速度仅为0.576MB/s。\\ \\

我们可以很容易看出来,对于机械硬盘来说,顺序读的速度和随机读的速度比值为2500倍,顺序写和随机写的比值约为1000倍。这显然要让我们注意,在不得已进行磁盘写入的时候,如果能合并就尽量合并成顺序写入就合并成顺序写入。这其实也是InnoDB在努力做的,我们接下来要讲的change buffer、自适应hash索引、异步IO、刷新邻接页,这些InnoDB的关键特性就是在努力完成这一点。

而当我们在分析时候,一般会把内存的读写耗费时间给忽略,因为内存的读写实在是太快了,下面给出一些对比,方便理解为什么忽略内存读写的耗时。

速度级别比较:
	DDR3内存读写速度大概10G每秒(10000M)
	固态硬盘速度是300M每秒,是内存的三十分之一
	机械硬盘的速度是100M每秒,是内存的百分之一

	DDR4内存读写速度大概50G每秒(50000M)
	固态硬盘速度是300M每秒,是内存的二百分之一
	机械硬盘的速度是100M每秒,是内存的五百分之一

1. InnoDB内存模型

就如引言所描述的,当我们需要与其他进行比较的时候,InnoDB的内存模型是我们不能忽略的。

11.jpg

其中数据页和索引页的区别就只是其内容,一个是数据,一个是索引。

在看到其内存模型后,必须伴随要随之而思考的是:内存满了,要怎么处理?也就是内存的置换策略InnoDB采用LRU方式来对内存进行置换,将最频繁使用的放在队列前端,要淘汰的放在队列尾端。Innodb对传统的LRU算法进行了适应性修改。我们来思考是怎样的问题:

按照传统的LRU算法,每次最新访问的就应该放在队列最前端。根据局部性原理,这是完全可以的。但考虑到数据库,很容易有这样一种情况,这个页被访问了,但也就仅仅访问一次,后续不再访问,那么是否也就浪费了。因为LRU链表有限,一般都是满负荷工作,当你进来一个,就意味着会出去一个,如果把经常访问的给置换了,就会造成速度缺失

但同时也问题来了,那么为什么操作系统那样设计,难道数据库就不满足局部性原理吗?其实也不是,是因为数据库还牵扯到大量的扫描操作(比如全表扫描、索引扫描),这个时候,会把数据都加载到内存一遍,但实际上只会用很少的数据

于是,InnoDB的设计者不知道是否是参考JVM里面的垃圾分代来设计,但最终也设计成了一个分代的内存管理方式。修正的LRU算法如下:

这种新的LRU算法加入了一个midpoint位置,由用户设置,默认是从尾部到首部的5/8位置。 当新插入页时,插入到midpoint位置,midpoint位置之前的成为old列表,midpoint位置之后的成为new列表。在超过innodb_old_blocks_time的时候之后,下一次访问新插入的页,会被放到整个LRU链表的头,也就是类似的年代迁移。 这样如果出现全表扫描,也只会一直置换midpoint之前的old列表,new列表可以继续使用。而通过增加innodb_old_blocks_time,可以更有效的保护new列表,也就是热点数据。

我们讨论这么久的LRU的内存管理方式,本质上还是回到之前的速度数据,因为如果内存管理不当,那么就会导致大量的缺页,增加了磁盘的访问,于是就会让我们的时间增加太多

2. Checkpoint技术

思考几个问题:

1. 数据库宕机了,如何恢复?是从最开始恢复?
2. 当缓冲池不够用时,要怎么处理?将脏页刷新到磁盘,但怎么标识你当前的刷新了?
3. 重做日志不可用时,刷新脏页要怎么标识?

对于InnoDB,为了解决这些问题,提出了Checkpoint技术。

对于Checkpoint之前的页,需要确保都已经刷新到磁盘。对于Checkpoint之前的页,需要确保都已经刷新到磁盘。

于是,只要标识到了Checkpoint,对于以上问题,就可以有以下解决方式。

1. 从Checkpoint点开始恢复即可
2. 当脏页刷新时候,同时强制刷新执行Checkpoint。
3. 当重做日志不可用时,也强制刷新执行Checkpoint,使得其至少刷新到当前重做日志的位置

通过2.3.的强制保证,会一直满足(2)的条件,于是就可以恢复时候从Checkpoint开始恢复

Checkpoint的实现是通过LSN(Log Sequence Number)来实现的,LSN是八字节的数字。在每个页,重做日志,Checkpoint中都有LSN,这样就可以标识是否一直,以及Checkpoint的点在哪儿了。

3.Change buffer

同样的,我们在引出这个概念前,先思考几个问题?

  1. 在数据库表数据插入的时候,我们是按主键索引递增,那表的辅助索引要怎么更新
  2. 更新辅助索引的时候,因为是按主键索引递增,那么基本上不可能按照辅助索引有序,那么更新辅助索引必然是离散的,怎么办?

同样的,为了解决辅助索引更新时离散的问题,采用了Change buffer技术,把多个离散更新合并成一个顺序更新。我们也知道对于磁盘的顺序读写和随机读写的差距是千倍的,所以在辅助索引量大的情况下节省效率非常可观。

但能使用Change buffer技术的,需要满足以下的条件:

1.索引是辅助索引2.所以是非唯一的1. 索引是辅助索引\\ 2. 所以是非唯一的\\

原因很简单,唯一索引的更新需要先去判断是否唯一,就需要对辅助索引的索引页进行读取,没办法节省效率,而主键索引必然是唯一索引。

Change buffer的实现如下:

  1. 对于每一个要更新的非唯一辅助索引,先创建一个change buffer节点(里面通过每次更新内容获得[table_space,offset]),节点根据[table_space,offset]排序放入change buffer树中
  2. change buffer树是一颗b+树,非叶子节点只有[table_space,offset],而叶子节点除了[table_space,offset],还有相应的更新操作的相关信息
  3. 当一个辅助索引页被读入内存的时候,就可以把change buffer上相关的节点操作合并到这个页上,于是当刷新到磁盘时就是顺序写入了

同时,一个很有趣的点是,Change buffer不仅仅在内存,实际上也在磁盘上面有空间,这其实是为了保证我们出错时对辅助索引的更新是正确的,不被减免的。

4. Double Write

这个技术也是出于对业务的弥补,考虑以下场景:

当你在写某一页的时候,写了一半,数据库宕机了怎么办?

很容易想到:redo日志就是天生用来干这个的,再走一遍redo日志不就可以了吗?

但事实上会有一个问题:

  1. redo日志记录的是对页的物理操作,比如偏移量xxx,写下‘ttt’记录。
  2. 那么当页本身就被损坏了呢?比如我们从偏移量50开始写,宕机时候不知道什么原因导致了50之前的内容也出现了混乱,那么这个时候再对页进行写,就是无意义了

实际上,简而言之:我们需要能恢复一个完整的不被破坏的页。于是就有了double write技术。

22.png

在对脏页进行更新的时候,过程如下:

1.将脏页拷贝进double write buffer
2.double write buffer里面的脏页顺序写入磁盘共享表空间
3.将脏页刷新到磁盘(本该在的位置)

相对于一次写,可以看到多开销的是将2.将double write buffer里面的脏页顺序写入磁盘共享表空间,但是由于这一步是顺序的,所以实际上消耗的时间并不多,相对于保证数据的完整性和安全性,值得。

5. 自适应hash索引、异步IO、刷新邻接页

相比于b+树,hash索引显然是一种更快的查找方式,于是InnoDB也做了一个自适应Hash索引,由系统自己创建,存在于内存的。

存在的条件是:

1.连续的使用某个相同的访问模式,例如where a=''  , where a='' and b=''
2.使用模式超过一个阈值,一般是100
3.页通过该模式访问了N次,N=页中记录/16

总结下来就是一句话:通过相同的模式访问某一页超过一定阈值,就给该页设置自适应hash索引

自适应hash索引的建立速度也不用担心,很快,因为当达到这个自适应hash索引建立的条件时候,这个页都在缓冲区中,直接建立即可。

异步IO的优势是让多个IO合并成一个IO,特别是在读取的页是连续的时候,合并IO的优势很明显。

刷新邻接页是指当刷新一个页的时候,如果相邻的也是脏页,也一并更新,但是如果这个页又很快被写脏,就会导致重复刷新,可以把这个特性关掉。

6.总结

本次通过博客总结了InnoDB的内存模型,以及分析了InnoDB使用的几种技术,Change buffer、double write等解决的问题,以及我总觉得在分析数据库实现技术时候,一定要抓住的点是改磁盘的随机读写为顺序读写。