mysql底层原理
⼀般我们要更新⼀条数据,数据⼀开始肯定是存放在磁盘中的,⽤到时才会被加载到mysql,存放的数据在逻辑概念上我们称为表,物理层⾯上在磁盘中是按数据⻚形式存放的,那么加载到mysql中的就称为缓存⻚。
每个缓存⻚都有对应的⼀份描述信息,存放了缓存⻚的⼀些元数据相关的⼀些信息,通过描述信息可以快速定位到缓存⻚
那么从磁盘中加载⼀个数据⻚到mysql中真的就这么简单吗?会不会同⼀份数据⻚加载到mysql中出现重复加载的情况?如何快速知道当前数据⻚是否已经加载到mysql中了?
这时候可能很多⼈已经想到了:缓存。对于已经加载到mysql中的数据⻚,我们⼤可以设计⼀个缓存将加载过的数据⻚信息缓存⼀下,⼀⽅⾯可以防⽌同⼀份数据⻚重复加载到mysql,另⼀⽅⾯当我们需要使⽤到数据⻚的信息时,可以通过缓存信息快速定位mysql中对应的缓存⻚,没错,InnoDB存储引擎中就是按照这样的思路设计了⼀个数据⻚缓存:
当⼀条update语句执⾏时,通过sql语句中的数据库名和表名解析可以知道我们需要加载的数据⻚处于哪个表空间,根据sql语句本身也可以通过⼀致性算法得数据⻚号(具体的sql解析和算法这⾥暂可简单了解下),根据数据⻚号和表空间号,可以从数据⻚缓存中(本质也就是⼀个哈希表)得到对应缓存⻚地址,通过缓存⻚地址我们直接就可以到InnoDB的缓冲池中定位到缓存⻚;当然,如果数据⻚还没有加载过,缓存⻚地址肯定是不存在,此时就需要从磁盘中加载数据⻚到mysql中了
这个时候⼜有⼀个问题,既然现在我们已经知道磁盘中的数据⻚是加载到buffer pool缓冲池中的,那么我们怎么样才能知道哪些缓存⻚是空闲的?哪些缓存⻚是没有被加载过数据⻚信息的呢?毕竟加载过的数据的缓存⻚和没加载过数据的缓存⻚混在⼀起,倘若此时想找⼀个空闲的缓存⻚肯定也是⼀件很麻烦的事。InnoDB存储引擎在设计时当然也考虑到了这点,这⾥它引⼊了free链表这个数据结构,将那些还没有被使⽤的缓存⻚的描述信息⽤双向循环链表给组合在⼀起,需要⽤到时就卸⼀个节点出来存放数据⻚信息
此时数据⻚被加载到缓存⻚了,缓存⻚中已经有数据了,相关的变动信息肯定也要回写到描述信息中,并且现在因为缓存⻚已经有数据,就不能再待在free链表中了,就需要将该缓存⻚对应的描述信息节点从free链表给摘掉,转移到了lru链表中
lru链表实现的⽬的就是为让哪些被访问的缓存⻚能够尽量排到靠前位置,那么此时如果此时内存不够需要淘汰掉⼀些缓存⻚时,此时就可以到lru链表尾部,将哪些最近最少被访问的尾部节点给刷盘释放缓存⻚腾出内存
到这⾥为此,为了更新⼀个sql,我们已经把该sql所需要的数据、通过InnoDB存储引擎的各种底层机制,给加载到了Buffer Pool缓冲池中了,接下来就是在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链表的处理逻辑。当⼀个数据⻚被加载到缓冲池中时,可能顺带会把其他⽆关紧要的数据⻚也加载到缓冲池中,这些顺带加载到内存的数据⻚,它们往往被访问的频率是⾮常低的,但是由于lru链表的特点,新加⼊的总是会优先
被排在lru的链表头,导致这些顺带进来的、访问频率⽐较低的缓存⻚排在⽐较靠前的位置,导致free链表不够时,lru链表反⽽会把那些本来访问频率较⾼、但是此时被排挤到lru链表尾的缓存⻚给刷盘清理了,这是很不合理的。
优化后的lru链表主要引⼊了冷热数据分离的思想解决了mysql预读机制带来的问题。把lru链表分为热数据区和冷数据区,热数据区主要存放那些访问频率⾼的缓存⻚,冷数据区存放访问频率较低的缓存⻚;从磁盘加载数据到lru链表时,⾸先会将加载到的缓存⻚直接先放到冷数据链的表头,如果1000ms(默认,可配置)后冷数据的缓存⻚⼜被访问了,此时就认为这些1000ms之后被访问的缓存⻚,在不久的未来可能还会被访问,可以认为它们是热数据了,就会把这些缓存⻚从冷数据区的链表给移动到热数据区链表的表头,通过该步骤可以将热数据从冷数据堆中给巧妙的分离出来
此时如果要加载其他数据⻚发现缓冲池内存不够,实际上后台⼀直会有⼀个线程开启的⼀个定时任务,不断的从lru链表的尾部将缓存⻚给刷到磁盘中并释放缓存⻚,lru链表冷热数据分离的设计,确保了定时任务从lru链表尾部回收的缓存⻚都是访问频率很低的数据,对性
能的影响也就降到了最低。