数据库内存与Buffer Pool
文章目录
- 数据库内存与Buffer Pool
- 一:MySQL内存结构
- 1:MySQL工作组件
- 2:工作线程的本地内存
- 3:共享内存区域
- 4:存储引擎缓冲区
- 二:InnoDB的核心:Buffer Pool
- 1:数据页(Data Page)
- 2:索引缓冲页(Index Page)
- 3:锁空间(Lock Space)
- 4:数据字典(Dict Info)
- 5:日志缓冲区(log buffer)
- 6:自适应哈希索引(Adaptivity Hash)
- 7:写缓冲区(Change buffer)
- 三:InnoDB缓冲池内存是如何管理的
- 1:什么是缓冲页的控制块
- 2:空闲页的管理
- 3:标记页的管理
- 4:淘汰机制(重点)
- 5:末位淘汰的问题
- 5.1:预读失效问题(young, old解决)
- 5.2:缓冲池污染问题(young区晋升解决)
一:MySQL内存结构
JVM & MySQL
实际上MySQL内存模型和JVM类似,JVM内存主要会划分为线程共享区和线程私有区
上图中的MySQL内存区域,左边则是线程私有区域,每条工作线程中都会分配的区域,各线程之间互不影响,而右边的三大板块,则属于线程共享区域,即所有线程都可访问的内存。
然而,MySQL线程共享区这块也会细分,右边上面的两个板块,都属于MySQL-Server层使用的内存,也就意味着这两块内存是所有引擎都共享的区域
而最下面这个区域,每个存储引擎都不相同,也就是InnoDB会构建自己的Buffer缓冲区,MyISAM也会构建自己的缓冲区
1:MySQL工作组件
这个比较容易理解,也就是对应着MySQL架构图中的组件层
因为后续客户端连接时,都需要经过一系列的连接工作,处理SQL时也需要经过一系列的解析、验证、优化工作
所以MySQL会在启动时,会先将这些工作组件初始化到内存中,方便后续处理客户端的操作。
数据库的连接池中,存的到底是什么?
存的实际上就是数据库连接对象,MySQL内部的连接对象,其中包含了客户端连接信息,如客户端IP、登录的用户、所连接的DB…等这类信息
同时这些连接对象在内部会绑定一条工作线程,因此你也可以将它理解成是一个线程池!
MySQL复用连接的本质,实则是在复用线程,出现一个新的客户端连接时,首先会根据客户端信息为其创建连接对象,然后再复用连接池中的空闲线程。
2:工作线程的本地内存
工作线程的本地内存区域,也被称之为线程私有区,即MySQL在创建每条线程时,都会为其分配这些内存:
可以看到,在工作线程的本地内存中,除开最基本的线程堆栈外,MySQL还往内部“塞了”一堆东西
这些东西在不同的SQL运行时,都有各自的作用,但基本上是为了更好的保存临时数据而设计的。
为啥不是直接在共享内存中搞一块大的空间,然后提供给所有线程来操作呢?
因为这些数据本身就是一条线程在执行SQL时产生的临时数据,其他线程压根不会去用到另一条线程的临时数据,所以这些临时数据没有必要被共享。
除此之外,将这些缓冲区都放在线程本地内存中,还有一点最大的好处:能够提升多线程并发执行的性能!
不过也并非所有数据都适合放在线程的本地内存中,有一些多条线程之间都会访问的数据,如果再放到本地内存中,就会造成很大的冗余性,比如典型的索引根节点数据,每条线程都有可能会通过索引查询数据,因此每条线程都“缓存”一份放在自己的内存中,就会占用大量的内存空间,这样反而弊大于利。
3:共享内存区域
这里解释下文件描述符:File Descriptor
比如我现在想要操作users表的数据,那首先得找到这张表,但表的位置可能分布在磁盘的任何一处,总不能触发磁盘IO把整个磁盘检索一遍
所以内存中直接设计了一个缓存区,专门缓存这些表数据文件的磁盘位置
要对某张表进行操作时,直接去文件描述符缓存中找,然后根据其中记录的地址,去磁盘中固定的位置上操作表数据。
表结构的文件描述符缓存,作用也是相同的,直接根据内存中的文件描述符,去操作磁盘中对应位置的表结构文件。
MySQL8.x为什么移除了Query Cache
Query Cache的核心工作原理是对于一些频繁执行的查询SQL,直接将结果缓存在内存中,之后再次来查询相同数据时,就无需走磁盘,而是直接从查询缓存中获取数据并返回。看似使用,其实很鸡肋,原因如下:
- 缓存命中率低:几乎大部分SQL都无法从查询缓存中获得数据。
- 占用内存高:将大量查询结果放入到内存中,会占用至少几百MB的内存。
- 增加查询步骤:查询表之前会先查一次缓存,查询后会将结果放入缓存,额外多几步开销。
- 缓存维护成本不小,需要LRU算法淘汰缓存,同时每次更新、插入、删除数据时,都要清空缓存中对应的数据。
- InnoDB引擎构建出的缓冲区中,也会类似的功能,因为与查询缓存也存在冲突。
- 项目中一般都会使用Redis先做业务缓存,因此能来到MySQL的查询语句,几乎都是要从表中读数据的,所以查询缓存的地位就显得更加突兀
4:存储引擎缓冲区
几乎任何存储引擎都会在启动时,向操作系统申请一块内存,用来作为缓冲区
每个引擎的缓冲区也并不相同,但有一点是共通的:即所有引擎的缓冲区,对于MySQL的工作线程而言,都是一块共享的内存区域
为何各大存储引擎都会设计一个缓冲池?
虽然MySQL是基于磁盘存储数据的,但总不能每次读写操作都走磁盘
这样绝对会导致资源开销极大,同时性能也极低,因此各引擎都在内存中设计了一个缓冲池,用来提升数据库整体的读写性能。
🎉 而InnoDB引擎,是尤为特殊的存在,几乎将所有的操作都放在了内存中完成,这也是InnoDB能取代MyISAM作为默认引擎的原因之一
二:InnoDB的核心:Buffer Pool
MySQL众多存储引擎中,应用最为广泛的是InnoDB,Buffer-Pool中主要有下面的内容:
Buffer Pool到底会占用多大内存呢?
show global variables like "%innodb_buffer_pool_size%";
MySQL5.6以后的版本中,默认大小为128MB,这块内存是MySQL启动时向OS申请的一块连续空间。
当然,也可以手动调整innodb_buffer_pool_size
参数来控制,一般建议设置为机器内存的60~80%。
1:数据页(Data Page)
主要用来缓冲磁盘的表数据,将写操作转移到内存进行。
InnoDB引擎为了方便读取,会将磁盘中的数据划分为一个个的「页」,每个页的默认大小为16KB,以页作为内存和磁盘交互的基本单位
而InnoDB的缓冲池也会以页作为单位,也就意味着:当InnoDB拿到申请的连续内存后,会按照16KB的尺寸将整块空间,划分成一个个的缓冲页。
在MySQL运行之初,这些划分出的缓冲页,都属于空闲页,也就是未使用的内存
随着运行时长的慢慢增长,会将磁盘中的数据页,一点点的载入内存当中
因为磁盘中的表数据是以16KB作为单位划分的,而内存中的缓冲页也是这个大小,因此发生一次磁盘IO读到的数据(读一页磁盘数据),会放入到一个缓冲页中存储,而这些承载磁盘数据的缓冲页,就被称之为数据页
当磁盘中的数据被载入到内存之后,带来的优势会极为明显:
- 读数据时:如果在数据页中有,则直接会从内存中读取数据并返回,没有再去磁盘检索数据。
- 写数据时:会先修改数据页的数据,修改后会标记相应的数据页,然后直接返回,再由后台线程去完成数据的落盘工作。
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)
为啥我们可以通过SQL语句查询到库中的表信息、查询一张表的索引、约束等信息呢?
-- 查询当前库中的所有表
show tables;
-- 查询一张表的全部索引
show index from tableName;
这些语句执行后都能查询出对应的信息,但这些信息咋来的呢?
这首先跟MySQL的系统表有关,在InnoDB引擎中主要存在SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四张系统表
主要是用来维护用户定义的所有表的各种信息,如下:
- SYS_TABLES:这张表中会存储所有引擎为InnoDB的表信息。
- 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:写缓冲区(Change buffer)
如果要变更的数据页在缓冲区中存在,则会直接修改缓冲区中的数据页,然后标记一下变更过的数据页
但如果要操作的数据页并未被加载到缓冲区,那依旧会走磁盘去操作数据
走磁盘显然会影响性能,因此InnoDB就创造了一个「写入缓冲」
以insert语句为例,不管在MySQL的任何版本中,执行一条插入语句之前,因为这条数据在磁盘中都不存在,因此缓冲区中自然也不可能会有对应的数据页
而「写入缓冲」出现的原因,就是为了解决此问题,当一条写入语句执行时,流程如下:
- 判断要变更的数据页是否被载入到内存。
- 如果内存中有对应的数据页,则直接变更缓冲区中的数据页,完成标记后则直接返回。
- 如果内存中没有对应的数据页,则将要变更的数据放入到「写入缓冲」中,然后返回。
此时会发现,不管内存中是否存在相应的数据页,InnoDB都不会走磁盘写数据,而是直接在内存中完成所有操作
⚠️ 并不是所有的写入动作,都可以在内存中完成,「写入缓冲」是有限制的:插入的数据字段不能具备唯一约束或唯一索引
因为如果存在唯一字段的表,在插入数据前必须要先判断表中是否存在相同值
一张表的数据不可能全部都载入数据,所以这个判断重复值的工作必须依赖磁盘中的表数据来完成,所以插入具备唯一性的数据时,就必须要走磁盘。
那如果我的表上有一个主键id岂不是一定要走磁盘了?
其实并不一定,如果表的主键声明了是一个自增ID,那这个自增序列会由MySQL-Server自己来维护,因此ID会由MySQL来生成,是绝对不会出现重复值的。
因此对于这种情况,会将要插入的数据放到「写入缓冲区」中。
如果表中存在唯一索引、或者表的主键未声明是自增ID,难道插入数据时就不会用到这个「写入缓冲区」吗?
其实也会用,前面说过,一条插入语句的执行过程如下:
- 先向聚簇索引中,插入一条相应的行记录(数据)。
- 对于非聚簇索引,都插入一个新的索引键,并将值指向聚簇索引中插入的主键值。
所以插入数据时还需额外维护表中的次级索引,会为插入的新数据构建次级索引的索引键,并且将索引键插入到次级索引树当中
这个过程就会用到「写入缓冲区」。
因为首先需要走一次磁盘,先插入行记录,插入完成后,假设表中存在三个非聚簇索引(次级索引),都会将要插入的索引键放在「写入缓冲区」。
「写入缓冲区」中的数据究竟啥时候会真正写入到磁盘呢?
- 当一条SQL需要用到对应的索引键查询数据时,会触发后台线程执行刷盘工作。
- 当「写入缓冲区」内存空间不足时,会触发后台线程执行刷盘工作。
- 当距离上一次刷盘的时间,间隔达到一定程度(默认10s),会触发后台线程执行刷盘工作。
- MySQL-Server正在关闭时,也会触发后台线程执行刷盘工作。
上述这四种情况,都会导致后台线程执行刷盘工作,从而将数据真正的落入磁盘中存储。
三:InnoDB缓冲池内存是如何管理的
InnoDB虽然在启动时,会将连续的内存划分为一块块的缓冲页,但这仅是逻辑上的划分,本质上所有的缓冲页之间,也是连续的内存。
但随着MySQL在线上运行的时间越来越长,自然会导致这片连续的缓冲页变得七零八落
当从磁盘加载一个数据页时,总不能将所有的缓冲页全部遍历一次,所以为了更好的管理缓冲池,InnoDB会为每个缓冲页创建一个控制块。
1:什么是缓冲页的控制块
控制块是专门用于管理缓冲页而设计的一种结构,其中会包含:数据页所属的表空间、页号、缓冲页地址、链表节点指针等信息
所有的控制块都会放在缓冲池最前面
控制块也会占用缓冲池的内存空间,InnoDB会为每一个缓冲页都分配一个对应的控制块,后续InnoDB可以基于控制块去管理每一块缓冲页
2:空闲页的管理
为了能够更快的找到缓冲池中的空闲页,InnoDB会以控制块作为节点,将所有空闲的缓冲页组成一个空闲链表,即Free链表
因为控制块对应着一个个的缓冲页,以控制块作为链表节点,也就等价于是由缓冲页组成的链表,在链表中会存在一个头结点,内部主要有三个值:
- head:这是一根指针,指向空闲链表的第一个控制块。
- tail:同样是一根指针,指向空闲链表的最后一个控制块。
- count:这是一个数字,用来记录空闲链表的节点数量。
有了空闲链表后,会有什么好处呢?
当需要一块新的缓冲页存储磁盘数据时,直接找到空闲链表,根据空闲链表的指针,从中拿一块空闲缓冲页使用即可。
3:标记页的管理
当线程变更了内存中的数据页之后,会先对这个数据页做个标记,然后直接给客户端返回「执行成功」的响应。
在这个过程中,被线程变更并标记过的数据页,则被称之为标记页,有些地方也被称之为“脏页”。
对于内存中变更过的数据页,最终绝对是需要刷写到磁盘中的,这个工作会由MySQL的后台线程完成
当后台线程要刷盘时,它咋知道哪些数据页是变更过的呢?
为了后台线程刷盘时效率更高,InnoDB同样又创造了一个Flush链表,它的结构和Free链表一模一样,两者的不同点在于:
- Free链表:记录空闲缓冲页,为了使用时能更快的找到空闲缓冲页。
- Flush链表:记录标记过的缓冲页,为了刷盘时能够更快的找到变更数据页。
当后台线程开始刷盘工作时,会直接找到Flush链表,然后直接将该链表中对应的缓冲页,其变更过的数据刷写到磁盘。
4:淘汰机制(重点)
Buffer Pool的内存空间是有限的,因此无法支撑所有数据源源不断的载入内存,所以InnoDB内部有一套自己的淘汰机制
首先明确下淘汰的含义:淘汰是指将一个已使用的缓冲页,其中的所有数据清空,使其变为一个空闲页。
淘汰愿景:那些频繁被访问的数据页可以长期驻留在内存中,一些很少被访问的数据页能够淘汰掉。
与Free空闲链表、Flush刷写链表相同,对于要可淘汰的数据页,也会被组合成一个LRU淘汰链表
对于空闲页和标记页是不会纳入淘汰范围的
- 空闲页:本身这些缓冲页都没有被使用,内存都是空白的,淘汰空闲页没有任何意义。
- 标记页:被标记过的缓冲页中,由于存在数据还未落盘,所以淘汰掉之后代表数据会丢失。
因此LRU链表是由已使用、但未曾变更过的缓冲页组成的,不过要注意:有些数据页会在Flush、LRU两个链表之间“跳动”:
- 当LRU链表中的一个数据页发生变更后,会从LRU链表转到Flush链表。
- 当标记页中的变更数据落盘后,此时标记页又会从Flush链表回到LRU链表。
末位淘汰机制 - 残酷的森林法则
- 当一条线程来读写数据时,命中了缓冲区中的某个数据页,那就直接将该页挪到LRU链表最前面。
- 当未命中缓冲数据页时,需要走磁盘载入数据页,此时内存不够的情况下,会淘汰链表末尾的数据页。
假设此时LRU
链表由8
个缓冲页组成,并且此时缓冲区空间已满
此时假设一条查询语句,命中了其中的第6
个数据页,此时这个数据页会被挪到最前面:
此时又来了一条SQL
,要操作的数据在缓冲区不存在,因此会从磁盘读取数据并载入内存,但因为目前缓冲区已经满了,所以需要淘汰一个缓冲页,用来存放载入的新数据,此时就会将末尾的数据页淘汰
5:末位淘汰的问题
5.1:预读失效问题(young, old解决)
因为局部性原理,MySQL
在读取数据时,也就是读取一个数据时,默认会将其附近的16KB
数据一次性全部载入内存
当数据载入内存后会分配一个缓冲页来存放,并且会将相应的数据页放在LRU
链表的最前面,而这个数据页一共是有16KB
数据的,也就意味着里面会有多行表数据,假设此时程序只读取了这页数据中的一行记录,对于其他数据并不需要读取,这也就是所谓的预读失效问题
同时,MySQL
读取数据时,除开会利用局部性原理将一整页数据载入内存外,InnoDB
也有自己的预读机制
InnoDB
并未采用最基本的末尾淘汰算法,而是对其做了些许优化,会将整个LRU
算法划分为old、young
两个区域组成。
young、old
两个区域在LRU
链表中的占比,默认为63:37
,你也可以通过innodb_old_blocks_pc
这个参数,来手动调整old
区在整个LRU
链表中的占比。
默认不改的情况下,假设LRU
链表中由100
缓冲页构成,那么前63
个属于young
区,后37
个属于old
区
LRU
链表被划分为两个区域后,从磁盘中预读的数据页,就只需要加入到old
区域的头部,当这个数据页被真正访问时,才会将其插入young
区的头部。
如果预读的这页在后续一直没有被访问,就会从old
区域移除,从而不会影响young
区域中的热点数据。
也就是说,在划分为两个区域后,
young
区域是用来存储真正的热点数据页,而old
区则是用来存放有可能成为热点数据页的“候选人”当需要淘汰缓冲页时,会优先淘汰
old
区中的数据页,毕竟young
区中留下的都是久经考验的精英!
5.2:缓冲池污染问题(young区晋升解决)
什么是缓冲池污染问题
假设一条线程在执行SQL
语句,目前是需要查询一张百万级别的所有表数据,由于Buffer Pool
空间有限,所以如果按照原本的淘汰规则来清理内存,这次查询过程可能会导致Buffer Pool
里面的所有热点数据全部被换出。
等这次查询结束后,内存中只剩下了这次查询载入的数据页,当有线程访问原本哪些热点数据时,由于缓冲区中的数据页被换出了,因此就会产生大量的磁盘IO
。
上述这个过程,则被称之为Buffer Pool
污染问题。
⚠️ 并不是需要查询大量结果才会导致这个问题出现,而是当扫描的数据过多时,都会引发此问题,比如典型的对大表执行了全表扫描
young
区晋升限制
在JVM
中,为了防止新生代过早晋升年老代,从而频繁触发FullGC
的问题,在设计时也有晋升条件限制
默认情况下,一个对象只有达到了15
岁之后,才能从新生代晋升年老代,毕竟能够熬过16
轮新生代GC
的对象,也绝对不会无缘无故突然挂掉。
而InnoDB
中的young
区晋升限制,同样是这个原理
加入了young
区的晋升限制后,就能有效避免这种访问一次的数据页过早进入young
区
InnoDB
加了一个停留时间的限制,如果一个数据页想从old
区晋升到young
区,必须要在old
区中存活一定时间,这个时间默认为1s/1000ms
也可以通过参数innodb_old_blocks_time
调整。
由于存在这个时间限制,所以old
区的数据页,想要进入young
区,就必须达成两个条件:
- 在
old
区中停留的时间超过了1000ms
。 - 在
old
区中,一秒后有线程再次访问了这个数据页。
通过这种晋升限制的方式,就能完美的解决全表扫描引起的缓冲池污染问题,这也是InnoDB
最终的淘汰机制
当一个缓冲页的数据被淘汰后,也就是一个缓冲页的数据被清空后,会将其再次加入到Free
空闲链表中等待分配。