Mysql快速学习——《二》: Mysql的InnoDB存储引擎设计

上篇文章介绍了一条SQL语句在数据库中的执行流程,当SQL执行到存储引擎这里,因为不同的引擎的实现机制有所不同,现在就以使用最广泛的为InnoDB引擎再来细化说明Innodb在数据查询和更新流程的细节。

上篇: Mysql快速学习——《一》: Mysql的基础架构
参考: The InnoDB Storage Engine
思维导图: mysql

存储引擎层

存储引擎层(Storage Engines),它决定了 MySQL 会怎样存储数据,怎样读取和写入数据,也在很大程度上决定了 MySQL 的读写性能和数据可靠性。
对于这么重要的一层能力,MySQL 提供了极强的扩展性,你可以定义自己要使用什么样的存储引擎:InnoDBMyISAMMEMORYCSV,甚至可以自己开发一个存储引擎然后使用它。

InnoDB存储引擎

下面是InnoDB 引擎的逻辑架构图, 弄懂它那你对MYSQL的理解将更进一步:


innoDB架构图

从上图可以看到InnoDB分为2大块:

  • In-Memory Structures (内存部分)
  • On-Disk Structures (磁盘部分)

其中In-Memory Structures (内存部分)包括:

  • Buffer Pool - 内存缓冲池
  • Change Buffer - 写交换缓冲池
  • Adaptive Hash Index - 自适应哈希索引
  • Log Buffer

On-Disk Structures (磁盘部分), 从架构图可以看到, Tablespaces 分为五种(我们平时创建的表的数据,可以存放到 The System Tablespace 、File-Per-Table Tablespaces、General Tablespace 三者中的任意一个地方,具体取决于你的配置和创建表时的 sql 语句)

  • The System Tablespace - 系统表空间
    ---- 在数据库建立的时候自动创建的,它包含了整个数据库的数据字典。
  • File-Per-Table Tablespaces - 独立表空间
    ---- 是对The System Tablespace(系统表空间)的一个更灵活的选择, 在MySQL 5.6.6和更高版本默认启用的。
  • General Tablespace - 通用表空间
    --- 类似于系统表空间,常规表空间是共享表空间,可以存储多个表的数据。
  • Undo Tablespaces - 回退表空间
  • Temporary Tablespaces

On-Disk Structures (磁盘部分)除了表结构定义和索引,还有一些为了高性能和高可靠而设计的角色,比如

  • redo log
  • undo log
  • Change Buffer
  • Doublewrite Buffer

内存部分组件详解

1. Buffer Pool

缓存表数据与索引数据,把磁盘上的数据加载到缓冲池,避免每次访问都进行磁盘IO,起到加速访问的作用。


InnoDB缓冲池策略
  • 按页(4K)读取
    磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(一般是4K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率。
  • “集中读写”的原则(预读)
    数据访问,通常都遵循“集中读写”的原则,使用一些数据,大概率会使用附近的数据,这就是所谓的“局部性原理”。InnoDB会把一些“可能要访问”的页提前加入缓冲池,避免未来的磁盘IO操作。

传统LRU缓冲池算法

为了减少数据移动,LRU一般用链表实现。最常见的玩法是,把入缓冲池的页放到LRU的头部,作为最近访问的元素,从而最晚被淘汰。这里又分两种情况:
(1)页已经在缓冲池里,那就只做“移至”LRU头部的动作,而没有页被淘汰;
(2)页不在缓冲池里,除了做“放入”LRU头部的动作,还要做“淘汰”LRU尾部页的动作;

InnoDB并不直接使用传统的LRU缓冲池算法, 因为传统的LRU缓冲池算法会出现以下问题:
(1)预读失效: 由于预读(Read-Ahead),提前把页放入了缓冲池,但最终MySQL并没有从页中读取数据,称为预读失效。
(2)缓冲池污染: 当某一个SQL语句,要批量扫描大量数据时,可能导致把缓冲池的所有页都替换出去,导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。


InnoDB对传统LRU旳优化
  • 预读失败优化 - 新老生代机制
    (1)将LRU分为两个部分:新生代(new sublist) + 老生代(old sublist)
    (2)新老生代收尾相连,即:新生代的尾(tail)连接着老生代的头(head);
    (3)新页(例如被预读的页)加入缓冲池时,只加入到老生代头部
    (4)如果数据真正被读取(预读成功),才会加入到新生代的头部
    (5)如果数据没有被读取,则会比新生代里的“热数据页”更早被淘汰出缓冲池


    新老生代机制
  • 缓冲池污染优化 - 老生代停留时间窗口机制
    (1)假设T=老生代停留时间窗口;
    (2)插入老生代头部的页,即使立刻被访问,并不会立刻放入新生代头部;
    (3)只有满足“被访问”并且“在老生代停留时间”大于T,才会被放入新生代头部;


    老生代停留时间窗口机制

buffer_pool相关重要参数
  • innodb_buffer_pool_size
    配置缓冲池的大小,在内存允许的情况下,DBA往往会建议调大这个参数,越多数据和索引放到内存里,数据库的性能会越好。
  • innodb_old_blocks_pct
    老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。
  • innodb_old_blocks_time
    老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。

2. Change Buffer

写请求的处理流程

(1)如果索引页不在buffer pool中, 则先把索引页,从磁盘加载到缓冲池,一次磁盘随机读操作;
(2)修改缓冲池中的页,一次内存操作;
(3)写入redo log,一次磁盘顺序写操作;


写请求处理

是否会出现一致性问题呢?

不会, 因为:
(1)读取,会命中缓冲池的页;
(2)缓冲池LRU数据淘汰,会将“脏页”刷回磁盘;
(3)数据库异常奔溃,能够从redo log中恢复数据;


利用Change Buffer进行优化

上述场景中, 被读取的数据没有命中缓冲池的时候,会先从磁盘索引页到缓冲池中, 这样至少产生一次磁盘IO,对于写多读少的业务场景,性能压力会剧增, 于是InnoDB引入了Change Buffer:

  • 当对页进行了写操作,并不会立刻将磁盘页加载到缓冲池
  • 先把页的写操作记录到缓冲变更池(buffer changes)
  • 等未来数据被读取时,再将数据合并(merge)恢复到缓冲池中

写缓冲的目的是降低写操作的磁盘IO,提升数据库性能。


Change Buffer

在内存中,Change Buffer占用Buffer Pool的一部分。在磁盘上,Change Buffer是系统表空间的一部分,其中的索引会在关闭数据库服务器时更改。


Change Buffer相关参数配置
  • 配置Change Pool最大大小
    ---innodb_change_buffer_max_size: 允许将Change Buffer的最大大小配置为缓冲池总大小的百分比。默认情况下, innodb_change_buffer_max_size设置为25.最大设置为50。
  • 配置Change Buffer的适用范围
    ---innodb_change_buffering: all | none | inserts | deletes | changes | purges

3. Adaptive Hash Index

AHI是InnoDB索引的索引, 为了在索引很大时快速得到数据。
AHI 在实现上就是一个哈希表:从某个检索条件到某个数据页的哈希表,仿佛并不复杂,但其中的关窍在于哈希表不能太大(哈希表维护本身就有成本,哈希表太大则成本会高于收益),又不能太小(太小则缓存命中率太低,没有任何收益)。
AHI建立需要遵循以下约束:
(1)某个索引树要被使用足够多次
(2)该索引树上的某个检索条件要被经常使用
(3)该索引树上的某个数据页要被经常使用

4. Log Buffer

当在MySQL中对InnoDB表进行更改时,这些更改首先存储在InnoDB日志缓冲区的内存中,然后写入通常称为重做日志(redo logs)的InnoDB日志文件中。
日志缓冲区是内存存储区域,用于保存要写入磁盘上的日志文件的数据。日志缓冲区大小由innodb_log_buffer_size 变量定义,默认大小为16MB。
日志缓冲区的内容定期刷新到磁盘。较大的日志缓冲区可以运行大型事务,而无需在事务提交之前将重做日志数据写入磁盘。因此,如果有更新,插入或删除许多行的事务,则增加日志缓冲区的大小可以节省磁盘I/O。

磁盘部分组件详解

1. redo log

在更新操作时,会先更新Buffer Pool中的数据然后再去操作磁盘,但是在极端情况下会出现系统宕机或者断电导致磁盘还未更新就丢失了数据,此时需要把对内存所做的修改写入到一个redo log buffer里去,这里也是一个内存缓冲区,用于存放redo日志的。

2. undo log

如果执行一个更新语句,且这个语句还在事务里的话,在事务提交以前,我们都可以选择回滚,而这部分回滚的数据,就是未更新以前的数据,它是保存在undo日志里的。

3. Change Buffer

--

4. Doublewrite Buffer

如果说 Change Buffer 是提升性能,那么 Doublewrite Buffer 就是保证数据页的可靠性。


怎么理解呢?

前面提到过,MySQL 以「页」为读取和写入单位,一个「页」里面有多行数据,写入数据时,MySQL 会先写内存中的页,然后再刷新到磁盘中的页。
这时问题来了,假设在某一次从内存刷新到磁盘的过程中,一个「页」刷了一半,突然操作系统或者 MySQL 进程奔溃了,这时候,内存里的页数据被清除了,而磁盘里的页数据,刷了一半,处于一个中间状态,不尴不尬,可以说是一个「不完整」,甚至是「坏掉的」的页。

有同学说,不是有 Redo Log 么?其实这个时候 Redo Log 也已经无力回天,Redo Log 是要在磁盘中的页数据是正常的、没有损坏的情况下,才能把磁盘里页数据 load 到内存,然后应用 Redo Log。而如果磁盘中的页数据已经损坏,是无法应用 Redo Log 的。
所以,MySQL 在刷数据到磁盘之前,要先把数据写到另外一个地方,也就是 Doublewrite Buffer,写完后,再开始写磁盘。Doublewrite Buffer 可以理解为是一个备份(recovery),万一真的发生 crash,就可以利用 Doublewrite Buffer 来修复磁盘里的数据。

代码块

你可能感兴趣的:(Mysql快速学习——《二》: Mysql的InnoDB存储引擎设计)