为了账号安全,请及时绑定邮箱和手机立即绑定

一文了解InnoDB存储引擎

标签:
MongoDB

从Mysql5.5版本开始,InnoDB是默认的表存储引擎。其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU。

本文主要内容:

  • InnoDB体系架构

  • CheckPoint技术

  • InnoDB关键特性

一、InnoDB体系架构

下图简单描述了InnoDB存储引擎的体系结构:


webp

InnoDB存储引擎有多个内存块,这些内存块组成了一个大的内存池。后台线程主要负责刷新内存池中的数据、将已修改的数据刷新到磁盘等等。接下来我们分别介绍后台线程和内存池。

1.1 后台线程

InnoDB后台有多个不同的线程,用来负责不同的任务。主要有如下:

  • Master Thread
    这是最核心的一个线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括赃页的刷新、合并插入缓冲、UNDO 页的回收等.

  • IO Thread
    在 InnoDB 存储引擎中大量使用了异步 IO 来处理写 IO 请求, IO Thread 的工作主要是负责这些 IO 请求的回调处理。

  • Purge Thread
    事务被提交之后, undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用并分配的 undo页. InnoDB 支持多个 Purge Thread, 这样做可以加快 undo 页的回收。

  • Page Cleaner Thread
    Page Cleaner Thread 是在InnoDB 1.2.x版本新引入的,其作用是将之前版本中脏页的刷新操作都放入单独的线程中来完成,这样减轻了 Master Thread 的工作及对于用户查询线程的阻塞。

1.2 内存

这一部分,我将网上的比较好的资料尽力去综合,直观的描述出来。
InnoDB 存储引擎是基于磁盘存储的,也就是说数据都是存储在磁盘上的,由于 CPU 速度和磁盘速度之间的鸿沟, InnoDB 引擎使用缓冲池技术来提高数据库的整体性能。缓冲池简单来说就是一块内存区域.在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,下一次读取相同的页时,首先判断该页是不是在缓冲池中,若在,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。对于数据库中页的修改操作,首先修改在缓冲池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘。

缓冲池中缓存的数据页类型有:索引页、数据页、 undo 页、插入缓冲、自适应哈希索引、 InnoDB 的锁信息、数据字典信息等。索引页和数据页占缓冲池的很大一部分(知道有这些页,把这些页当做名词即可,不用感到迷惑)。在InnoDB中,缓冲池中的页大小默认为16KB。


webp

我们已经知道这个Buffer Pool其实是一片连续的内存空间,那现在就面临这个问题了:怎么将磁盘上的页缓存到内存中的Buffer Pool中呢?直接把需要缓存的页向Buffer Pool里一个一个往里怼么?不不不,为了更好的管理这些被缓存的页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、页在Buffer Pool中的地址,一些锁信息以及LSN信息(锁和LSN这里可以先忽略),当然还有一些别的控制信息。

每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:

webp

控制块和缓存页之间的那个碎片是个什么呢?你想想啊,每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为碎片了。当然,如果你把Buffer Pool的大小设置的刚刚好的话,也可能不会产生碎片~

前面我们知道了缓冲池的结构。接下来说InnoDB存储引擎是怎么对缓冲池进行管理的。讲下我当时的困惑吧,(这部分可以不看,因为有些名词还没涉及到)看《Mysql技术内幕》这本书,讲LRU List比较多,Free List只是顺带一过,没有将free list初始时是怎么分配的或者是什么样的结构,导致我对缓冲池的管理总是想象不出来,后来经过在网上寻找资料,算是弄明白了。来看一下吧:

当我们最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是分配Buffer Pool的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到Buffer Pool中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中,那么问题来了,从磁盘上读取一个页到Buffer Pool中的时候该放到哪个缓存页的位置呢?或者说怎么区分Buffer Pool中哪些缓存页是空闲的,哪些已经被使用了呢?我们最好在某个地方记录一下哪些页是可用的,我们可以把所有空闲的页包装成一个节点组成一个链表,这个链表也可以被称作Free链表(或者说空闲链表)。因为刚刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每一个缓存页都会被加入到Free链表中,假设该Buffer Pool中可容纳的缓存页数量为n,那增加了Free链表的效果图就是这样的:

webp

从图中可以看出,我们为了管理好这个Free链表,特意为这个链表定义了一个控制信息,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。我们在每个Free链表的节点中都记录了某个缓存页控制块的地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个Free链表节点都对应一个空闲的缓存页。

有了这个Free链表事儿就好办了,每当需要从磁盘中加载一个页到Buffer Pool中时,就从Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的Free链表节点从链表中移除,表示该缓存页已经被使用了~

我觉得原作者这部分讲的非常好,图也很用心,直接解决了我的疑问。

不要因为走的太远而忘记为什么出发。
简单回顾一下,为什么讲free list?是为了讲怎么管理buffer pool对吧。那free list就相当于是数据库服务刚刚启动没有数据页时,维护buffer pool的空闲缓存页的数据结构。

下面再来简单地回顾Buffer Pool的工作机制。Buffer Pool两个最主要的功能:一个是加速读,一个是加速写。加速读呢? 就是当需要访问一个数据页面的时候,如果这个页面已经在缓存池中,那么就不再需要访问磁盘,直接从缓冲池中就能获取这个页面的内容。加速写呢?就是当需要修改一个页面的时候,先将这个页面在缓冲池中进行修改,记下相关的重做日志,这个页面的修改就算已经完成了。至于这个被修改的页面什么时候真正刷新到磁盘,这个是后台刷新线程来完成的。

在实现上面两个功能的同时,需要考虑客观条件的限制,因为机器的内存大小是有限的,所以MySQL的InnoDB Buffer Pool的大小同样是有限的,如果需要缓存的页占用的内存大小超过了Buffer Pool大小,也就是Free链表中已经没有多余的空闲缓存页的时候岂不是很尴尬,发生了这样的事儿该咋办?当然是把某些旧的缓存页从Buffer Pool中移除,然后再把新的页放进来喽~ 那么问题来了,移除哪些缓存页呢?

为了回答这个问题,我们还需要回到我们设立Buffer Pool的初衷,我们就是想减少和磁盘的I/O交互,最好每次在访问某个页的时候它都已经被缓存到Buffer Pool中了。假设我们一共访问了n次页,那么被访问的页已经在缓存中的次数除以n就是所谓的缓存命中率,我们的期望就是让缓存命中率越高越好~

怎么提高缓存命中率呢?InnoDB Buffer Pool采用经典的LRU算法来进行页面淘汰,以提高缓存命中率。当Buffer Pool中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。不过,我们怎么知道哪些缓存页最近频繁使用,哪些最近很少使用呢?呵呵,神奇的链表再一次派上了用场,我们可以再创建一个链表,由于这个链表是为了按照最近最少使用的原则去淘汰缓存页的,所以这个链表可以被称为LRU链表(Least Recently Used)。当我们需要访问某个页时,可以这样处理LRU链表:

  • 如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页包装成节点塞到链表的头部。

  • 如果该页在Buffer Pool中,则直接把该页对应的LRU链表节点移动到链表的头部。

但是这样做会有一些性能上的问题,比如你的一次全表扫描或一次逻辑备份就把热数据给冲完了,就会导致导致缓冲池污染问题!Buffer Pool中的所有数据页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到Buffer Pool的操作,而这种全表扫描的语句执行的频率也不高,每次执行都要把Buffer Pool中的缓存页换一次血,这严重的影响到其他查询对 Buffer Pool 的使用,严重的降低了缓存命中率 !

所以InnoDB存储引擎对传统的LRU算法做了一些优化,在InnoDB中加入了midpoint。新读到的页,虽然是最新访问的页,但并不是直接插入到LRU列表的首部,而是插入LRU列表的midpoint位置。这个算法称之为midpoint insertion stategy。默认配置插入到列表长度的5/8处。midpoint由参数innodb_old_blocks_pct控制。

midpoint之前的列表称之为new列表,之后的列表称之为old列表。可以简单的将new列表中的页理解为最为活跃的热点数据。

同时InnoDB存储引擎还引入了innodb_old_blocks_time来表示页读取到mid位置之后需要等待多久才会被加入到LRU列表的热端。可以通过设置该参数保证热点数据不轻易被刷出。

好了,基本拿下了LRU list后,我们继续。前面我们讲到页面更新是在缓存池中先进行的,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名:dirty page)。所以需要考虑这些被修改的页面什么时候刷新到磁盘?以什么样的顺序刷新到磁盘?当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能(毕竟磁盘慢的像乌龟一样)。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,由后台刷新线程依次刷新到磁盘,实现修改落地到磁盘。

但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,假如Buffer Pool被设置的很大,比方说300G,那一次性同步这么多数据岂不是要慢死!所以,我们不得不再创建一个存储脏页的链表,凡是在LRU链表中被修改过的页都需要加入这个链表中,因为这个链表中的页都是需要被刷新到磁盘上的,所以也叫FLUSH链表,有时候也会被简写为FLU链表。链表的构造和Free链表差不多,这就不赘述了。这里的脏页修改指的此页被加载进Buffer Pool后第一次被修改,只有第一次被修改时才需要加入FLUSH链表(代码中是根据Page头部的oldest_modification == 0来判断是否是第一次修改),如果这个页被再次修改就不会再放到FLUSH链表了,因为已经存在。需要注意的是,脏页数据实际还在LRU链表中,而FLUSH链表中的脏页记录只是通过指针指向LRU链表中的脏页。并且在FLUSH链表中的脏页是根据oldest_lsn(这个值表示这个页第一次被更改时的lsn号,对应值oldest_modification,每个页头部记录)进行排序刷新到磁盘的,值越小表示要最先被刷新,避免数据不一致。

注意:脏页既存在于LRU列表中,也存在与Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。

这三个重要列表(LRU list, free list,flush list)的关系可以用下图表示:


webp

Free链表跟LRU链表的关系是相互流通的,页在这两个链表间来回置换。而FLUSH链表记录了脏页数据,也是通过指针指向了LRU链表,所以图中FLUSH链表被LRU链表包裹。

二、CheckPoint技术

说完缓冲池,下面说CheckPoint技术。
CheckPoint技术是用来解决如下几个问题:

  • 缩短数据库恢复时间

  • 缓冲池不够用时,将脏页刷新到磁盘

  • 重做日志不可用时,刷新脏页

缩短数据库恢复时间,重做日志中记录了的checkpoint的位置,这个点之前的页已经刷新回磁盘,只需要对checkpoint之后的重做日志进行恢复。这样就大大缩短了恢复时间。

缓冲池不够用时,根据LRU算法,溢出最近最少使用的页,如果页为脏页,强制执行checkpoint,将脏页刷新回磁盘。

重做日志不可用,是指重做日志的这部分不可以被覆盖,为什么?因为:由于重做日志的设计是循环使用的。这部分对应的数据还未刷新到磁盘上。数据库恢复时,如果不需要这部分日志,即可被覆盖;如果需要,必须强制执行checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。

checkpoint每次刷新多少页到磁盘?每次从哪里取脏页?什么时间触发checkpoint?

InnoDB存储引擎内部,两种checkpoint,分别为:

  • Sharp Checkpoint

  • Fuzzy Checkpoint

Sharp Checkpoint发生在数据库关闭时,将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数:innodb_fast_shutdown=1。
不适用于数据库运行时的刷新。

在数据库运行时,InnoDB存储引擎内部采用Fuzzy Checkpoint,只刷新一部分脏页。

几种发生Fuzzy Checkpoint的情况:
①MasterThread Checkpoint
异步刷新,每秒或每10秒从缓冲池脏页列表刷新一定比例的页回磁盘。异步刷新,即此时InnoDB存储引擎可以进行其他操作,用户查询线程不会受阻。



作者:小北觅
链接:https://www.jianshu.com/p/519fd7747137


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消