背景:
通过前四章的学习,对于mysql有了初步的认识,我们在此基础上,在较之前稍微更细致的了解下mysql的执行流程。此外需要说明的是,相关学习资料借鉴于中华石杉老师,感谢共享。
引言
~~~~
对于mysql存放的数据,逻辑概念上我们称之为表,在磁盘等物理层面而言是按数据页形式进行存放的,当其加载到mysql中我们称之为缓冲页
。
~~~~
每个缓冲页
都有对应的一份描述信息
,存放了缓存页的一些元数据相关信息,通过描述信息可以快速定位到缓存页,呢么最气开始描述信息指向的缓存页都是没数据的,这一步会先将磁盘数据进行加载(见上图)。
~~~~ 后续相关内容建立在存储引擎为innodb下;
一)数据加载至BufferPool
~~~~ 从磁盘加载一个数据页到mysql中存在很多不简单的问题,比如它是如何保证同一份数据页不会在mysql中重复加载?以及mysql如何是如何得到当前数据页是否被加载的呢?
答案是
缓存
。
~~~~
对于已经加载到musql的数据页,mysql将对其进行缓存,这样一方面当我们需要使用到数据页信息的时候,则可以通过缓存信息快速定位到mysql中对应的缓存页
,mysql中的innodb大致就是以此思路。
当一条语句要执行时,
- 通过sql语句中的
数据库名
和表名
可以知道我们需要加载的数据页处于哪个表空间。 - 根据sql语句本身也可以通过一致性算法得到
数据页
。 - 进而根据
数据页号
,可以从数据页缓存中(本质为一个哈希表)得到对应缓存页地址。 - 通过缓存页地址我们直接就可以到innoDB的缓冲池中定位到
缓存页
。
通过以上几个步骤,我们将磁盘中的数据页加载到innodb 中的buffer pool 了。
呢么,mysql是如何确定哪些缓存页是空闲的呢?
倘若此时想找一个空闲的缓存页去存放从磁盘读取的数据页,该怎么做呢? 毕竟加载过数据的缓存页跟没加载过数据的缓存页混在一起,还是蛮麻烦的事情。
mysql 引入了free链表这样的数据结构,将呢么还没被使用的缓存页的描述信息用双向虚幻链表进行组合,需要用到时就卸一个节点出来存放数据。
呢么,目前流程大致如下:
~~~~
此时数据页
被加载到缓存页
,当缓存页
具备了数据后,相关的变动信息也需要写回到描述信息
中。同时,因为缓存页
已经有了数据,描述信息节点
需要从free链表中脱离,转移到lru
链表中,过程如下。
~~~~ LRU链表的目的就是为让被访问的缓存页能够尽量排到靠前的位置,呢么此时如果内存不足,需要淘汰掉一些缓存页时,就可以从lru链表尾部,最终从尾结点开始进行删除。
通过上述流程,将sql所需的数据将在到Buffer poll中了,接下来到后续的innoDB中执行更新操作。
二)BufferPool中的数据如何处理
~~~~
此时待处理数据已从磁盘加载到缓冲池中,下一步当然就是执行更新操作了。先对需要更新的行数据加锁、原始数据写一份到redo log 中便于可能的回滚操作、执行update操作,此时缓存页的数据就被更新了。
~~~~
且此时此刻,缓存页
的数据和磁盘中
的数据就不一致了,这样的缓存页我们称之为脏页
,过程如下。
~~~~
接下来就是如何进行刷脏,呢么首先我们该怎么知道哪些缓存页是脏页呢?
如果能把脏页和空闲的缓存页分离出来,我们就可以把相关的脏页数据及时写到磁盘中,随后进行脏页释放。这里的innoDB设计的方法类型与free链表,并设计了一个flush链表,存放的是在缓冲池
中被更新过数据的缓存页
。这些缓存页的描述信息
都会被添加到flush链表中。其中需要说明的是free链表、lru链表、flush链表都是双向循环链表,且节点都为缓存页的描述信息
,其中flush链表的节点同时也在lru链表中。
2.1)缓冲池内存不足触发脏页刷盘
~~~~ 若后续的sql需要把磁盘中的数据加载中缓存页中,碰到内存告罄,则需要先清理下内中的缓存页了。
~~~~
通过之前的lru链表,可以找到lru链表中的尾节点,相比而言访问量是最低的,呢么当内存不够用的时候,自然要被优先清理掉。因为flush链表
的节点也在lru链表中,此时在缓存页清理的时候需要做一项简单的判断。
- 若
缓存页
既在lru表尾的节点同时也在flush链表中,第一步事情是需要先进行刷脏,随后释放掉缓存页的内存,保证事务的相关修改可以正确同步至磁盘中。- 若
缓存页
不在flush链表中,则直接执行简单的释放缓存页内存即可,然后将这些释放完内存缓存页的描述信息
,重新添加到free链表中
。
2.2)mysql预读机制带来的问题
~~~~
mysql预读机制对上述流程是有较大的干扰。
当一个数据页被加载到缓冲池中,它可能会顺带把其他无关紧要的数据页
也加载到缓冲池
中,这些顺带加载到内存的数据页,往往被访问的概率会低于期望数据。但是由于lru链表的特别,新加入的总会有限被排在lru的链表头。导致这些顺带进来的、访问频率比较低的缓存页
排在比较靠前的位置,也会消耗free链表
。LRU链表反而会把呢些本来访问频率较高、但是此时被排挤到lru链表尾
的缓存页的数据进行刷盘清理。
优化后的lru链表主要引入了
冷热数据分离
的思想解决了mysql预读机制带来的问题。
将Lru链表分为热数据区与冷数据区,从磁盘加载数据到LRU链表的时,首先
会将加载到的缓存页
直接放到冷数据的表头
,如果1000MS
(配置)后冷数据又被访问了,将变更为热数据,此时将从冷数据区的链表给移动到热数据区链表的表头
,通过该步骤将热、冷分离。
流程见下:
~~~~
同样,为了避免缓冲池内存不够,实际上后台会有一个线程挂在定时任务上,不断从lru链表尾部将缓存页刷回至磁盘中并同时释放缓存页。
为了维护【热】数据的访问,LRU的访问做了很多优化,所以当一次错误的全表查询又推动相关缓存页进入热LRU,是真的很不应该的一件事情。