1、Buffer pool
MySQL作为一个存储系统,为了提高性能,减少磁盘io,设计了缓冲池机制(buffer pool),innodb的存储结构组成如下图:
buffer pool作为innodb内存结构的四大组件之一,不属于mysql的server层,是innodb存储引擎的缓冲池。
1.1 什么是buffer pool
buffer pool是一块内存区域,是一种“降低磁盘访问机制”,buffer pool缓存数据表和索引数据,吧磁盘上的数据加载到缓冲池,避免每次访问都进行磁盘IO,起到加速访问的作用。buffer pool也是以页为存储单位(与磁盘中数据页索引页单位一样,默认大小16K),buffer pool底层采用链表的数据结构管理page。
所有数据页的读写操作都要通过buffer pool进行
- innodb的读操作:先从buffer pool中查看数据的数据页是否存在,如果不存在,则将数据页先从磁盘读到buffer pool中然后在读
- innodb的写操作:先把数据和日志写入buffer pool和log buffer,再由后台线程以一定的频率将buffer中的内存刷到磁盘。
写操作的事务持久性由redo log落盘保证,buffer pool只是为了提高读写效率。
1.2 buffer pool的控制块
buffer pool中缓存的是数据页,数据页大小和磁盘默认数据页大小一样,为了更好的管理缓存页,buffer pool有个“描述数据的区域”:
innodb为每一个缓存的数据页都创建了一个单独的区域,记录数据页的元数据信息,包括页所属的表空间,数据页编号,缓存页在buffer pool中的地址,链表节点信息,锁信息以及LSN信息等,成为控制块。
控制块和缓存页一一对应,都是存在buffer pool中,控制块大概占缓存页的5%左右。
1.3 buffer pool是如何管理的?
buffer pool里面有三个链表,LRU链表,free链表,flush链表,innodb就是通过这三个链表的使用来控制管理数据页。
1.3.1 buffer pool 的初始化
启动mysql服务器的时候,需要完成对buffer pool的初始化过程,即分配buffer pool空间,划分为若干控制块和缓存页。
- 申请空间:根据设置的buffer pool大小(innodb_buffer_pool_size)去申请一块连续的内存区域作为buffer pool区域。(实际申请的会比innodb_buffer_pool_size设置的值大一些,因为会额外申请内存空间去存储控制块)
- 划分空间:按照默认缓存页大小16KB和对应的800字节左右的控制块,在buffer pool中划分若干“控制块&缓冲页”对。
划分空间后buffer pool中缓存页都是空的,后续对数据操作时,才会从磁盘中读出数据存放在buffer pool。
1.3.2 free链表
free链表即空闲链表,解决的问题就是从磁盘读取数据页到buffer pool时,如何判断buffer pool 中哪些数据页是空的,可以用来缓存数据。
free链表是一个双向链表,由一个基节点和若干子节点组成,记录空闲的数据页对应的控制块信息。
- 基节点:单独申请的内存空间约40字节,不在buffer pool的连续内存空间里面,它包含链表中子节点的头节点和尾节点地址,以及当前链表中节点数据等信息
- 子节点:每个子节点就是空闲缓存页的控制块,即只要一个缓存页空闲,对应的控制块就会放到free链表中,每个控制块都有两个指针。
通过free链表,可以知道buffer pool中哪些缓存页可以用来缓存数据。那么如何确定数据页是否需要被缓存呢?(即如何知道数据页已经在buffer pool中了,不需要在去磁盘读取了)
数据库提供了一个数据页缓存哈希表,表空间号+数据页号 作为key,缓存页控制块地址为value。使用数据页时,先在数据页哈希表中去查找,如果找到了,直接根据value定位控制块,然后找缓存页,如果没有找到,则读取磁盘数据页写入缓存,然后在加入哈希表。
1.3.3 LRU链表。
首先考虑两个问题:
- 磁盘读取数据页到buffer pool时,会将对应的控制块从free链表中移除,移除之后的控制块放在那里呢?
- buffer pool默认大小128M,当加载满了之后,新的数据要加载怎么处理?
这两个问题是借助LRU链表解决的:
buffer pool作为innodb自带的缓冲池,读写都是在其中进行的,但是buffer pool的大小是有限制的,因此希望频繁访问的数据可以长时间留在buffer pool中,而访问较少的数据,能够优先释放掉为其他数据页留出缓存空间。基于这个目的,采用了LRU链表 ---> 频繁访问的数据放在链表头部,不怎么访问的数据放在链表末尾,空间不够的时候就从尾部开始淘汰,从而为其他数据腾出空间。LRU链表本质也是由控制块组成。
传统的LRU会有一定的问题:假如执行了一次全表扫描 select * 。。。 这样会导致大量的数据加载到buffer pool里面,从而把原本的热数据被淘汰,下次访问,又需要重新读取磁盘。因此mysql对LRU进行了改进:
- young区域,位于链表头部(5/8),存放热点数据
- old区域,位于链表尾部(3/8),存放非热点数据
每次有数据被加载到buffer pool后,先插入到old区域的头部,并标记第一次访问的时间,后续如果在访问到当前页,并且访问时间间隔大于设置的间隔时间innodb_old_blocks_time(默认1s),才会把当前页给提高到young区域。通过这种方式,就避免了全部扫描导致的只访问一次的数据页覆盖掉热点数据的问题。
此外,因为热点区域的数据被频繁访问,如果young区域每次访问某一页,就把当前页移动到young区域的head,会导致LRU链表频繁的变形,因此mysql又做了一个优化:young区域前1/4被访问不会被移动到头部,只有后面的3/4被访问才会移动到头部。
1.3.4 flush链表
flush链表和free链表结构很类似,也是由基节点和子节点租出的双向链表,链表节点时被修改过的缓存页对应的控制块。
flush链表的作用是:帮助定位脏页,即需要刷盘的缓存页,当控制块被加入到flush链表后,后台线程就可以遍历flush链表,将脏页写入磁盘。 ---- 但是这只是被更新数据已经加载到buffer pool的前提,如果没有预先加载,则先记录在change buffer中。
1.3.5 buffer pool中的数据页
- free page 空闲页,表示此数据页未被使用,控制块位于free链表
- clean page 干净页,表示此数据页已经被使用,缓存了数据,控制块位于LRU链表
- dirty page 脏页,表示此数据页已经被使用并且被修改,数据页中的数据和磁盘中的数据不一致,脏页的控制块同时存在于LRU和Flush链表。当脏页上的数据被写入磁盘后,内存数据和磁盘数据一致,脏页也就变成了干净页。
2.Log buffer
作用:优化每次更新操作之后都写入redo log而产生的磁盘IO问题。
当buffer pool中某页的数据更改后,这个页就变成了脏页(因为mysql为了提升性能,避免频繁的随机IO,每次更改数据不是第一时间去更改磁盘数据,这就导致了buffer pool中的数据和磁盘数据不一致)
为了保证脏页落盘的安全性,防止断电丢失等,会预写redo log日志(磁盘中)。---随机IO变成顺序IO,效率提升
但是如果每次更新操作都写入redo log日志,虽然是顺序IO,但是频繁IO也影响性能,因此在内存中开辟了一个log buffer区域,用于缓存要写入redo log的数据,在事务提交的时候将log buffer里面的数据进行持久化。----(mysql默认的策略,但是也可以更改)
innodb_flush_log_at_trx_commit 参数
Log buffer的持久化策略如下,可以通过参数进行调节 innodb_flush_log_at_trx_commit 进行调节:
- 0:每次事务提交时,不会立即对redo log 持久化,这个任务交给后台线程做(事务提交快)
- 1:默认的,每次事务提交的时候,立即把redo log持久化(数据安全)
- 2:事务提交时,立刻log buffer的数据写到操作系统的缓冲区,但是不会立即持久化redo log,而是后台线程去持久化 (这种,即便mysql挂了,但是如果操作系统不挂,数据也不会丢失)
3.Change buffer
在MySQL5.5之前,叫插入缓冲(Insert Buffer),只针对INSERT做了优化;现在对DELETE和UPDATE也有效,叫做写缓冲(Change Buffer)。
它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(Buffer Changes),等未来数据被读取时,再将数据合并(Merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO,提升数据库性能。
1、mysql的写操作如何减少IO次数?--- 引出change buffer
mysql的读请求可以通过缓冲池减少与磁盘IO,提示性能,那么写请求是否也可以呢?
情况1:要修改的页4刚好在缓冲池内:
处理过程
- 直接修改缓冲池中的页,一次内存操作
- 写入redo log日志,一次磁盘顺序操作
是否会有一致性问题?
不会
- 读取,直接命中缓冲池中的页
- 缓冲池的LRU数据淘汰,会将脏页刷回磁盘
- 数据库异常崩溃,从redo log中恢复数据
情况2:要修改的页不在缓冲池内:
可以先把要修改的页读到缓冲池,然后执行情况1操作,而change buffer就是为了优化这种场景。
处理过程:
- change buffer中记录这个写操作,一次内存操作
- 写入redo log,一次磁盘顺序操作。
是否会有一致性问题?
不会
- 数据库异常崩溃,能从redo log中恢复数据
- changebuffer 会定期刷盘,到 写缓冲系统表空间
- 数据读取有另外的流程,将数据合并到缓冲池。
2、后续读取change buffer 涉及到的页数据操作?
上述的场景2中,通过change buffer修改了没在缓冲池中的页,现在要读取这个页,操作如下:
- 查询,缓冲池未命中,从磁盘拷贝这个页 (不可避免)
- 从change buffer中读取这个页的相关信息
- 数据合并(merge),存至缓冲池中。
3、change buffer 只适用于非唯一普通索引页?
如果索引设置了唯一属性,在进行修改操作的时候,innodb必须进行唯一性检查,也就是说,即便索引页不在缓冲池中,磁盘的读取也是不可避免的。此时就应该直接把相应的页放入缓冲池然后进行修改,而没有必要进行“写缓冲”这多余操作。
4、change buffer中数据刷新的触发时机?
- 相关数据页被访问
- 后台线程,在数据库空闲时
- 数据库缓冲池不够用
- 数据库正常关闭
- redo log写满
5、change buffer适用场景?
- 数据库大部分是非唯一索引
- 业务写多读少,或者不是写后立即读取。
总结
1、Change Buffer是一种特殊的内存结构,它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(Buffer Changes),等未来数据被读取时,再将数据合并(Merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO,提升数据库性能;
2、如果索引设置了唯一(Unique)属性,在进行修改操作时,InnoDB必须进行唯一性检查,是不能使用到Change Buffer的;
3、根据业务使用情况,可以指定Change Buffer占整个缓冲池的比例。同时,也可以指定配置哪些写操作启用写缓冲,可以设置成all/none/inserts/deletes等;
4、Change Buffer不会出现数据不一致的情况,原因有二:
-
数据库异常奔溃,能够从redo log中恢复数据;
-
写缓冲不只是一个内存结构,它也会被定期刷盘到写缓冲系统表空间。
4.Doublewrite Buffer
一般情况下,Linux文件系统的页大小是4KB,而我们知道 mysql的页大小为16KB(oracle为8KB),这就说明,mysql将buffer pool中的一页数据刷到磁盘里面,需要写4个文件系统页。但是这个操作并不是原子性的,如果写到一半断电,就会发生“页数据损坏”。
redo log无法修复这类页数据损坏异常。(修复的前提是页数据正确并且redo日志正常。)
double write buffer 和传统的buffer不同,它分为内存和磁盘两层架构。工作流程如下:当有页数据要刷盘时:
- 页数据先copy到内存中的DWB中
- DWB的内存中的数据页,会先刷到DBW的磁盘上
- DWB内存中的数据页,在刷到数据磁盘的存储文件上 (.ibd文件)
doublewrite buffer 是如何解决页数据损坏问题?
- 假设步骤2 掉电,磁盘中依旧是完整数据页,只要有数据页完整就可以通过redolog还原数据
- 假设步骤3 掉电,DWB磁盘结构中存储着完整数据,所有不会出现数据丢失。
- 步骤2和步骤3写了两次磁盘,因此叫doublewrite!
两次磁盘写会不会影响性能?
- 内存copy到DWB的内存区域,速度很快
- DWB的内存写到DWB的磁盘,属于顺序追加写,速度也快
- 刷磁盘,随机写,但是本来就需要进行。
- 另外,128页工2M的DWB,会分两次刷入磁盘,每次最多64页,数据量小,执行快,因此综合来看,虽然性能有影响,但是影响不大。
总结:
- 1个MySQL的页(Page)可以映射4个操作系统(文件系统)的页(OS Page),Doublewrite Buffer是为了保证因系统页损坏导致的MySQL数据丢失的保证方案;
- Doublewrite Buffer是内存+磁盘结构,内存结构是由128个页组成,大小为16KB × 128 = 2MB,“Double”的由来是因为数据写两次磁盘:一次是写DWB的磁盘结构(此过程分两次写入,每次写入16KB × 64 = 1MB的数据),一次是直接写入Data File(.ibd);
- MySQL 8.0较MySQL 5.6、5.7版本的Doublewrite Buffer产生了一些新的变化,DWB磁盘结构的数据存放从共享系统表空间中分离出来,存放在单独的.dblwr文件中;
- MySQL 8.0新增了一些涉及Doublewrite Buffer的参数,可以更细粒度的管理Doublewrite Buffer,但新增参数多用于高级性能调整,我们使用默认值即可,无需更多关注。