InnoDB 存储引擎

学习Mysql有一段时间了,对SQL语法、索引、查询优化做了一定的了解,但是总感觉浮于表面,所以想换一种角度去理解Mysql-它的存储实现引擎:InnoDB。InnoDB是Mysql5.5之后默认的存储引擎,本文也主要是参照Mysql5.6的官方手册。

一、InnoDB的整体架构

InnoDB 存储引擎_第1张图片

这是一张Mysql官网的图,我们先对图中的各个名词及作用做一个解释,方便后续的理解:

  1. 页:Mysql中存储、读取数据的单元,默认值是16KB。
  2. Buffer Pool:缓存索引和表数据的内存区域,缓存的基本单元是页,页采用LRU算法进行更换。
  3. Change Buffer(Memory):逻辑上是Buffer Pool的一部分,当辅助索引页发生改变且不在Buffer Pool中时,它会缓存更改的辅助索引页。
  4. Log Buffer:日志缓冲区是存储要写入磁盘日志文件的数据的内存区域。
  5. adaptive hash index:当InnoDB注意到某些值被引用的非常频繁时,他会在内存中基于B-Tree索引之上再创建一个哈希索引,让B-Tree索引也具备一些哈希索引的优点,比如访问速度快等。
  6. System Tablespace:是InnoDB Data Dictionary、Doublewrite Buffer、Change Buffer和Undo Logs的存储区域。如果表是在系统表空间中创建的,而不是在每个表空间中创建文件,则它还可能包含表和索引数据。
  7. InnoDB Data Dictionary:InnoDB数据字典由内部系统表组成,这些表包含用于跟踪表、索引和表列等对象的元数据。元数据物理上位于InnoDB系统表空间中。
  8. Doublewrite Buffer: 是一块用来保证InnoDB数据完整性的存储区域。
  9. Change Buffer(Disk):Change Buffer(Memory)的物理存储位置,Mysql实例启动时会被重新加载到内存中。
  10. Undo Logs:撤消日志是与单个读写事务关联的撤消日志记录的集合。撤消日志记录包含有关如何撤消事务对聚集索引记录的最新更改的信息。如果另一个事务需要将原始数据视为一致读取操作的一部分,则将从撤消日志记录中检索未修改的数据。
  11. Tables:存储表的索引和数据。
  12. Redo Logs:重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间更正不完整事务写入的数据。File-per-table Tablespaces:每表表空间一个文件包含单个InnoDB表的数据和索引,并存储在文件系统中自己的数据文件中。
  13. Undo TableSpaces:撤消表空间包含撤消日志,撤消日志是撤消日志记录的集合,其中包含有关如何撤消事务对聚集索引记录的最新更改的信息。

二、InnoDB如何存储数据

InnoDB 存储引擎_第2张图片

InnoDB通过磁盘存储数据,数据在磁盘中通过一棵B+Tree进行组织,如上图所示,是一棵简单版的B+Tree,B+Tree是B-Tree的一种变形,关于B-Tree可以看我的另一篇博客《多路平衡查找树B-Tree》。总的来说B+Tree与B-Tree主要有三点不同:

  1. B-Tree中所有节点都存储数据,而在B+Tree树中只有叶子节点存储数据,非叶子节点只存储索引关键字。
  2. B+Tree叶子节点首尾相连,因为所有数据都存储在叶子节点上,因此可以直接扫描叶子节点访问全表,B-Tree则不是。
  3. B+Tree相比B-Tree提供了一种稳定的数据存储结构,整个索引结构更加稳定
    1. 插入稳定:在叶子节点进行插入,非叶子节点能容纳更多的数据因此降低页分裂的次数。
    2. 删除稳定:在叶子节点进行删除,降低页合并的次数。
    3. 查询稳定:数据始终在叶子层,通过链表而不是树的形式扫描全部。

在InnoDB中,这种存储结构有另外一个名字:聚簇索引。聚簇索引不是一种单独的索引类型,而是一种数据存储方式。在磁盘中无论是数据还是索引都以页为单位进行存储,页的默认大小是16KB,可以通过Innodb_page_size进行指定。每一页中存储不定数量的数据行,InnoDB通常会在每页预留15/16的空间用于未来的插入或更新,当某行的一列可变长度记录太大以至于不能在一个页中容纳时,InnoDB会创建溢出页并在改行引用溢出页。

在InnoDB的每一行中,InnoDB提供了三个额外的字段:

  1. DB_TRX_ID:插入或更新改行的最近事务ID(6字节)。
  2. DB_ROLL_PTR:指向Undo Log记录的回滚段(7字节)。
  3. DB_ROW_ID:单调递增的一个行ID,在行被插入时产生,当没有指定主键时被当做主键索引,其他情况下无作用(6字节)。

三、InnoDB如何加载和更新数据。

在磁盘中InnoDB以页为单位管理数据,在内存中也是一样,当InnoDB读取某一页时,会将读到的页缓存到Buffer Pool中。

InnoDB 存储引擎_第3张图片

如上图所示,Buffer Pool通过链表的形式组织Buffer Pool中的页,在逻辑上链表分为两部分:New SubList和Old SubList,Buffer Pool的空间是有限的,当Buffer Pool被填充满时,通过LRU的一种变形策略淘汰页,因此InnoDB读取一个新页过程如下:

  1. 判断该页是否在Buffer Pool中,若存在,则把该页移动到New SubList Head并读取该页。
  2. 若不存在,则需要向磁盘读取该页,InnoDB预计该页相关的数据不久后会用到,因此通过Read-Ahead的方式预取该页所在区段的所有页,并将新读入的页插入到Old SubList Head中,若插入页是要查询的页,则会将该页直接插入到New SubList Head中并读取。这也是为什么不直接使用LRU的原因,假如直接在链表Head插入,则插入的页可能始终没有被使用而淘汰,因此通过这种方式,New SubList存在的页始终是近期被访问过的页。
  3. 当数据库运行时,缓冲池中未被访问的页面会“老化”到列表的尾部。新的和旧的子列表中的页面随着其他页面的更新而老化。旧子列表中的页面也会随着页面插入到中点而老化。最终,一个未使用的页面到达旧子列表的尾部并被逐出。

当数据发生改变时,即表数据改变和表索引改变,整个过程如下:

  1. 当数据库收到DML相关的操作时,首先会修改Buffer Pool中的内容并将请求记录在Log Buffer中,在提交事务时会将Log Buffer中的内容刷新到磁盘Redo Log,这样做主要有两个好处:
    1. InnoDB采用Write Ahead Log策略来防止宕机数据丢失,即事务提交时,先写重做日志,再修改内存数据页,假如发生意外情况,在重启Mysql实例时可以通过Redo Log进行数据恢复。
    2. 重做日志在磁盘上由两个名为ib_logfile0和ib_logfile1的文件物理表示,是一块连续的磁盘区域。因此写Redo Log的花费比直接刷新脏页小很多。MySQL以循环方式写入重做日志文件。
  2. 当辅助索引数据发生改变时,假如发生改变的索引页不在Buffer Pool中,InnoDB会将发生改变的辅助索引页缓存在内存中的Change Buffer中,只有当发生改变的页被加载到内存中时才会将先前发生的改变合并到该页上,因此InnoDB会在Mysql实例空闲或者关闭时将Change Buffer的内容刷新到System Tablespaces中的Change Buffer中,以便实例重启时加载为完成合并的Change Buffer。
  3. Buffer Pool默认缓存100页,当页面淘汰时,假如该页是脏页,需要将该页写回到磁盘,InnoDB使用了一种叫做doublewrite的特殊文件flush技术,在把pages写到date files之前,InnoDB先把它们写到System Tablespaces中一个叫doublewrite buffer的连续区域内,在写doublewrite buffer完成后,InnoDB才会把pages写到data file的适当的位置。如果在写page的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复。noDB用双写缓冲区来避免页没有写完整导致的数据损坏。有人可能会问数据恢复不是有Redo Log吗,为什么还需要这个?有兴趣的同学可以去了解下partial page write问题,简单说就是脏页在写回时会覆盖之前的数据,假如数据没有备份,发生意外情况,通过Redo Log 恢复时找不到该页中的行原来的事务ID,因此无法恢复。

三、InnoDB如何处理事务。

InnoDB是支持事务的存储引擎,有关事务的简单介绍可以看我的另一篇博客《Mysql架构与概念》。这一节主要讨论以下InnoDB是如何实现事务的相关特性。

Consistent Read:一致读是一种读取操作,它使用快照信息根据时间点显示查询结果,而不考虑同时运行的其他事务所做的更改。如果查询到的数据已被另一个事务更改,则会根据Undo Logs的内容重建原始数据。这种技术避免了一些锁定问题。对于可重复读取隔离级别,快照基于执行第一次读取操作的时间。在读取提交隔离级别下,快照将重置为每个一致读取操作的时间。一致读取是InnoDB进程在读取提交和可重复读取隔离级别中选择语句的默认模式。因为一致读取不会对其访问的表设置任何锁,所以在对表执行一致读取时,其他会话可以自由修改这些表,一致读会忽略表上的任何锁。

Undo Logs:撤消日志是与单个读写事务关联的撤消日志记录的集合。撤消日志记录包含有关如何撤消事务对聚集索引记录的最新更改的信息。如果另一个事务需要将原始数据视为Consistent Read操作的一部分,则将从撤消日志记录中检索未修改的数据。

锁:有关锁的介绍可以看我的另一篇博客《Mysql Innodb Lock》。

InnoDB中的事务主要体现在两个方面:MVCC和锁机制。

MVCC:InnoDB是一个多版本的存储引擎:它保存有关已更改行的旧版本的信息,以支持事务特性,如并发和回滚。这些信息存储在表空间中的一个称为回滚段的数据结构中。InnoDB使用回滚段中的信息来执行事务回滚所需的撤销操作。它还使用这些信息来构建行的早期版本,以便进行Consistent Read。回滚段通过行中的隐藏列DB_ROLL_PTR指定。

锁机制:

两段锁协议是指每个事务的执行可以分为两个阶段:生长阶段(加锁阶段)和衰退阶段(解锁阶段)。

加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁,在进行写操作之前要申请并获得X锁。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。

解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

两段封锁法可以这样来实现:事务开始后就处于加锁阶段,一直到执行ROLLBACK和COMMIT之前都是加锁阶段。ROLLBACK和COMMIT使事务进入解锁阶段,即在ROLLBACK和COMMIT模块中DBMS释放所有封锁。

假设是在RR隔离级别下,InnoDB加锁的过程如下:

  1. 对查询的记录进行扫描,若是select查询,则默认走Consistent Read,忽略查询记录上的任何锁。
  2. 若是更新类(Update或Delete)查询,首先看Where条件是否能走索引,若能够通过索引查询,
    1. 判断是否是精确查询,精确查询对扫描到的索引加记录锁。
    2. 若为范围查询,则对扫描到的索引加临键锁或间隙锁。
  3. 若不能通过索引查询,则进行全表扫描,对扫描到的数据加临键锁或间隙锁。

InnoDB加锁是对扫描到的索引记录加锁,若索引是聚集索引,则直接在聚集索引上加锁,若是辅助索引,则在给辅助索引加锁的同时,也要在对应的聚集索引上加锁。

你可能感兴趣的:(mysql,http)