InnoDB

来自智得网
跳转至: 导航、​ 搜索

简介

InnoDB整体架构

InnoDB引擎设计上借鉴了Oracle数据库的架构,是一个事务安全的存储引擎,其最早有Innobase Oy公司研发,从Mysql 5.5开始就是默认的存储引擎。

Mysql支持行锁,MVCC,一致性非锁定读等特性,是mysql在OLTP应用中首选的存储引擎。

InnoDB引擎的整体架构可以分为几个模块:

  • 线程模型,InnoDB的线程模型较为复杂,有处理请求的IO线程,还有大量定时执行的线程。
  • 内存模型,InnoDB引擎主要的内存区域是Buffer Pool,Buffer Pool同时承担Buffer和Cache的功能,即为读请求提供缓冲,也为写请求提供缓冲区。
  • 存储模型,InnoDB引擎提供了B+树等数据结构作为索引。
  • 事务模型,数据库事务的ACID,InnoDB引擎分别通过以下能力实现:
    • 原子性,通过UndoLog回滚操作实现原子性。
    • 一致性,提供唯一索引,外健等机制进行数据约束。
    • 隔离性,不同的隔离级别实现的方式不同,对于默认的可重复读隔离级别通过MVCC、间隙锁等实现。
    • 持久性,通过RedoLog等机制实现。

原理

线程模型

线程类型 主要功能
Master线程 根据InnoDB引擎的当前状态执行不同的循环,loop/background loop/flush loop/suspend loop。

这些循环的主要功能包括: 将日志缓冲/脏页刷新到磁盘,合并插入缓冲,产生checkpoint,清理无用的undo页,table cache等

IO线程 IO线程主要分为4类:

read线程处理读请求, 并负责将数据页从磁盘上读取出来, 可以通过参数设置线程数量。 write线程负责将数据页从缓冲区写⼊磁盘, 也可以通过参数设置线程数量, page_cleaner线程发起刷脏页操作后,write_thread 就开始执行。 redo_log_thread负责把⽇志缓冲中的内容刷新到 Redo log ⽂件中。 change buffer thread把插入缓存(change buffer)中的内容合并刷新到磁盘中。

page cleaner线程 负责脏页刷新的线程,可以通过参数设置。
purge线程 删除无用的undo页
checkpoint线程 缓冲区的页面定时刷新到磁盘的过程称为checkpoint。

日期刷新数据到磁盘可以缩短出现故障时候数据库的恢复时间,缓冲区的数据空间也可以得到限制。redo log发生切换时,也会执行checkpoint。 为了避免内存数据的丢失,数据库普遍采用WAL(Write Ahead Log)的策略,即先写日志,然后再写缓冲区的页。

内存模型

InnoDB存储引擎的持久化数据存在磁盘上,磁盘上记录的数据按照页的方式进行管理,出于性能考虑,InnoDB引擎设计了Buffer Pool(缓冲池),用于缓存磁盘上的数据,同时用于写操作的buffer。

InnoDB引擎对数据库的读写操作,都会首先从磁盘上加载数据到Buffer Pool,对于写操作会先修改Buffer Pool的数据,然后再以一定频率刷到磁盘上。

通常情况下,Cache和Buffer两个词的语义是有差别的,Cache一般是指数据缓存,主要解决的频繁读取热点数据的性能问题,Buffer一般是指缓冲,是指写请求首先写入缓冲区,之后再批量更新到持久化存储,解决的是写操作的性能问题,但是InnoDB引擎的Buffer Pool兼顾了二者的作用。

Buffer Pool默认的空间是128MB,这个值可以通过变量innodb_buffer_pool_size进行查询,在启动的时候也可以在配置文件中修改该变量的值。

SHOW VARIABLES LIKE 'innodb_buffer_pool_size'

Buffer Pool以页(Page)为单位,页的大小一般为16K,和磁盘存储的页大小保持一致。数据库启动的时候会按照Chunk为单位申请Buffer Pool的内存空间,一个 chunk 默认就是128M,一个Chunk是一片连续的空间,这些空间按照16K一页的方式组成连续的缓存页。

BufferPool结构图

除了缓存页之外,每个页还会有一个控制块,控制块记录了缓存页的表空间、页号、缓存页地址、链表节点。

在InnoDB运行过程中,缓存页的状态分为三种:

  • free page:未被使用的空闲page
  • clean page:被使用的page,但是数据未被修改。
  • dirty page:被修改过的数据,和磁盘数据相比已经发生变化。


在需要缓存新数据的时候,Buffer Pool需要可以获取哪些缓存页还未使用,可以从这些缓存页中获取一个作为缓存。所以Buffer Pool设计了Free链表,在初始化的时候,所有的缓存页都在Free链表中,一旦这个缓存页缓存了数据,就会从缓存链表中移除,缓存链表的节点就是控制块。

被修改过的数据,需要刷新到持久化存储,为了提升效率,避免低效的轮训,Buffer Pool还设计了Flush链表,原理和Free链表基本一致。

在数据库查询过程中,InnoDB引擎需要知道持久化存储上的数据页是否已经缓存在Buffer Pool,InnoDB用Hash表存储这个映射关系,Hash表的Key就是表空间+持久化存储的页号,value是缓存页的地址。

因为Buffer Pool的空间有限,一般要比持久化存储的空间低一个数量级以上,所以Buffer Pool需要管理缓存数据,定期或者在需要的时候需要将Buffer Pool中的缓存页转换到持久化存储,从而可以缓存新的页面。

InnoDB使用LRU算法来管理缓存的更新以及淘汰。

但是相较于传统的LRU算法,InnoDB做了以下有化:

LRU链表分为两个部分,分别是冷数据区和热数据区,当数据第一次访问被缓存到缓存页的时候,只会进入冷数据区,只有此后一段时间内(默认1s)再次被访问,才会被移动到热数据区。这个优化可以避免临时读入的数据影响索引数据等真正热点的数据。LRU链表中冷热区域的分界点称为midpoint位置,默认情况下,midpoint位置位于LRU链表67%的位置,该位置之前数据为热点区。在缓存页不足需要淘汰一些缓存页时,就是从LRU链表中的冷数据区域的尾部开始将缓存页刷入磁盘,提供给加载新的数据页使用。

一般热数据区域里的缓存页是被经常访问的,所以频繁移动会带来一定的性能损耗, InnoDB引擎只有热数据区域的后3/4部分的缓存页被访问了,才会给你移动到链表头部。假设热数据区域的链表里有100个缓存页,那么排在前面的25个缓存页,即使被访问了,也不会移动到链表头部去的。但是对于排在后面的75个缓存页只要被访问,就会移动到链表头部去。

除了常规16K大小的缓存页,InnoDB引擎1.0之后就支持压缩页的功能,将默认16K的页压缩为1KB,2KB,4KB和8KB等。对于非16K的页面,通过unzip_LRU列表进行管理的,申请新页面的时候通过伙伴算法进行分配。例如申请4KB的页面,先在4k的unzip_LRU查找,如果有直接使用,否则的话在8KB的unzip_LRU列表中查询,如果可以找到空闲页,则存放在2个4KB页面到unzip_LRU中,如果8KB没有空闲页,则查找16KB的空闲页,分为1个8KB,2个4KB的页面放到对应的unzip_LRU列表。
InnoDB内存模型

redo log日志也会先放入缓冲区,成为redolog缓冲,但是redolog刷新频率较高,一般情况每秒都会刷新,所以该缓冲区空间不会特别大,默认8MB。

Buffer Pool中保存的数据主要包括索引页,数据页,传入缓冲,锁信息等。

  • 索引页

索引是数据库进行数据查询的辅助结构,对查询性能的影响非常密切。

假如数据库的主键是bigint(8个字节),索引中指向下一层节点的指针为6个字节,那么索引中一条记录是14字节,一个缓存页16K可以保存的记录数是16*1024/14=1170条。

所以对于一个表,如果是两层B+树索引占用内存一共会有(1+1170)*16K≈20M。

三层B+树索引占用的内存大约是(1+1170+1170*11170)*16K≈20G。

数据库中一般有多张表,而且除了主键索引之外,还会有其他的辅助索引,为了使索引页常驻内存,要求B+树的高度不要超过2,所以常见的B+树加上叶子层的数据,一般是3层。

  • 插入缓冲

因为非聚簇索引是随机写,为了提高插入的性能,会把数据先存储在Insert Buffer对象中,定时将Insert Buffer和辅助索引的子节点合并,但是只有非唯一的辅助索引才可以使用插入缓冲,因为在Insert Buffer中没有办法校验索引的唯一性。

除了Insert buffer,目前InnoDB引擎还有Update buffer等。统称change buffer,change Buffer也使用B+树存储。

  • 锁信息

InnoDB引擎使用的锁都在Buffer Pool的内存区进行分配。

除了Buffer Pool之外,InnoDB模型还保存其他的内存区域,包括redolog缓冲区,double write区域。

innodb的页面大小一般是4kb,8kb,16kb,32kb或者64kb,而通用的磁盘院子写入的单位是sector size,一般是512字节,所以数据页写入的过程中,如果出现宕机会出现部分数据写入成功的场景,违反了数据页原子性的要求。所以写入数据的时候,会首先写入double write专门的区域,然后再写数据页。

数据模型

InnoDB写入的过程

缓冲区的页面定时刷新到磁盘的过程称为checkpoint,为了避免内存数据的丢失,数据库普遍采用WAL(Write Ahead Log)的策略,即先写日志,然后再写缓冲区的页。



事务支持

InnoDB引擎支持事务,事务分为ACID

原子性:undo log

持久性:redo log

一致性:undo log + redo log

隔离性:锁 + MVCC

数据库是通过redolog支持持久性的。InnoDB引擎支持插入更新的过程中,在写入缓冲区的时候会通过WAL的方式首先记录redo日志。

InnoDB在崩溃的时候,redo日志没有提交标志,会进行对应的回滚操作。

在事务提交的过程,引擎会多次写入redolog,但是binlog会在事务提交成功之后一次写入,提前保存在binlog cache中。

事务的原子性是通过undo日志实现的,每次数据修改之前,程序会把修改之前的数据保存到undo log。

除了原子性之外,undolog还用来实现MVCC。

Mysql中的锁

Mysql中的锁分为多种类型。

为了提高并发性能,InnoDB引擎引入了多版本控制协议(MVCC),如果读取数据的行正在执行写操作,读操作不需要等待行锁的释放,可以通过去undo日志查询快找进行。

共享锁和排他锁

InnoDB引擎实现了行级的共享锁以及排他锁,简称为S锁和X锁。其中S锁运行持有锁的事务执行查询操作,同一行记录可以同时有多个S锁,X锁运行持有锁的事务进行记录的更新/删除等。

意向锁

意向锁是表级锁,表达事务想要的锁类型。意向锁可以提升数据库的性能,例如事务1使用排他锁锁定了数据表的一行记录,如果没有意向锁,事务2使用排他表锁锁定整个数据表的时候,没办法方便的判断数据表中的记录是否有被其他事务锁定。

有两种类型的意向锁

IS,共享意向锁,当事务希望获取某一行的S锁,则必须先申请表级别的IS锁。

IX,共享排他锁,当事务想要获取某一行的X锁,则必须先申请表级别的IX锁。

场景1

A使用排他行锁锁住了user表的id=5的这行数据,并对user表加了意向排他表锁。

B使用排它/共享表锁(比如上面说的lock tables ...read/write)去锁user表,因为A表存在意向排他表锁,则加锁失败。

即IX和X与S都冲突

场景2

若A使用共享锁锁住了user表的id=5的这行数据,并对user表加了意向共享表锁。

若B使用共享表锁(比如上面说的lock tables ...read)锁user表,而共享锁与意向共享锁是兼容的,所以B加锁成功。

若B使用排他表锁(比如上面说的lock tables ...write)锁user表,而排他表锁与意向共享表锁是冲突的,所以B加锁失败。

即IS与X冲突,与S兼容。

A申请user表的意向共享表锁,并锁住id=5这行数据

B申请user表的意向共享表锁,并锁住id=6这行数据

B申请user表的意向排他表锁,并锁住id=7这行数据

也就是因为意向锁之间相互兼容,所以意向锁对行级锁之间是不冲突的。

X IX S IS
X Confict Confict Confict Confict
IX Confict Compatible Confict Compatible
S Confict Confict Compatible Compatible
IS Confict Compatible Compatible Compatible
间隙锁

间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。

产生间隙锁的条件(RR事务隔离级别下;):

  1. 使用普通索引锁定;
  2. 使用多列唯一索引;
  3. 使用唯一索引锁定多行记录。

对于唯一索引而言:

  1. 对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE `id` = 5 FOR UPDATE;
  2. 对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;

对于非唯一索引:

  1. 在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样;
  2. 在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序。