MySQL进阶浅谈InnoDB中的BufferPool

Posted 小颜-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL进阶浅谈InnoDB中的BufferPool相关的知识,希望对你有一定的参考价值。

mysql进阶】浅谈InnoDB中的BufferPool

参考资料:

MySQL之内存篇:深入探寻数据库内存与Buffer Pool的奥妙!

揭开 Buffer Pool 的面纱

《MySQL是怎么运行的:从根儿上理解MySQL》

文章目录

一、前言——缓存的重要性

对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以 的形式存放在 表空间 中的,而所谓的 表空间 只不过是InnoDB 对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。但是各位也都知道,磁盘的速度慢的跟乌龟一样,怎么能配得上“快如风,疾如电”的 CPU 呢?所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其 缓存 起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。

二、InnoDB的核心 - Buffer Pool

刚刚聊到过,InnoDB引擎几乎将所有操作都放在了内存中完成,这句话主要是跟它的Buffer Pool有关,但Buffer Pool到底会占用多大内存呢?这点可以通过show global variables like "%innodb_buffer_pool_size%";指令查询,如下:

show global variables like "%innodb_buffer_pool_size%";
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| innodb_buffer_pool_size | 44040192 |
+-------------------------+----------+
1 row in set (0.06 sec)

MySQL5.6版本以下,默认大小为42MB,而MySQL5.6以后的版本中,默认大小为128MB,这块内存是MySQL启动时向OS申请的一块连续空间。当然,我们也可以手动调整innodb_buffer_pool_size参数来控制,一般建议设置为机器内存的60~80%

接下来咱们先把Buffer Pool中每个区域的具体作用说明白,也就是这张图:

1:数据页(Data Page)

InnoDB引擎为了方便读取,会将磁盘中的数据划分为一个个的「页」,每个页的默认大小为16KB,以页作为内存和磁盘交互的基本单位,而InnoDB的缓冲池也会以页作为单位,也就意味着:当InnoDB拿到申请的连续内存后,会按照16KB的尺寸将整块空间,划分成一个个的缓冲页。

MySQL运行之初,这些划分出的缓冲页,都属于空闲页,也就是未使用的内存,随着运行时长的慢慢增长,会将磁盘中的数据页,一点点的载入内存当中,因为磁盘中的表数据是以16KB作为单位划分的,而内存中的缓冲页也是这个大小,因此发生一次磁盘IO读到的数据(读一页磁盘数据),会放入到一个缓冲页中存储,而这些承载磁盘数据的缓冲页,就被称之为数据页,其过程如下:

当磁盘中的数据被载入到内存之后,带来的优势会极为明显:

  • 读数据时:如果在数据页中有,则直接会从内存中读取数据并返回,没有再去磁盘检索数据。
  • 写数据时:会先修改数据页的数据,修改后会标记相应的数据页,然后直接返回,再由后台线程去完成数据的落盘工作。

此时有没有发现:InnoDB的缓冲池,其实也具备「查询缓存」的功能~

不过MySQL会将哪些表数据放到缓冲池中呢?其实刚启动时里面并不会有数据,而是随着业务SQL的执行,一点点将磁盘中的数据加载进内存的,比如执行一条查询语句,因为最初内存中并没有加载数据页,因此会走磁盘检索数据,检索数据的过程中,不管此次IO读到的数据是不是目标数据,都会将它们放在内存中,而不是直接回收。

观察上述这个过程,这样做有什么好处呢?方便后续其他SQL要操作对应数据时,可以直接在内存中读到数据。

在条件允许,即内存充足的情况下,InnoDB会试图将磁盘中的所有表数据全部载入内存。

不过一般的机器,磁盘空间都会比内存要大出很多倍,所以当表数据较大时,也不可能无限制的载入,因而InnoDB会有一套完善的内存管理与淘汰机制,以此防止内存溢出风险(对于这点后续再详细阐述)。

2:索引缓冲页(Index Page)

上面讲到了,InnoDB会将部分乃至所有表数据载入内存,以此达到提升性能的目的,但不可能无限制载入,比如现在机器的内存为16GB,但磁盘中有30GB表数据,这显然无法放入进内存,所以无可避免的一点:在运行过程中,MySQL会走磁盘读数据。

比如一条查询语句要读的数据,在内存中没有相关的缓冲数据页,因此需要触发磁盘IO检索数据,但此时这条SQL可以命中索引,那会通过索引去查找数据,但问题来了!索引的根节点可能位于磁盘的任意位置,难道把磁盘的所有位置全部走一遍吗?这显然并不现实,所以InnoDB也会有对应的优化机制,即内存中也会缓冲索引页。

MySQL启动时,就会将当前库中所有已存在的索引,其根节点放入到内存缓冲区中,因为索引的根节点只有16KB,因此就算目前库中就算创建了1000个索引,所有索引的根节点加起来占用的内存空间,也不过才15MB左右。将索引的根节点载入内存后,对于需要走索引查询的SQL,就会直接以相应的索引根节点为起始,然后去走索引查找数据,这样就避免了全盘查找索引根节点的这步操作。

Buffer Pool中有一块专门的区域:Index Page,专门用来存放载入的索引数据,存储这些数据的缓冲页,则被称之为索引页。随着运行时间的增长,也会将一些非根节点的索引页载入内存中,这是一种对于访问频率较高的索引页,专门推出的优化机制。

3:锁空间(Lock Space)

锁是基于事务实现,每个事务会生成自己的锁结构,而这些锁结构也同样需要空间来存储,而锁空间就是专门用来存储锁结构的一块内存区域。

但锁空间也不仅仅只会存储锁结构,还会存储一些并发事务的链表,例如死锁检测时需要的「事务等待链表、锁的信息链表」等。

锁空间一般都是有大小限制的,当锁空间内存不足时,就会导致行锁粗化成表锁,以此来减少锁结构的数量,释放一定程度上的内存,但此时并发冲突就会变高!

4:数据字典(Dict Info)

对于数据字典估计大家很少有人接触过,毕竟这个是用来辅助InnoDB运行用的,咱们先思考一个问题,为啥我们可以通过SQL语句查询到库中的表信息、查询一张表的索引、约束等信息呢?如下:

-- 查询当前库中的所有表
show tables;
-- 查询一张表的全部索引
show index from `tableName`;

这些语句执行后都能查询出对应的信息,但这些信息咋来的呢?这首先跟MySQL的系统表有关,在InnoDB引擎中主要存在SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四张系统表,主要是用来维护用户定义的所有表的各种信息,如下:

  • ID:一张表的ID号。
  • NAME:一张表的名称。
  • N_COLS:一张表的字段数量。
  • TYPE:一张表所使用的存储引擎、编码格式、压缩算法、排序规则等。
  • SPACE:一张表所位于的表空间。

SYS_COLUMNS:这张表用来存储所有用户定义的表字段信息。

  • TABLE_ID:表示一个字段属于那张表。
  • POS:一个字段在一张表中属于第几列。
  • NAME:一个字段的名称。
  • MTYPE:一个字段的数据类型。
  • PRTYPE:一个字段的精度值。
  • LEN:一个字段的存储长度限制。

SYS_INDEXES:这张表用来存储所有InnoDB引擎表的索引信息。

  • TABLE_ID:表示这个索引属于哪张表。
  • ID:一个索引的ID号。
  • NAME:一个索引的名称。
  • N_FIELDS:一个索引由几个字段组成。
  • TYPE:一个索引的类型,如唯一、联合、全文、主键索引等。
  • SPACE:一个索引的数据所位于的表空间位置。
  • PAGE_NO:这个索引对应的B+Tree根节点位置。

SYS_FIELDS:这张表用来存储所有索引的定义信息。

  • INDEX_ID:当前这个索引字段属于哪个索引。
  • POS:当前这个索引字段,位于索引的第几列。
  • COL_NAME:当前索引字段的名称。

这四张表也被称为InnoDB的内部表,这四张表在载入内存前,位于.ibdata文件中,在MySQL启动时会开始加载,载入内存后就会放入到Dict Info这块区域,当利用show语句查询表的结构信息时,就会在字典信息中检索数据。

5:日志缓冲区(Log Buffer)

InnoDB的缓冲池中,主要存在两个日志缓冲区,即undo_log_buffer、redo_log_buffer,分别对应着撤销日志和重做日志,但对于日志缓冲区就不过多介绍了,它俩的作用主要是用来提升日志记录的写入速度,因为日志文件在磁盘中,执行SQL时直接往磁盘写日志,其效率太低了,因此会先写缓冲区,再由后台线程去刷写日志。

6:自适应哈希索引(Adaptivity Hash)

自适应哈希索引又是一个比较有趣的技术点,这种技术可以算的上是一种AI技术,哈希算法查找数据的效率非常高,在没有哈希冲突的情况下复杂度为O(1),而B+Tree检索数据的效率,取决于树的高度。建立索引时,只能选用一种数据结构来作为索引的底层结构:

  • 如果选择哈希结构,虽然效率高,但数据是无序的,因此不方便做排序查询。
  • 如果选择B+Tree结构,虽然有序,但查询的效率会受到树高的影响。

此时似乎陷入了两难的地步,两种结构各有优劣,但一般为了满足业务按序查询的需求,所以会折中选择B+Tree结构,虽然没有哈希索引那么快,但速度也还可以。

分析上述这个场景,明明选哈希结构的效率特别惊人,但就是不能用,这就好比你面前有一道绝世佳肴,但就不能吃一样,这显然令人十分难受。

而正是由于此原因,InnoDB创始人在研发时,就实现了一种名为自适应哈希索引的技术,在MySQL运行过程中,InnoDB引擎会对表上的索引做监控,如果某些数据经常走索引查询,那InnoDB就会为其建立一个哈希索引,以此来提升数据检索的效率,并且减少走B+Tree带来的开销,由于这种哈希索引是运行过程中,InnoDB根据B+Tree的索引查询次数来建立的,因此被称之为自适应哈希索引。

自适应哈希索引和普通哈希索引的区别在哪儿呢?普通哈希索引是在创建索引时将结构声明为Hash结构,这种索引会以索引字段的整表数据建立哈希,而自适应哈希索引是根据缓冲池的B+树构造而来,只会基于热点数据构建,因此建立的速度会非常快,毕竟无需对整表都建立哈希索引。

自适应哈希索引在InnoDB中是默认开启的,可以通过手动调整innodb_adaptive_hash_index参数来控制关闭,但一般尽量不要去关闭它,因为该技术能让MySQL的整体性能翻倍。

MySQL8.0以下的版本中,如果同时删除一张大表的很多数据,有可能会因为自适应哈希索引的原因,造成线上MySQL出现抖动,不过该问题在MySQL8.x版本中已经被修复,但如若你的MySQL版本在此之下,那尽量不要在业务高峰期删除大量数据。

对于自适应哈希索引的使用情况,可以通过show engine innodb status \\G;命令查看,但哈希索引由于自身特性的原因,因此也仅只能用于等值查询的场景,无法支持排序、范围查询。

7:写入缓冲区(Insert Buffer)

Change Buufer写入缓冲」属于InnoDB的一大特性,其实「写入缓冲」在一开始被称之为「Insert Buffer插入缓冲」,也就是只对insert操作生效,到了MySQL5.5之后的版本中,才正式改为「写入缓冲」,对于insert、delete、update语句都可生效,那它的具体作用是干啥的呢?一起来简单的聊一聊。

结合前面聊过的「数据缓冲页」,咱们可以得知一点:如果要变更的数据页在缓冲区中存在,则会直接修改缓冲区中的数据页,然后标记一下变更过的数据页,但如果要操作的数据页并未被加载到缓冲区,那依旧会走磁盘去操作数据,走磁盘显然会影响性能,因此InnoDB就创造了一个「写入缓冲」。

insert语句为例,不管在MySQL的任何版本中,执行一条插入语句之前,因为这条数据在磁盘中都不存在,因此缓冲区中自然也不可能会有对应的数据页,按照前面的说法,似乎必须走磁盘插入数据了对不?

「写入缓冲」出现的原因,就是为了解决此问题,当一条写入语句执行时,流程如下:

  • ①判断要变更的数据页是否被载入到内存。
  • ②如果内存中有对应的数据页,则直接变更缓冲区中的数据页,完成标记后则直接返回。
  • ③如果内存中没有对应的数据页,则将要变更的数据放入到「写入缓冲」中,然后返回。

此时会发现,不管内存中是否存在相应的数据页,InnoDB都不会走磁盘写数据,而是直接在内存中完成所有操作,但是要注意:并不是所有的写入动作,都可以在内存中完成,「写入缓冲」是有限制的,如下:

  • 插入的数据字段不能具备唯一约束或唯一索引。

为啥呢?因为如果存在唯一字段的表,在插入数据前必须要先判断表中是否存在相同值,一张表的数据不可能全部都载入数据,所以这个判断重复值的工作必须依赖磁盘中的表数据来完成,所以插入具备唯一性的数据时,就必须要走磁盘。

这里有小伙伴或许会疑惑了,那我表中会有一个主键呀,默认会存在一个主键索引,主键索引也是一种特殊的唯一索引,那不就意味着所有具备主键的表,都不能通过「写入缓冲」来插入数据呀?这点不一定,如果表的主键声明了是一个自增ID,那这个自增序列会由MySQL-Server自己来维护,因此ID会由MySQL来生成,是绝对不会出现重复值的,因此对于这种情况,会将要插入的数据放到「写入缓冲区」中。

那如果表中存在唯一索引、或者表的主键未声明是自增ID,难道插入数据时就不会用到这个「写入缓冲区」吗?答案是NO,依旧会用,为啥?一条插入语句的执行过程如下:

  • ①先向聚簇索引中,插入一条相应的行记录(数据)。
  • ②对于非聚簇索引,都插入一个新的索引键,并将值指向聚簇索引中插入的主键值。

发现没有?插入数据时还需额外维护表中的次级索引,会为插入的新数据构建次级索引的索引键,并且将索引键插入到次级索引树当中,而这个过程就会用到「写入缓冲区」。

因为首先需要走一次磁盘,先插入行记录,插入完成后,假设表中存在三个非聚簇索引(次级索引),那难道再写三次磁盘维护次级索引吗?NO,对于不具备唯一性的索引,都会将要插入的索引键放在「写入缓冲区」。

对于修改、删除语句的执行,也是同理,那「写入缓冲区」中的数据究竟啥时候会真正写入到磁盘呢?

  • 当一条SQL需要用到对应的索引键查询数据时,会触发后台线程执行刷盘工作。
  • 当「写入缓冲区」内存空间不足时,会触发后台线程执行刷盘工作。
  • 当距离上一次刷盘的时间,间隔达到一定程度(默认10s),会触发后台线程执行刷盘工作。
  • MySQL-Server正在关闭时,也会触发后台线程执行刷盘工作。

上述这四种情况,都会导致后台线程执行刷盘工作,从而将数据真正的落入磁盘中存储。

到这里就已经将InnoDB缓冲池中,运行期间会出现的东西都讲明白啦,但最开始咱们提过一点,缓冲池是有大小限制的,毕竟内存有限,因此也不可能让咱们无限制的使用,那InnoDB是如何管理缓冲池内存的呢?接下来一起聊聊这个话题。

二、InnoDB缓冲池的内存是如何管理的?

1:BufferPool 简介

设计 InnoDB 的大叔为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做 Buffer Pool (中文名是 缓冲池 )。

2:BufferPool内部组成

Buffer Pool 中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是 16KB 。为了更好的管理这些在Buffer Pool 中的缓存页,设计 InnoDB 的大叔为每一个缓存页都创建了一些所谓的 控制信息 ,这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息

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

碎片:每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为 碎片 了。

3:free链表的管理

当我们最初启动 MySQL 服务器的时候,需要完成对 Buffer Pool 的初始化过程,就是先向操作系统申请 BufferPool 的内存空间,然后把它划分成若干对控制块和缓存页。

但是此时并没有真实的磁盘页被缓存到 BufferPool 中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到 Buffer Pool 中。那么问题来了,从磁盘上读取一个页到 Buffer Pool 中的时候该放到哪个缓存页的位置呢?或者说怎么区分 BufferPool 中哪些缓存页是空闲的,哪些已经被使用了呢?

我们最好在某个地方记录一下Buffer Pool中哪些缓存页是可用的,这个时候缓存页对应的 控制块 就派上大用场了,我们可以把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表也可以被称作 free链表 (或者说空闲链表)。刚刚完成初始化的 Buffer Pool 中所有的缓存页都是空闲的,所以每一个缓存页对应的控制块都会被加入到 free链表 中,假设该 Buffer Pool 中可容纳的缓存页数量为 n ,那增加了 free链表 的效果图就是这样的:

从图中可以看出,我们为了管理好这个 free链表 ,特意为这个链表定义了一个 基节点 ,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。这里需要注意的是,链表的基节点占用的内存空间并不包含在为 Buffer Pool 申请的一大片连续内存空间之内,而是单独申请的一块内存空间。

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

4:flush链表的管理

如果我们修改了 Buffer Pool 中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为 脏页 (英文名: dirty page )。当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步。

但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道 Buffer Pool 中哪些页是 脏页 ,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,假如 Buffer Pool 被设置的很大,比方说 300G ,那一次性同步这么多数据岂不是要慢死!

所以,我们不得不再创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫 flush链表 。链表的构造和 free链表 差不多,假设某个时间点 Buffer Pool 中的脏页数量为 n ,那么对应的 flush链表 就长这样:

5:LRU链表的管理

缓存不够的窘境

管理 Buffer Pool 的缓存页其实也是这个道理,当 Buffer Pool 中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。不过,我们怎么知道哪些缓存页最近频繁使用,哪些最近很少使用呢?呵呵,神奇的链表再一次派上了用场,我们可以再创建一个链表,由于这个链表是为了 按照最近最少使用 的原则去淘汰缓存页的,所以这个链表可以被称为LRU链表。当我们需要访问某个页时,可以这样处理 LRU链表 :

  • 如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓存页时,就把该缓存页对应的控制块 作为节点塞到链表的头部
  • 如果该页已经缓存在 Buffer Pool 中,则直接把该页对应的 控制块 移动到 LRU链表 的头部
划分区域的LRU链表

上边的这个简单的 LRU链表 用了没多长时间就发现问题了,因为存在这两种比较尴尬的情况

  • InnoDB 提供了一个看起来比较贴心的服务—— 预读 (英文名: read ahead )。所谓 预读 ,就是 InnoDB 认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到 Buffer Pool 中。根据触发方式的不同, 预读 又可以细分为下边两种:

    • 线性预读

      设计 InnoDB 的大叔提供了一个系统变量 innodb_read_ahead_threshold ,如果顺序访问了某个区( extent )的页面超过这个系统变量的值,就会触发一次 异步 读取下一个区中全部的页面到 BufferPool 的请求,注意 异步 读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。

    • 随机预读

      如果 Buffer Pool 中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次 异步 读取本区中所有其的页面到 Buffer Pool 的请求。

    预读 本来是个好事儿,如果预读到 Buffer Pool 中的页成功的被使用到,那就可以极大的提高语句执行的效率。可是如果用不到呢?这些预读的页都会放到 LRU 链表的头部,但是如果此时 Buffer Pool 的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在 LRU链表 尾部的一些缓存页会很快的被淘汰掉,也就是所谓的 劣币驱逐良币 ,会大大降低缓存命中率。

  • 可能会写一些需要扫描全表的查询语句

    扫描全表意味着什么?意味着将访问到该表所在的所有页!假设这个表中记录非常多的话,那该表会占用特别多的 页 ,当需要访问这些页时,会把它们统统都加载到 Buffer Pool 中,这也就意味着吧唧一下,Buffer Pool 中的所有页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到 Buffer Pool的操作。而这种全表扫描的语句执行的频率也不高,每次执行都要把 Buffer Pool 中的缓存页换一次血,这严重的影响到其他查询对 Buffer Pool 的使用,从而大大降低了缓存命中率。

总结一下上边说的可能降低 Buffer Pool 的两种情况:

  • 加载到 Buffer Pool 中的页不一定被用到。
  • 如果非常多的使用频率偏低的页被同时加载到 Buffer Pool 时,可能会把那些使用频率非常高的页从Buffer Pool 中淘汰掉。

因为有这两种情况的存在,所以设计 InnoDB 的大叔把这个 LRU链表 按照一定比例分成两截,分别是:

  • 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做 热数据 ,或者称young区域
  • 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做 冷数据 ,或者称 old区域

有了这个被划分成 young 和 old 区域的 LRU 链表之后,设计 InnoDB 的大叔就可以针对我们上边提到的两种可能降低缓存命中率的情况进行优化了:

  • 针对预读的页面可能不进行后续访情况的优化

    设计 InnoDB 的大叔规定,当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。这样针对预读到 Buffer Pool 却不进行后续访问的页面就会被逐渐从old 区域逐出,而不会影响 young 区域中被使用比较频繁的缓存页。

  • 针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化

    全表扫描有一个特点,那就是它的执行频率非常低,谁也不会没事儿老在那写全表扫描的语句玩,而且在执行全表扫描的过程中,即使某个页面中有很多条记录,也就是去多次访问这个页面所花费的时间也是非常少的。所以我们只需要规定,在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。上述的这个间隔时间是由系统变量innodb_old_blocks_time 控制的,你看:

    mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time';
    

综上所述,正是因为将 LRU 链表划分为 youngold 区域这两个部分,又添加了 innodb_old_blocks_time 这个系统变量,才使得预读机制和全表扫描造成的缓存命中率降低的问题得到了遏制,因为用不到的预读页面以及全表扫描的页面都只会被放到 old 区域,而不影响 young 区域中的缓存页。

6:刷新脏页到磁盘

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径:

  • 从 LRU链表 的冷数据中刷新一部分页面到磁盘。

    后台线程会定时从 LRU链表 尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth 来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为 BUF_FLUSH_LRU 。

  • 从 flush链表 中刷新一部分页面到磁盘。

    后台线程也会定时从 flush链表 中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为 BUF_FLUSH_LIST 。

7:多个Buffer Pool实例

我们上边说过, Buffer Pool 本质是 InnoDB 向操作系统申请的一块连续的内存空间,在多线程环境下,访问Buffer Pool 中的各种链表都需要加锁处理啥的,在 Buffer Pool 特别大而且多线程并发访问特别高的情况下,单一的 Buffer Pool 可能会影响请求的处理速度。所以在 Buffer Pool 特别大的时候,我们可以把它们拆分成若干个小的 Buffer Pool ,每个 Buffer Pool 都称为一个 实例 ,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,独立的吧啦吧啦,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。

我们可以在服务器启动的时候通过设置 innodb_buffer_pool_instances 的值来修改 Buffer Pool 实例的个数,比方说这样:

[server]
innodb_buffer_pool_instances = 2

这样就表明我们要创建2个 Buffer Pool 实例,示意图就是这样:

那每个 Buffer Pool 实例实际占多少内存空间呢?其实使用这个公式算出来的:

innodb_buffer_pool_size / innodb_buffer_pool_instances

也就是总共的大小除以实例的个数,结果就是每个 Buffer Pool 实例占用的大小。

8:MySQL5.6支持的Buffer Pool的新特性

BufferPool缓冲池预热

缓冲池预热是一种特别好的机制,在之前版本的内存缓冲池中,当MySQL关闭时,原本内存中的热点数据都会被清空,重启后所有的热点数据又需要经过时间的沉淀,然后才能留在内存中。但MySQL5.6版本中,每次关闭MySQL时都会将内存中的热点数据页保存到磁盘中,当重启时会直接从磁盘中载入之前的热点数据,避免了热点数据的重新“选拔”!

缓冲池刷盘策略优化

在之前的版本的InnoDB-BufferPool缓冲池中,变更过的数据页会共用MySQL后台的刷盘线程,也就是redo-log、undo-log、bin-log.....一系列内存到磁盘的刷盘工作,都是采用同一批线程来完成。在MySQL5.6版本中,BufferPool引入了独立的刷盘线程,也就意味着缓冲池中变更过的数据页会由专门的线程来负责刷盘,这样能够提升缓冲池的刷盘效率,无需排队等待刷写。

同时BufferPool的刷盘线程,还支持开启多线程并发刷盘操作,这样在缓冲池较大的情况下,能够进一步提升刷盘的效率,从而让数据落盘的效率更快,从一定程度上也提升了数据的安全性,毕竟内存中的数据随时都有几率丢失,但落盘后基本上不是硬件损坏,都有可能恢复过来。

浅谈MySQL存储引擎-InnoDB&MyISAM

存储引擎在MySQL的逻辑架构中位于第三层,负责MySQL中的数据的存储和提取。MySQL存储引擎有很多,不同的存储引擎保存数据和索引的方式是不同的。每一种存储引擎都有它的优势和劣势,本文只讨论最常见的InnoDB和MyISAM两种存储引擎进行讨论。本文中关于数据存储形式和索引的可以查看图解MySQL索引

MySQL逻辑架构图:

技术分享图片

InnoDB存储引擎

InnoDB是默认的事务型存储引擎,也是最重要,使用最广泛的存储引擎。在没有特殊情况下,一般优先使用InnoDB存储引擎。

1??、数据存储形式

使用InnoDB时,会将数据表分为.frm 和 idb两个文件进行存储。

技术分享图片

2??、锁的粒度

InnoDB采用MVCC(多版本并发控制)来支持高并发,InnoDB实现了四个隔离级别,默认级别是REPETABLE READ,并通过间隙锁策略防止幻读的出现。它的锁粒度是行锁。【通过MVCC实现,MVCC在稍后会进行介绍】

3??、事务

InnoDB是典型的事务型存储引擎,并且通过一些机制和工具,支持真正的热备份。

4??、数据的存储特点

InnoDB表是基于聚簇索引(另一篇博客有介绍)建立的,聚簇索引对主键的查询有很高的性能,不过他的二级索引(非主键索引)必须包含主键列,索引其他的索引会很大。

MyISAM存储引擎

1??、数据存储形式

MyISAM采用的是索引与数据分离的形式,将数据保存在三个文件中.frm.MYD,.MYIs。

技术分享图片

2??、锁的粒度

MyISAM不支持行锁,所以读取时对表加上共享锁,在写入是对表加上排他锁。由于是对整张表加锁,相比InnoDB,在并发写入时效率很低。

3??、事务

MyISAM不支持事务。

4??、数据的存储特点

MyISAM是基于非聚簇索引进行存储的。

5??、其他

MyISAM提供了大量的特性,包括全文索引,压缩,空间函数,延迟更新索引键等。

进行压缩后的表是不能进行修改的,但是压缩表可以极大减少磁盘占用空间,因此也可以减少磁盘IO,从而提供查询性能。

全文索引,是一种基于分词创建的索引,可以支持复杂的查询。

延迟更新索引键,不会将更新的索引数据立即写入到磁盘,而是会写到内存中的缓冲区中,只有在清除缓冲区时候才会将对应的索引写入磁盘,这种方式大大提升了写入性能。

三、对比与选择

两种存储引擎各有各的有点,MyISAM专注性能,InnoDB专注事务。两者最大的区别就是InnoDB支持事务,和行锁。

技术分享图片

如何在两种存储引擎中进行选择?

① 是否有事务操作?有,InnoDB。

②是否存储并发修改?有,InnoDB。

③是否追求快速查询,且数据修改较少?是,MyISAM。

④是否使用全文索引?如果不引用第三方框架,可以选择MyISAM,但是可以选用第三方框架和InnDB效率会更高。

四、浅谈MVCC

MySQL大多数事务型存储引擎实现的都不是简单的行锁。基于提升并发性能的考虑,他们一般都同时实现了多版本并发控制(MVCC)。

可以认为MVCC是行级锁的一个变种,它能在大多数情况下避免加锁操作,因此开销更低。无论怎样实现,它们大豆实现了非阻塞的读操作,写操作也只锁定制定的行。

MVCC是通过保存数据在某一个时间点的快照来实现的,也就是说无论事务执行多久,每个事务看到的数据都是一致的。InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存了行的过期时间(或删除时间),当然,并非存储的是时间,而是系统版本号。每开启一个事务,版本号都会递增,事务开始时刻的系统版本号会作为事务的版本号。

id name 创建时间(行版本号) 删除时间(删除版本号)
1 Mary 1 null
2 Jann 1 null

以InnoDB存储引擎的的REPEATABLE READ隔离级别来说:

SELECT

? ①只查询创建时间版本号小于当前事务版本号的数据行(保证事务读取的行要么在事务开始之前就存在,要么是事务本身插入的行)

? ②行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行,在开始事务之前未被删除

只有复合上诉两个条件的记录才会作为结果返回

INSERT

? 为插入的数据保存当前系统版本号作为行版本号

DELETE

? 保存当前系统版本号作为删除行版本号

?

UPDATE

? 插入一行数据,并将当前系统版本号赋予行版本号;同事保存当前系统版本号到原来的行作为删除版本号。

注:MVCC只在REPEATABLE和READ COMMITTED两个隔离级别下才能正常工作。
我的个人博客:李强的个人博客(基于SSM,Nginx+Redis的后台架构)


以上是关于MySQL进阶浅谈InnoDB中的BufferPool的主要内容,如果未能解决你的问题,请参考以下文章

浅谈MYSQL引擎之INNODB引擎

浅谈MySQL引擎

MYSQL中InnoDB特性浅谈

浅谈 MySQL InnoDB 的内存组件

浅谈 MySQL InnoDB 的内存组件

浅谈 MySQL InnoDB 的内存组件