InnoDB存储引擎执行原理深度剖析(3)
在InnoDB中执行更新操作
此时我们需要的数据已经从磁盘中加载到缓冲池中了,下一步当然就是执行更新操作了:先对需要更新的那行数据加锁、原始数据写一份到redo log中便于可能的回滚操作、执行update操作,此时缓存页的数据就被更新了,当然就和磁盘中的数据页的数据就不一致了,这样的缓存页我们称之为脏页,如下图所示
那么,如何才能知道缓冲池中,那些缓存页是脏页呢?如果能把脏页和空闲缓存页分离出来,我们就可以把那些脏页的数据及时给刷到磁盘中、再释放掉脏页内存,在内存不够的情况下不就可以重复利用了吗。这里InnoDB的设计方法类似free链表,设计了一个flush链表,也就是那些在缓冲池中被更新过数据的缓存页,这些缓存页的描述信息都会被添加到flush链表中(这里提到的free链表、lru链表、flush链表都是双向循环链表,且节点都为缓存页的描述信息,其中flush链表的节点同时也在lru链表中),如下图所示:
缓冲池内存不足触发脏页刷盘
经过以上流程执行了一段时间后,直到InnoDB缓冲池中的内存即将不够用了,此时如果再来一条sql语句的更新操作,要想成功把磁盘中的数据加载到缓存页中,就需要先清理下内存中的缓存页了。
通过之前提到的lru链表,可以找到lru链表表尾的节点,这些节点之所以在表尾,是因为基本上没什么人访问它们,那它们在内存不够用的场景下,当然要被优先给清理掉啊;因为flush链表的节点也在lru链表中,此时在缓存页清理时需要做一个简单的判断:若缓存页既在lru表尾的节点同时也在flush链表中,就需要先把脏页给刷盘了,然后再释放掉缓存页的内存,保证那些事务修改的数据能够落库;若缓存页不在flush链表,那更简单直接释放缓存页内存,然后将这些释放完内存缓存页的描述信息,重现给添加到free链表中,完成一次大的循环(free链表->lru链表->flush链表->free链表),如下图所示:
mysql的预读机制带来的问题以及优化后的lru链表对该问题的解决
mysql预读机制可能会扰乱我们之前设想的lru链表的处理逻辑。当一个数据页被加载到缓冲池中时,可能顺带会把其他无关紧要的数据页也加载到缓冲池中,这些顺带加载到内存的数据页,它们往往被访问的频率是非常低的,但是由于lru链表的特点,新加入的总是会优先被排在lru的链表头,导致这些顺带进来的、访问频率比较低的缓存页排在比较靠前的位置,导致free链表不够时,lru链表反而会把那些本来访问频率较高、但是此时被排挤到lru链表尾的缓存页给刷盘清理了,这是很不合理的。
优化后的lru链表主要引入了冷热数据分离的思想解决了mysql预读机制带来的问题。把lru链表分为热数据区和冷数据区,热数据区主要存放那些访问频率高的缓存页,冷数据区存放访问频率较低的缓存页;从磁盘加载数据到lru链表时,首先会将加载到的缓存页直接先放到冷数据链的表头,如果1000ms(默认,可配置)后冷数据的缓存页又被访问了,此时就认为这些1000ms之后被访问的缓存页,在不久的未来可能还会被访问,可以认为它们是热数据了,就会把这些缓存页从冷数据区的链表给移动到热数据区链表的表头,通过该步骤可以将热数据从冷数据堆中给巧妙的分离出来,如下图所示:
此时如果要加载其他数据页发现缓冲池内存不够,实际上后台一直会有一个线程开启的一个定时任务,不断的从lru链表的尾部将缓存页给刷到磁盘中并释放缓存页,lru链表冷热数据分离的设计,确保了定时任务从lru链表尾部回收的缓存页都是访问频率很低的数据,对性能的影响也就降到了最低。