InnoDB存储引擎执行原理深度剖析图解

359 阅读9分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

一、磁盘数据如何加载到mysql中


一般更新一条数据,数据一开始是存放在磁盘中的,用到时才会被加载到mysql,存放的数据在逻辑概念上称为表,物理层面上在磁盘中是按数据页形式存放的,那么加载到mysql中的就称为缓存页。

每个缓存页都有对应的一份描述信息,存放了缓存页的一些元数据相关的一些信息,通过描述信息可以快速定位到缓存页,最开始描述信息指向的缓存页当然都是空闲没有数据的。

yuque_diagram.jpg 1.1、如何判断当前数据页是否已经加载到mysql中了?
当一条update语句执行时,通过sql语句中的数据库名和表名解析可以知道需要加载的数据页处于哪个表空间,根据sql语句本身也可以通过一致性算法得数据页号,根据数据页号和表空间号,可以从数据页缓存中(本质也就是一个哈希表)得到对应缓存页地址,通过缓存页地址直接就可以到InnoDB的缓冲池中定位到缓存页;如果数据页还没有加载过,缓存页地址是不存在,此时就需要从磁盘中加载数据页到mysql中。

yuque_diagram (1).jpg

1.2、如何知道哪些缓存页是空闲的?
引入了free链表数据结构,将那些还没有被使用的缓存页的描述信息用双向循环链表给组合在一起,需要用到时就卸一个节点出来存放数据页信息。

此时数据页被加载到缓存页,缓存页中已经有数据了,相关的变动信息肯定也是要回写到描述信息中,并且现在因为缓存页已经有数据,就不能再待在free链表,就需要将该缓存页对应的描述信息节点从free链表给移出,转移到lru链表中

yuque_diagram (2).jpg lru链表实现的目的就是为让哪些被访问的缓存页能够尽量排到靠前位置,那么如果此时内存不够需要淘汰掉一些缓存页时,此时就可以到lru链表尾部,将哪些最近最少被访问的尾部节点给刷盘释放缓存页腾出内存来。

二、在InnoDB中执行更新操作


数据已经从磁盘中加载到缓冲池中了,执行更新操作,先对需要更新的那行数据加锁,原始数据写一份到undo log中便于可能的回滚操作,执行update操作,此时缓存页的数据就被更新了。此时就和磁盘中的数据页的数据不一致了,这样的缓存页称为脏页

yuque_diagram (3).jpg

如何才能知道缓冲池中那些缓存页是脏页?
设计了一个flush链表,也就是那些在缓冲池中被更新过数据的缓存页,这些缓存页的描述信息都会被添加到flush链表中(free/lru/flush链表都是双向循环链表,且节点都为缓存页的描述信息,其中flush链表的节点同时也在lru链表中)

yuque_diagram (4).jpg


三、缓存池内存不足触发脏页刷盘

InnoDB缓冲池中的内存即将不够用时,可以找到lru链表表尾的节点,因为尾部的访问少或者不再被访问,因办flush链表的节点也在lru链表中,此时在缓存页清理时需要判断:若缓存页既在lru表尾的节点同时也在flush链表中,就需要先把脏页给刷盘了,然后再释放缓存页的内存,保证那些事务修改的数据能够落库;若缓存页不在flush链表,那直接释放缓存页内存,然后将这些释放完内存缓存页的描述信息,重现给添加到free链表中。

yuque_diagram (5).jpg

四、mysql的预读机制带来的问题以及优化后的lru链表


mysql预读机制,当一个数据页被加载到缓冲池中时,可能顺带会把其他无关紧要的数据页也加载到缓冲中,这些顺带加载到内存的数据页,它们被访问的频率是非常低的,但是由于lru链表的特点,新加入的总是会优先被排在lru的链表头,导致这些顺带进来的,访问频率比较低的缓存页排在比较靠前的位置,导致free链表不够时,lru链表反而把那些本来访问频率较高,但是此时被排挤到lru链表尾的缓存页给刷盘清理了。

优化后的lru链表主要引入了冷热数据分离的思想解决了mysql预读机制带来的问题,把lru链表分为热数据区和冷数据区,热数据区主要存放那些访问频率高的缓存页,冷数据区存放访问频率较低的缓存页;从磁盘加载数据到lru链表时,首先会将加载到的缓存页直接先放到冷数据链表头,如果1000ms(默认,可配置)后冷数据的缓存页又被访问了,此时就认为这些1000ms之后被访问的缓存页,在不久的未来可能还会被访问,可以认为它们是热数据了,就会把这些缓存页从冷数据区的链表给移动到热数据区链表的表头,通过该步骤可以将热数据从冷数据堆中给分离出来。

yuque_diagram (6).jpg

此时如果要加载其他数据页发现缓冲池内存不够,实际上后台一直会有一个线程开启的一个定时任务,不断的从lru链表的尾部将缓存页给刷到磁盘中并释放缓存页,lru链表冷热数据分离的设计,确保了定时任务从lru链表尾部回收的缓存页都是访问频率很低的数据,对性能的影响也就降到了最低。

五、思考

5.1、数据页和缓存页是什么?怎么知道哪些缓存页是空闲的,哪些缓存页是可以被清除的?
  • 在逻辑层面上,数据通常以表的形式展示,但是体现在具体存储的物理层面上,在磁盘存的数据是以数据页的方式一页一页存储的,每个数据页有多行数据
  • InnoDB存储引擎中,通过free链表可以查看到当前内存中,那里缓存页是空闲的,即可直接拿来存放新的数据;而此时如果free链表用完了,flush链表则记录了哪些缓存存放的是脏数据,可以被刷盘清空缓存页。
5.2、mysql预读机制是什么,什么情况下会触发,mysql是为了应对什么样的场景才设计预读机制?


mysql预读机制的设计初衷是为了提高性能,之所以在加载完一个数据页到内存的同时,会连带着把相邻其他数据页也加载到内存中,是考虑到后续其他操作可能会用到,所以在本次加载数据页时把其他的数据页也一并加载到内存。

预读机制的触发时机和参数配置有关:

  • innodb_read_ahead_threshold: 56(默认)
  • innodb_random_read_ahead: OFF(默认)

第一个参数:当前加载数据页的数据区中(数据区中存放多个数据页)按顺序已经访问了56个数据页了,此时mysql就会自动分析可能即将会访问下一个数据区的数据,此时就会触发将当前数据区的下一个数据区的数据全部都加载到内存中;

第二个参数:当前加载数据页的数据区中,如果以按顺序并且连续访问了13个数据页,此时mysql就会自动分析可能要按顺序把当前数据区中的数据全部给加载到内存中,就触发预读机制把当前数据区中的所有数据页一次加载到内存中。

预读机制一般是为了应对全表扫描以及分页查询操作,这些操作有大量的数据页加载到缓存的操作,为了不频繁从磁盘加载数据而设计将磁盘中的数据预加载到内存,从而达到提高性能的优化目的。

5.3、类比redis在内存中也存在冷热数据共享的场景,如何考虑利用lru链表解决预读机制的思想,来对redis缓存的设计进行优化?


场景: 在高并发场景下,redis缓存由于一些不可抗力因素大面积宕机导致数据彻底无法恢复、新系统第一次上线中途宕机了,在这些情况下如果重启redis就算成功了,此时redis内存也是没有数据的,在随之面对即将到来的千万级甚至亿级的流量打过来,redis会承受不了,涌来的流量肯定会打到mysql中,导致mysql挂掉。

优化:可以在日常系统正常运行时,计算哪些数据的访问量比较高然后存放起来,比如把每次访问的数据都上报一份到MQ中,然后通过一些实时计算的框架如Storm计算出访问量最多的前几位的热数据信息,把他们的主键信息存入如Hbase\Zookeeper,然后每次系统启动时都先从Hbase\Zookeeper中找到存放的热数据信息的主键,再回库中去把数据查询出来写redis中,作为缓存预热。把这些最频繁访问的数据都加载到redis的内存中,也基本能抗住高并发访问。

5.4、内存极度不够用情况下,可能出现每当加载一个数据页就要把一个缓存页刷到磁盘中,出现双倍IO的性能问题,对于这种现象如何 考虑优化mysql内存参数来避免该情况的性能损耗?

主要原因是buffer pool的内存大小和数据设置的不太合理,不满足当前业务系统的并发压力。buffer pool内存设置的比较小,导致free链表经常空闲的缓存页不足而频繁找到flush链表刷盘;
另外每个线程都来操作buffer pool都是要上锁的,虽然buffer pool执行时的效率高,但是实际的多线程并发的串行化执行还是比较影响性能。

可配置参数innndb_buffer_pool_size增加innodb的内存,并通过参数innodb_buffer_pool_instances多配置几个buffer pool实例来负载均衡一下多线程并发的压力,提高innodb存储引擎整体的性能。