浅谈sql执行流程、innodb架构设计、buffer pool缓存池

发布时间 2024-01-01 15:38:03作者: 天~若比邻

一.从服务端到数据库sql执行流程:

  • 1.SQL接口:负责处理接收到sql的语句
  • 2.查询解析器:负责将sql变成数据库可以看懂的语言
  • 3.查询优化器:选择最优的查询路径(针对你编写的复杂sql语句生成查询路径树,然后从中选择一条最优的查询路径)
  • 4.执行器:根据执行计划调用存储引擎接口(执行器会根据我们的优化器生成一套执行计划,然后不停的调用存储引擎的各种接口去完成sql语句的执行计划)。
  • 5.存储引擎:存储引擎具体去执行sql语句计划。(他会按照一定的步骤去查询内存缓存数据,更新磁盘数据,查询磁盘数据等)

二.通过一个简单的更新语句,初步了解Innodb存储引擎架构设计

update user set name = 'xxx' where id = 1

  • 更新流程:

    • 1.当我们用Innodb存储引擎更新语句的时候,他首先会看一下buffer pool里面有没有这条数据,没有这条数据,他会首先从磁盘读取,将这条数据加载到buffer pool,并且为这条数据加上写锁;
    • 2.紧接着将更新前的值写入undo日志文件中,用于数据的回滚;
    • 3.然后修改内存数据(因为此时内存数据和磁盘数据不一致,所以内存数据也叫脏数据);
    • 4.将更新后的数据信息写入redo log buffer缓存区;
    • 5.提交事务的时候,将redo log(记录物理日志,记录了对哪个数据页的什么记录,做了什么修改)刷入磁盘文件;
    • 6.在提交事务的时候,也会将binlog(逻辑日志,记录了一个对哪个表的那条记录,做了什么修改)写入磁盘文件;
    • 7.最后,把本次更新对应的binlog文件名称和这次更新的binlog日志在文件里的位置,以及commit标记写入redo log日志。
  • 1.undo log日志:用于数据的回滚。把更新前的值,写入undo日志文件中。

  • 2.buffer pool: 内存缓存区。是innodb存储引擎的一个重要组件,这里面会缓存很多数据,以便于以后在查询的时候,内存缓冲池里有数据,就不用再去查磁盘了。

  • 3.redo log buffer: 内存缓存区(用来存放redo日志)

  • 4.redo log日志:提交事务的时候,将redo日志写入磁盘中。(mysql宕机时,数据还没来得及刷入磁盘,重启数据库时,redo log去恢复之前做过的修改,恢复buffer pool池数据)

  • 5.binlog日志:提交事务的时候,会把更新对应的binlog日志写入磁盘文件中。(mysql宕机时,用来恢复磁盘数据;主从复制:主节点开启binlog,从节点接收binlogrelay log,读取内容,生成sql语句执行)

  • redo log 日志redo log buffer刷入磁盘文件的策略(innodb_flush_log_at_trx_commit):

    • 1.参数设置为0,提交事务的时候,不会把redo log buffer里的数据刷入磁盘,数据库突然宕机了,buffer pool缓冲区的数据就丢失。

    • 2.参数设置为1,提交事务的时候,就必须把redo log buffer里面的数据刷入磁盘文件,事务提交成功了,也就是redo log一定在磁盘中。

    • 3.参数设置为2,提交事务的时候,不会将内存中的redo日志刷新到磁盘,而是会刷新到操作系统的缓存中,然后可能1s后再刷新到磁盘中。

  • binlog日志的刷盘策略(sync_binlog):

    • 1.为0时(默认值), binlog不直接进入磁盘文件,而是进入操作系统缓存,由操作系统判断将binlog刷入磁盘的时机。

    • 2.设置为1时,提交事务时,binlog直接写入到磁盘文件中。

  • redo logbinlog区别:

    • 1.redo logInnodb存储引擎特有的一个东西;binlogmysql自己的日志文件。

    • 2.redo log是偏向物理性质的重做日志,“他记录的是对哪个数据页的什么记录,做了什么修改”;binlog是偏向于逻辑性的日志,“他记录的是对哪个表的哪一行做了更新操作,更新以后的值是什么”。

三.Buffer Pool缓冲池:

  • mysql的数据模型就是表+行+字段的概念,实际上mysql对数据抽象出来一个数据页的概念,他把多行数据放在一个数据页中,也就是说我们的磁盘文件中又很多数据页,每个数据页中放了很多数据。我们实际上更新一条数据的时候,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接加载到buffer pool缓冲池中。也就是讲buffer pool缓冲池中,放的是一个一个的数据页,数据页加载到缓冲池之后,在缓冲池中叫缓存页。
  • 1.数据页:数据页默认大小是16KB
  • 2.缓存页:和数据页的大小是一一对应的,也是16KB
  • 3.描述数据:每个缓存页都有一个描述数据,记录了这个数据页所属的表空间、数据页号、缓存页在buffer pool中的地址等信息;描述数据本来也是一块数据;每个描述数据占缓存页大小的5%(800字节)左右;buffer pool (默认128M)实际的大小,会比设置的稍大一下;

  • 4.当你的数据库启动以后,就会按照你设置的buffer pool的大小,再稍微加大一点,去操作系统申请一块内存区域,作为buffer pool的内存区域;申请完毕,数据库就会按照默认的缓存页16KB的大小以及对应的800个字节左右的描述数据的大小,在buffer pool中划分一个个缓存页和对应的描述数据。在我们不停的执行增删改的操作的时候,此时就需要不停的从磁盘上对一个个数据页放入buffer pool中对应的缓存页中,把数据缓存起来,应对之后的增删改查;

    问题:那些缓存页是空闲的?
    解决:数据库为buffer pool设计了一个free链表。

  • 5.free链表:由描述数据块地址组成的双向链表(每个节点就是一个空闲缓存页的描述数据的地址);除此之外,free链表还有一个基础节点,是一个40字节大小的节点,里面存放了free链表的头节点地址和尾节点地址,以及当前链表中有多少个节点;

问题:怎么知道那些数据页是已经被缓存过的?
解决:数据库有一个哈希表的数据结构,他会用表空间号+数据页号作为key,缓存页地址作为value;当你要使用一个数据页的时候,通过这个key去哈希表里查一下,如果有value,就说明数据页已经被缓存了。

问题:更新过的数据页叫脏页,这些更新过的脏页数据,最终是要被刷回磁盘文件的;不可能所有的缓存页都刷回磁盘的,因为有些缓存页仅仅是因为查询才被读到buffer pool池的,根本没有修改过,全部刷回,对性能影响很大,如果频繁使用的缓存页,这次刷回,下次还要从磁盘中读取,加载buffer pool池中。
解决mysql引入了一个flush链表。

  • 6.flush链表:本质也是通过描述数据地址组成的双向链表;用来记录哪些缓存页是脏页;

    问题:当你不停的把磁盘上的数据页加载到空闲缓存页里,free链表中不停的移除缓存页,当没有空闲缓存页时,此时无法从磁盘上加载新的数据页到缓存页,就必须淘汰一些缓存页
    解决:数据库引入LRU链表(least recently used,最近最少使用),他和free链表、flush链表结构一样,都是双向链表;每次磁盘读取数据页放到空闲的缓存页中,并将描述数据块地址添加到LRU链表的表头位置;每次用到某个缓存页都会移到当前缓存页对应的描述数据块到链表头部位置,当缓存页不足的时候,淘汰链表尾部的缓存页,他一定是最近最少使用的缓存页。

  • 7.mysql预读机制:

    • innodb_read_ahead_threshold = 56(默认值),如果顺序的访问一个区里的多个数据页,数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载在缓存里区;

    • innodb_random_read_ahead = off(默认值),如果开启,如果Buffer Pool里缓存了一个区里的13个连续的数据页,此时也会触发预读机制,把这个区的其他数据页都加载到缓存里;

    问题:预读机制的触发,使得这些缓存页一下子都放在LRU链表的前面,其实他们并没有什么人会访问的话,就会导致本来一些频繁被访问的缓存页在LRU链表尾部;一旦要把一些缓存页淘汰的时候,就会把LRU链表尾部一些频繁被访问的缓存页给刷入磁盘和清空掉,这样就不太合理了;(全表扫描也会导致频繁访问的缓存页被淘汰)

    预读问题的解决
    a.基于冷热数据分离的思想设计LRU链表:真正的LRU链表,会被拆成两个部分,一部分是热数据,一部分是冷数据;由参数innodb_old_blocks_pct=37(默认),冷数据占比37%
    b.数据页第一次被加载进来,是放在冷数据区的头部;在1s(由参数innodb_old_blocks_time= 1000(默认值)控制)之后,如果访问这个缓存页,他才会被挪动到热数据区域的链表的头部

  • 8.mysqlLRU链表热数据区的进一步优化:

    问题:热数据区的缓存页经常被访问,频繁的移动对性能也不太友好的;
    解决LRU链表的热数据区访问规则被优化了一下,只有在热数据区后3/4部分的缓存页被访问了,才会移动到链表头部,前面1/4的缓存页被访问了,不会移动的;

  • 9.buffer pool的缓存页以及几个链表的配合使用及缓存页刷盘时机:

    • 数据页加载到一个缓存页,free链表里会移除这个缓存页,然后LRU链表的冷数据区的头部会放入这个缓存页;如果修改了一个缓存页,那么flush链表中会记录这个脏页,LRU链表中还可能会把这个缓存页从冷数据区移动到热数据区的头部。

    • 定时把LRU链表尾部的部分缓存页刷入磁盘:mysql后台有一个线程,会运行一个定时任务,每隔一段时间就把LRU链表的冷数据区的尾部的一些缓存页,刷入磁盘,清空这几个缓存页,并把他们加入free链表里;

    • flush链表刷入磁盘:flush链表中的节点都是脏页数据,LRU链表的热数据区的很多缓存页可能会被频繁修改,所以后台线程也会在mysql在不怎么繁忙的时候,把flush链表中的缓存页都刷入磁盘;只要flush链表中的一些缓存页被刷入磁盘,那么这些缓存页就会从flush链表和LRU链表中移除,然后加入到free链表中;

    问题:没有空闲缓存页了怎么办?
    解决:如果没有空闲缓存页,此时需要从磁盘加载数据页到一个空闲缓存页中,此时就会从LRU链表冷数据区的尾部找到一个缓存页,把数据刷入磁盘并清空,然后把数据页加载到这个腾出来的空闲缓存页中。