INNODB存储引擎之缓冲池
以下的资料总结自:官方文档和《MySQL技术内幕-INNODB存储引擎》一书。
对INNODB存储引擎缓冲池的那一段描述来自博文:http://www.ywnds.com/?p=9886 说句实话这片博文写的很清楚,通过问答形式加紧逻辑性!
这篇文字会详细的说明INNODB存储引擎的体系结构及特性。
- INNODB存储引擎的内存管理
- Checkpoint技术
- INNODB存储引擎的关键特性
- 插入缓冲
- DOUBLEWRITE
- AHI(自适应哈希索引)
- 异步IO
- 刷新临近页
INNODB的体系结构
首先通过一张图来说明INNODB存储引擎的体系结构。
INNODB存储引擎有多个内存块,这些内存块组成了一个大的内存池。(innodb_buffer_size)
INNODB的后台线程主要作用:刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。 其二:将已修改的数据文件刷新到磁盘文件,同时保证数据库发生异常的情况下INNODB能恢复到正常状态。
在上一篇博文中说明了INNODB存储引擎后台线程的具体作用: CLICK HERE!
上面的图片只是简单地说明了INNODB存储引擎的作用,下面是一张详细的图,说明了INNODB在具体是怎么样做这些事的!(图片来自官方网站的MySQL5.7的文档)
下面我们会详细的说明这张图的具体是怎么工作的!
缓冲池
INNODB存储引擎是基于磁盘存储的,并将其记录按照页的方式进行管理。在数据库系统中由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲技术来提高数据的整体性能。
缓冲池简单来说就是一块内存区域.通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中(这个过程称作FIX),下一次读取相同的页时,首先判断该页是不是在缓冲池中,若在,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
对于数据库中页的修改操作,首先修改在缓冲池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘,而是通过一种叫做checkpoint的机制把页刷新会磁盘。
因此数据的操作都是对缓冲池进行的操作,而不是磁盘。
缓冲池的大小设置:
缓冲池配置可以通过INNODB_BUFFER_POOL_SZIE来设置,官方文档建议,缓冲池的大小最多应设置为物理内存的80%,正常使用可以设置为(50%~80%)之间。
缓冲池是一块内存,INNODB存储引擎是通过页的方式对这块内存进行管理的。缓冲池中存储的页有: 索引页,数据页,插入缓冲,自适应哈希索引(AHI),INNODB存储的锁信息,数据字典信息等。缓冲池中的索引页和数据页只是占据了缓冲池的很大一部分而已。如图(图片地址)
从INNODB1.0.x开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同的缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库并发处理能力。
innodb_buffer_pool_instances: 设置有多少个缓冲池。通常建议把缓冲池个数设置为CPU的个数。
在使用show engine innodb status\G的时候会以---BUFFER POOL 5的形式分别标识每一个bp,所有的bp会均分INNODB_BUFFER_POOL_SZIE的大小。
缓冲池的管理
缓冲池的结构描述(或者组织形式,不太准确):
我们已经知道这个Buffer Pool其实是一片连续的内存空间,那现在就面临这个问题了:怎么将磁盘上的页缓存到内存中的Buffer Pool中呢?直接把需要缓存的页向Buffer Pool里一个一个往里怼么?不不不,为了更好的管理这些被缓存的页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号(space id)、页号(page number)、页在Buffer Pool中的地址,一些锁信息以及LSN信息(锁和LSN这里可以先忽略),当然还有一些别的控制信息。
每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:
控制块和缓存页之间的那个碎片是个什么呢?你想想啊,每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为碎片了。当然,如果你把Buffer Pool的大小设置的刚刚好的话,也可能不会产生碎片~
前面我们知道了缓冲池的结构。接下来说InnoDB存储引擎是怎么对缓冲池进行管理的?
当我们最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是分配Buffer Pool的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到Buffer Pool中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中,那么问题来了,从磁盘上读取一个页到Buffer Pool中的时候该放到哪个缓存页的位置呢?或者说怎么区分Buffer Pool中哪些缓存页是空闲的,哪些已经被使用了呢?我们最好在某个地方记录一下哪些页是可用的,我们可以把所有空闲的页包装成一个节点组成一个链表,这个链表也可以被称作Free链表(或者说空闲链表)。因为刚刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每一个缓存页都会被加入到Free链表中,假设该Buffer Pool中可容纳的缓存页数量为n,那增加了Free链表的效果图就是这样的:
从图中可以看出,我们为了管理好这个Free链表,特意为这个链表定义了一个控制信息,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。我们在每个Free链表的节点中都记录了某个缓存页控制块的地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个Free链表节点都对应一个空闲的缓存页。
有了这个Free链表事儿就好办了,每当需要从磁盘中加载一个页到Buffer Pool中时,就从Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的Free链表节点从链表中移除,表示该缓存页已经被使用了,并且把改页写入LRU链表!
不要因为走的太远而忘记为什么出发。
简单回顾一下,为什么讲free list?是为了讲怎么管理buffer pool对吧。那free list就相当于是数据库服务刚刚启动没有数据页时,维护buffer pool的空闲缓存页的数据结构。
下面再来简单地回顾Buffer Pool的工作机制。Buffer Pool两个最主要的功能:一个是加速读,一个是加速写。加速读呢? 就是当需要访问一个数据页面的时候,如果这个页面已经在缓存池中,那么就不再需要访问磁盘,直接从缓冲池中就能获取这个页面的内容。加速写呢?就是当需要修改一个页面的时候,先将这个页面在缓冲池中进行修改,记下相关的重做日志,这个页面的修改就算已经完成了。至于这个被修改的页面什么时候真正刷新到磁盘,这个是后台刷新线程来完成的。
在初始化的时候,bp中所有的页都是空闲页(也就是free list的页),需要读数据时,就会从free链表中申请页,因为物理内存不可能无限增大,但是数据库的数据却是在不停增大的,所以free链表的页是会用完的,这时候应该怎么办?这时候我们可以考虑把已经缓存的页从bp中删除一部分,那么究竟采用什么样的方式来删除,究竟该删除哪些已经缓存的页?
为了回答这个问题,我们还需要回到我们设立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列表的热端。可以通过设置该参数保证热点数据不轻易被刷出。
【free 链表是空的,数据库刚初始化的时候产生的,当需要读取数据时,会从free list中申请一个页,把从放入磁盘读取的数据放入这个申请的页中,这个页的集合叫LRU链表】
上面说到了读数据,下面说明写数据:
前面我们讲到页面更新是在缓存池中先进行的,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名: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,每个页头部记录)进行排序刷新到磁盘的,值越小表示要最先被刷新,避免数据不一致。
【理解脏页的概念?脏页是bp中被修改的页,脏页寄存在与lru链表中,也存在与flush链表中,flush链表中存在的是一个指向lru链表中具体数据的指针。因此只有lru链表中的页第一次别修改时,对应的指针才会存入到flush中,若以后再修改这个页,则是直接更新对应的数据。】
这三个重要列表(LRU list, free list,flush list)的关系可以用下图表示:
Free链表跟LRU链表的关系是相互流通的,页在这两个链表间来回置换。而FLUSH链表记录了脏页数据,也是通过指针指向了LRU链表,所以图中FLUSH链表被LRU链表包裹。
缓存中页的定位:
我们前边说过,当我们需要访问某个页中的数据时,就会把该页加载到Buffer Pool中,如果该页已经在Buffer Pool中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在Buffer Pool中呢?难不成需要依次遍历Buffer Pool中各个缓存页么?一个Buffer Pool中的缓存页这么多都遍历完岂不是要累死?
再回头想想,我们其实是根据表空间号 + 页号来定位一个页的,也就相当于表空间号 + 页号是一个key,缓存页就是对应的value,怎么通过一个key来快速找着一个value呢?那肯定是哈希表了。
所以我们可以用表空间号 + 页号作为key,缓存页作为value创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从Free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
上面基本说明了bp是怎么工作的,接下来我们看一个实例的bp信息。
mysql> show engine innodb ststus;
....
BUFFER POOL AND MEMORY ---------------------- Total large memory allocated 5502402560 #总的内存是多少,字节为单位 Dictionary memory allocated 991733 #为InnoDB
数据字典分配的总内存 Buffer pool size 327680 #总的bp有多少个页,每个页默认大小为16K(innodb_page_size的数值) Free buffers 8192 #当数据库刚启动时,bp中没有数据,会含有许多16KB的块,这些块就是free buffer。当读取数据时,就从free list中申请一个块,然后把这个块放入lru列表中.Free buffers表示当前free列表页中的数量。 Database pages 490679 #表示的就是lru列表中的页,也就是数据页。(可能情况是free buffer+database pages的数量之和等于bp,因为缓冲池中还可能会被分配自适应哈希索引,lock信息,insert buffer等页,这部分页不需要lru算法维护,因此不存在lru列表中) Old database pages 180966 # lru列表中old部分的页数量 Modified db pages 0 # 脏页的数量。flush列表 Percent of dirty pages(LRU & free pages): 0.000 # Max dirty pages percent: 75.000 # Pending reads 0 # 等待读入缓冲池的缓冲池页数。 Pending writes: LRU 0, flush list 0, single page 0 # Pages made young 452994, not young 1694417 0.00 youngs/s, 0.00 non-youngs/s (将页从lru列表的old部分加入到new部分时,称此时的操作为page made young.而因为innodb_old_blocks_time的设置导致页没有从old部分移动到new部分的操作称为page not made young.--pages made young:显示了lru列表中页移动到前端的次数。young/s, non-young/s表示每秒这两类操作的次数。) Pages read 1436912, created 4603153, written 3896513 0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 490679, unzip_LRU len: 49074 I/O sum[0]:cur[0], unzip sum[0]:cur[0]
....
#这个页数据的整体介绍可以查看官方文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html
我们还可以使用统计表查看如下:
mysql> use information_schema;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select * from INNODB_BUFFER_POOL_STATS \G
*************************** 1. row ***************************
POOL_ID: 0
POOL_SIZE: 8191
FREE_BUFFERS: 7005
DATABASE_PAGES: 1186
OLD_DATABASE_PAGES: 448
MODIFIED_DATABASE_PAGES: 0
PENDING_DECOMPRESS: 0
PENDING_READS: 0
PENDING_FLUSH_LRU: 0
PENDING_FLUSH_LIST: 0
PAGES_MADE_YOUNG: 0
PAGES_NOT_MADE_YOUNG: 0
PAGES_MADE_YOUNG_RATE: 0
PAGES_MADE_NOT_YOUNG_RATE: 0
NUMBER_PAGES_READ: 1126
NUMBER_PAGES_CREATED: 60
NUMBER_PAGES_WRITTEN: 70
PAGES_READ_RATE: 0
PAGES_CREATE_RATE: 0
PAGES_WRITTEN_RATE: 0
NUMBER_PAGES_GET: 22583
HIT_RATE: 0
YOUNG_MAKE_PER_THOUSAND_GETS: 0
NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
NUMBER_PAGES_READ_AHEAD: 384
NUMBER_READ_AHEAD_EVICTED: 0
READ_AHEAD_RATE: 0
READ_AHEAD_EVICTED_RATE: 0
LRU_IO_TOTAL: 0
LRU_IO_CURRENT: 0
UNCOMPRESS_TOTAL: 0
UNCOMPRESS_CURRENT: 0
1 row in set (0.00 sec)
#这个表的字段信息和上面命令输出的信息都可以用来查看当前bp的统计信息
数据访问机制
1. 当访问的页面在缓存池中命中,则直接从缓冲池中访问该页面。另外为了避免查询数据页时扫描LRU,还为每个buffer pool instance维护了一个page hash,通过space id和page id可以直接找到对应的page。一般情况下,当我们需要读入一个Page时,首先根据space id(space id对应的是表)和page id找到对应的buffer pool instance。然后查询page hash,如果page hash中没有,则表示需要从磁盘读取。
2. 如果没有命中,则需要将这个页面从磁盘上加载到缓存池中,因此需要在缓存池中的空闲列表中找一个空闲的内存块来缓存这个从磁盘读入的页面。
3. 但存在空闲内存块被使用完的情况,不保证一定有空闲的内存块。假如空闲列表为空,没有空闲的内存块,则需要想办法去产生空闲的内存块。
4. 首先去LRU列表中找可以替换的内存页面,查找方向是从列表的尾部开始找,如果找到可以替换的页面,将其从LRU列表中摘除,加入空闲列表,然后再去空闲列表中找空闲的内存块。第一次查找最多只扫描100个页面,循环进行到第二次时,会查找深度就是整个LRU列表。这就是LRU列表中的页面淘汰机制。
5. 如果在LRU列表中没有找到可以替换的页,则进行单页刷新,将脏页刷新到磁盘之后,然后将释放的内存块加入到空闲列表。然后再去空闲列表中取。为什么只做单页刷新呢?因为这个函数的目的是获取空闲内存页,进行脏页刷新是不得已而为之,所以只会进行一个页面的刷新,目的是为了尽快的获取空闲内存块。
因为空闲列表是一个公共的列表,所有的用户线程都可以使用,存在争用的情况。因此,自己产生的空闲内存块有可能会刚好被其他线程所使用,所以用户线程可能会重复执行上面的查找流程,直到找到空闲的内存块为止。
通过数据页访问机制,可以知道其中当无空闲页时产生空闲页就成为一个必须要做的事情了。如果需要刷新脏页来产生空闲页面或者需要扫描整个LRU列表来产生空闲页面的时候,查找空闲内存块的时间就会延长,这个是一个bad case,是我们希望尽量避免的。因此,innodb buffer pool中存在大量可以替换的页面,或者free列表中一直存在着空闲内存块,对快速获取到空闲内存块起决定性的作用。在innodb buffer pool的机制中,是采用何种方式来产生的空闲内存块,以及可以替换的内存页的呢?这就是我们下面要讲的内容——通过后台刷新机制来产生空闲的内存块以及可以替换的页面。
checkpoint技术
当前事务数据库系统普遍采用了write ahead log策略,即当事务提交时,先写重做日志,再修改页。当由于数据库宕机而导致数据丢失时,通过重做日志来完成数据的恢复。
想一种情况,当重做日志变得很大时,数据库的恢复时间就会变得很长,恢复代价变得很大?
checkpoint技术主要目的是:
- 缩短数据库恢复时间
- 缓冲池不够用时,将脏页刷新到磁盘。
- 重做日志不可用时,刷新脏页。
当数据库发生宕机时,数据库不需要所有的重做日志,因为checkpoint之前的页都已经刷新回磁盘,故数据库只需要对checkpoint之后的重做日志进行恢复即可。
此外当缓冲池不够用时,根据lru算法会释放最近最少使用的页,若此页为脏页,那么就需要强制执行checkpoint,将脏页刷新回磁盘。
因为当前事务数据库系统对重做日志的设计都是循环使用的,在写入重做日志时,若这部分重做日志不可用【是因为数据库在宕机恢复时若需要这使用这部分日志,若此时想要使用这部分重做日志(前面的不可用状态的重做日志)】则必须强制checkpoint,将缓冲池中的页刷新到对应当前重做日志的位置。
若重做日志可以被重用的部分是指这些重做日志已经不再需要(缓冲池中的页和重做日志的位置吻合),那就可以直接覆盖。
在这里刷新缓冲池中页的时候,我们提到过,要把页刷新道道重做日志的位置,那么INNODB是怎么确定这些位置的?
INNODB使用LSN(log sequenct number)来标识页刷新的位置。【在前面的字节数上加上写入的字节数】
每个页都有对应LSN数值,重做日志有LSN,checkpoint也有LSN。
mysql> show engine innodb status\G
......
LOG
---
Log sequence number 293633237 #当前缓冲池中的lsn的值,也就是redo log的lsn
Log flushed up to 293633237 #当前磁盘中的lsn的值,是刷redo log file flush to disk中的lsn;
Pages flushed up to 293633237 #是已经刷到磁盘数据页上的LSN;
Last checkpoint at 293633228 #上一次刷新的LSN
.....
在INNODB存储引擎中,checkpint发生的时间,条件及脏页选择都很复杂,而checkpoint所做的事情就是把缓冲池中的脏页刷新到磁盘。不同之处在于每次刷新多少脏页以及什么时候出发checkpoint?【这是只是简单说明下】
在INNODB存储引擎内部有两种checkpoint方式:
- Sharp Checkpoint
- Fuzzy Checkpoint
Sharp Checkpoint发生在将数据库关机时将所有的脏页刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1.
但是若在数据库运行时也使用Sharp Checkpoint,那么数据库的性能就会受到影响。故在INNODB内部使用Fuzzy Checkpoint的刷新方式,即每次只刷新一部分脏页,而不是刷新所有的脏页。
在INNODB内部在发生如下情况时,会进行fuzzy checkpoint刷新。
Master Thread Checkpoint: 【异步刷新,每秒或每10秒从缓冲池脏页列表刷新一定比例的页回磁盘。异步刷新,即此时InnoDB存储引擎可以进行其他操作,用户查询线程不会受阻】 FLUSH_LRU_LIST Checkpoint:InnoDB存储引擎需要保证LRU列表中差不多有100个空闲页可供使用。在InnoDB 1.1.x版本之前,用户查询线程会检查LRU列表是否有足够的空间操作。如果没有,根据LRU算法,溢出LRU列表尾端的页,如果这些页有脏页,需要进行checkpoint。因此叫:flush_lru_list checkpoint。
InnoDB 1.2.x开始,这个检查放在了单独的进程(Page Cleaner)中进行,并且可以使用innodb_lru_scan_depth 参数控制LRU列表中可用页的数量,默认是1024!好处:1.减少master Thread的压力 2.减轻用户线程阻塞。 异步/同步 Checkpoint:重做日志不可用时,需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选择的。 脏页太多时强制checkpoint:脏页数量太多时,强制进行checkpoint,当缓冲池中脏页的数量占据超过innodb_max_dirty_pages_pct设定的值时,就进行强制刷新。默认数值是75%。
INNODB的关键特性
插入缓冲
INNODB在插入非聚集的非唯一性索引时,会随机插入的数据,这就会导致性能下降。因此INNODB采用了insert buffer来完成非聚集非唯一性索引的插入。当插入这些索引时,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个insert buffer对象中,好似欺骗。然后再以一定的频率进行insert buffer和辅助所引页子节点的合并操作,这时通常能将多个插入合并到一个操作中,这就大大提高了对于非聚集索引的插入性能。
insert buffer需要同时满足以下两个条件:
- 索引时辅助索引
- 索引不是唯一索引
辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找所引页来判断记录的唯一性。如果去查找肯定导致又忽悠离散型读情况发生,从而导致insert buffer失去了意义。
INSERT BUFFER AND ADAPTIVE HASH INDEX ------------------------------------- Ibuf: size 1, free list len 0, seg size 2, 0 merges
#size:表示已经合并记录页的数量,free list:表示空闲列表的长度,seg size显示当前insert buffer的大小, mergers:表示合并页的数量 merged operations: insert 0, delete mark 0, delete 0 discarded operations: #表示change buffer发生merge,表已经被删除,此时就无需再将记录合并到辅助索引中。 insert 0, delete mark 0, delete 0 Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) Hash table size 34673, node heap has 0 buffer(s) 0.00 hash searches/s, 0.00 non-hash searches/s
change buffer
INNODB从1.0.x开始引入了Change Buffer,从这个版本开始INNODB存储引擎可以对DNL操作---insert(insert buffer), delete(dlete buffer),update(purge buffer)都进行缓冲。
和之前一样change buffer的对象依然是辅助的非唯一索引。
对一条记录进行update操作包含两个过程:1将记录标记为删除,2:将记录删除。delete buffer对应update的第一个阶段,purge buffer对应update的第二个阶段。
同时INNODB存储引擎还提供了参数innodb_change_buffering ,用来开启各种buffer的选项。可选择的值如下:
inserts, deletes, purges, changes, all, none.
#changes:表示inserts和deletes。
#all:表示启用所用
#none:表示全部关闭。默认是all
在写密集的情况下,change buffer会占用过多的缓冲池资源,在INNODB1.2版本中可以使用innodb_change_buffer_max_size参数进行控制,默认数值是25,表示1/4.
inert buffer的内部实现:
【站位】
两次写
在数据库发生宕机时,可能INNODB存储引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,之后就发生了宕机,这种情况被称为部分写失效。
doublewrite由两部分组成,一部分是内存中的double buffer,大小为2M,另一部分是物理磁盘上共享表空间的连续的128个页,即2个区,大小为2M。在对缓冲池进行脏页刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页首先复制到内存中的doublewrite buffer。之后通过doublewrite buffer再分两次,每次1M顺利地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免写缓冲带来的问题。在这个过程中doublewriter是连续的,因此开销不大。