InnoDB存储引擎

InnoDB:”in-no-db”


第一个完整支持ACID事务的引擎,还支持行锁设计MVCC外键提供一致性非锁定读

从MySQL 5.1开始,MySQL数据库允许存储引擎开发商以动态形式加载引擎,这样存储引擎的更新可以不受MySQL数据库版本的限制。MySQL 5.1中,可以支持两个版本的InnoDB,一个是静态编译的InnoDB版本,可以将其看作老版本的InnoDB,另外一个是动态加载的InnoDB版本,官方称为InnoDB plugin,或者InnoDB 1.0

各版本之间的比较

  • 老版本的InnoDB:支持ACID,行锁设计, MVCC
  • InnoDB 1.0x:继承了上述版本的所有功能,增加了compress和dynamic页格式
  • InnoDB 1.1x:继承了上述版本的所有功能,增加了linux AIO,多回滚段
  • InnoDB 1.2x:继承了上述版本的所有功能,增加了全文索引支持,在线索引添加

InnoDB体系结构InnoDB体系结构
内存池就是很多块内存组成的大内存,负责维护缓存磁盘上的数据,重做日志缓冲,维护内部数据等
后台线程负责刷新内存池的数据,保证缓存的是最新的数据;将内存数据刷新到磁盘中等

后台线程

前面说过InnoDB是一个单进程多线程的模型,后台不同的线程负责不同的任务

Master Thread:非常核心的后台线程,负责将缓冲池中的数据异步刷新到硬盘中,保证数据一致性,包括脏页的刷新、合并插入、UNDO页的回收等

IO Thread:InnoDB引擎中大量使用AIO(异步IO)来处理IO请求。从InnoDB 1.0.X版本之后,read thread和write thread分别增到4个,除此之外还有insert buffer和log IO thread

1
2
3
4
5
6
7
8
mysql> SHOW VARIABLES LIKE 'innodb_%io_threads';
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_read_io_threads | 4 |
| innodb_write_io_threads | 4 |
+-------------------------+-------+
2 rows in set (0.00 sec)

SHOW ENGINE INNODB STATUS;:可以查看IO thread状态

Purge Thread:清除、净化的意思,事务被提交后,undolog不再需要,因此需要Purge Thread回收undo页

1
2
3
4
5
6
7
mysql> SHOW VARIABLES LIKE 'innodb_purge_threads';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| innodb_purge_threads | 4 |
+----------------------+-------+
1 row in set (0.01 sec)

Page Cleaner Thread:将脏页刷新操作放入单独的线程中完成

内存

缓冲池

缓冲池

InnoDB存储引擎是基于磁盘的,由于CPU和磁盘速度之间的鸿沟,基于磁盘的DBMS通常需要缓冲池技术来提高性能

页从缓冲池刷新回磁盘的操作并不是每次页更新时触发,而是通过Checkpoint机制刷新磁盘。SHOW VARIABLES LIKE 'innodb_buffer_pool_size';来查看缓冲池大小

缓冲池不只是缓存索引页和数据页,还包括undo页、插入缓冲、自适应哈希索引、InnoDB存储的锁信息、数据字典信息

缓冲池的数量也可以自由设定,每个页根据hash值平均分配到不同缓冲池实例中。这样的目的就是增加数据库的并发处理能力。SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';查看缓冲池数量,也可以修改配置文件中的innodb_buffer_pool_instances大于1,以获取多个缓冲池实例

LRU

InnoDB通过LRU(Latest Recent Used,最近最少使用)算法来管理缓冲池。当缓冲池放不下新读取的页时,就会释放LRU列表尾端的页。但是新读取的页不是直接放入LRU列表的首部,而是midpoint,大约在列表5/8处,在midpoint之后的表称为old列表,之前的表称为new列表,SHOW VARIABLES LIKE 'innodb_old_blocks_pct';查看midpoint值,SET GLOBAL innodb_old_blocks_pct=20设置该参数,以降低old列表的比例

使用midpoint的原因:有些SQL操作,如索引或数据的扫描,这些操作会访问表中的很多页,这些页只在这次查询中有用,并不是活跃的热点数据,但是大量这种页如果放入首部,会使得所有页都被刷新出缓冲池

除此之外,还引入了innodb_old_blocks_time参数,其表示页读到mid位置后需要等待多久后才会加入LRU列表的热端,通过SET GLOBAL innodb_old_blocks_time=1000;设置该参数

当页从LRU列表的old部分加入new部分称为page made young,因为innodb_old_blocks_time导致没有从old移动到new称为page not made young

数据库刚启动时,LRU列表为空,页都存放在Free列表中。此时需要用到的时候直接将Free列表中的页删除,在LRU列表中增加相应的页,维持页数守恒

通过SHOW ENGINE INNODB STATUS;命令可以查看缓冲池的所有信息,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 102398
Buffer pool size 8191
Free buffers 7942
Database pages 249
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 215, created 34, written 36
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 249, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

可以看出Buffer pool共8191个页,Free buffers表示Free列表共7942个页,Database pages即是LRU列表,共249个页。Pages made young表示从LRU列表中页移动到前端的次数

除了上面的命令还可以通过表INNODB_BUFFER_POOL_STATS来观察缓冲池状态

1
2
mysql>SELECT pool_id,hit_rate, pages_made_young,pages_not_made_young
>FROM information_schema.INNODB_BUFFER_POOL_STATS;

InnoDB好提供压缩页功能,原本16KB的页压缩为1KB、2KB、4KB、8KB,也因此需要unzip_LRU列表进行管理,且对不同大小的页进行分别管理,例如要向Buffer pool申请4KB的大小

  • 检查4KB的unzip_LRU列表
  • 如果有可用的空闲页直接使用,否则检查8KB的unzip_LRU列表
  • 如果有空闲页,将页分为2个4KB页,存放到4KB的unzip_LRU列表之中
  • 如果没有,则申请16KB…

在LRU列表中的页被修改之后,称该页为脏页,即缓冲池的页和磁盘中的页数据不一致。Flush列表的页即为脏页列表,Modified db pages就表示脏页的数量

重做日志缓冲

重做日志(redo log):确保事务的持久性(保证数据库的事务可以被重演)。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性

InnoDB首先将日志信息放入这个缓冲区,然后按照一定频率(一般为1S)将其刷新到重做日志文件,该缓冲区大写通过innodb_log_buffer_size设置

下面三种情况会将缓冲区的重做日志刷新带外部磁盘的重做日志文件中

  • Master Thread定期刷新
  • 每个事务提交时
  • 重做日志缓冲区剩余空间小于1/2时

额外的内存池

在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身分配内存时,需要从额外的内存池中申请,当该区域的内存不够时,会从缓冲池中申请。每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了诸如LRU、锁、等待等方面的信息,而这个对象的内存需要从额外内存池中申请

Checkpoint

当前数据库都采取write ahead log策略,即当事务提交时,先写重做日志,在修改页

Checkpoint技术为了解决以下问题

  • 缩短数据库的恢复时间,因为这样数据库只要对Checkpoint后的重做日志进行恢复
  • 缓冲池不够用时, 将脏页刷新到磁盘
  • 重做日志不可用时,刷新脏页。重做日志是循环使用,新的日志会覆盖旧的日志,因此当重做日志不够时,就要强制产生Checkpoint,将缓冲池的页至少刷新到当前重做日志的位置

LSN(Log sequence number)标记版本,每个页、重做日志、Checkpoint都有版本

Checkpoint的困难之处在于如何决策每次刷新几页到磁盘,每次从哪里取脏页和什么时候出发Checkpoint

Checkpoint分为SharpFuzzy Checkpoint。Sharp将所有脏页刷新到磁盘,发生在数据库关闭的时候
Fuzzy发生在以下四个场景

  • master thread checkpoint
  • flush lru list
  • async flush 保证重做日志的循环使用的可用性???
  • dirty page too much

Master thread???

内部有多个loop组成:loop、backgroup loop、flush loop、suspend loop,Master Thread根据数据库运行状态在各个loop中切换

InnoDB关键特性

插入缓冲

Insert Buffer和数据页一样,也是物理页的一个组成部分

在插入操作时,数据页的存放主要还是按照Primary Key,但是对于非聚集索引叶子节点的插入则不是顺序的,这时需要离散访问非聚集索引页,由于随机读取(当存储器的消息被读取时,写入所需时间与数据位置无关)的存在导致插入操作性能下降,这主要是因为B+树的特性决定了非聚集索引插入的离散性

B+树的叶节点是有序的。当它用于聚集索引的时候,叶节点本身既是索引又是真实值。当它用于非聚集索引的时候,叶节点仅仅是索引,索引的指针指向的才是真实值。由于此时索引是有序的,因此其指向通常是无序的,所以两个连续的索引值可能对应的真实值所在的行可能会离得很远。

举个例子,一个表用整数id作为主键,且将主键当做聚集索引。此时再用表中的另一列age当做非聚集索引。由于表的行本身就是按主键排序的,因此age是无序的,所以age=10的行可能在第八行,而age=11的行却可能位于第三十行,差别很大。所以在插入的时候就无法做到连续的索引插入到连续的行中,而只能一条一条地定位和插入

Insert Buffer对于非聚集索引的插入或者更新操作,不是每一次直接插入到索引页中。而是先判断插入的非聚集索引是否在缓冲池中,若存在,直接插入,若不在,先放入Insert Buffer对象中,再以一定的频率和情况进行Inser Buffer和辅助索引页子节点的merge操作,这时通常能将多个插入合并到一个操作中

只有满足索引是辅助索引索引不是唯一的,InnoDB才会使用Insert Buffer。辅助索引不能是惟一的,因为数据库不会去查找索引页来判断插入的记录的唯一性,如果去查找就会有离散读取的情况发生,Insert Buffer就失去了意义

通过SHOW ENGINE INNODB STATUS;命令可以查看Insert Buffer信息,merges表示合并的次数

1
2
3
4
5
6
7
8
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0

在写密集情况下,插入缓存会占用过多的缓冲池内存,可以通过修改IBUF_POOL_SIZE_PER_MAX_SIZE对插入缓冲的大小进行控制,如果设置为3,则最大只能使用1/34

现在引入了Change Buffer,InnoDB对INSERT、DELETE、UPDATE都能进行缓冲,分别是Insert Buffer、Delete Buffer、Purger Buffer。如上述代码中表示,insert表示Insert Buffer,delete mark表示Delete Buffer,delete表示Purge Buffer,discarder operations表示当Change Buffer发生merge时,表已经被删除,此时就无需在将记录合并

Insert Buffer的数据结构就是一颗B+树,而且全局只有一棵,负责对所有表的辅助索引进行Insert Buffer

此B+树的非叶节点存放search key,构造如下图Insert Buffer

search key共九个字节,其中space表示待插入记录所在表的表空间id,每个表都有一个唯一的space id,offset表示页所在的偏移量,占用4字节

当一个辅助索引要插入页(space,offset)时,如果这个页不在缓冲池中,那InnoDB先构造一个search key,接下来将这条记录插入Insert Buffer B+树的叶子节点中,上图第二行是叶子节点的结构,metadata占用4个字节,其中两个字节保存IBUF_REC_OFFSET_COUNT,该值用来记录每个记录进入Insert Buffer的顺序

为了保证每次 Merge Insert Buffer页必须成功,还需要有一个特殊的页用来标记每个辅助索引页的可用空间,这个页的类型为Insert Buffer Bitmap。每个辅助索引页在Insert Buffer Bitmap占用4位,其中两位IBUF_BITMAP_FREE表示该辅助索引页的可用空间数量,例如00表示无可用空间,01表示剩余空间大于1/32页等等

最后思考Merge Insert Buffer何时发生

  • 辅助索引页被读取到缓冲池:例如在执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap,然后确认该辅助索引页是否有记录存放在Insert Buffer B+树中,若有则要进行合并
  • Insert Buffer Bitmap页追踪到该辅助索引页已经没有可用空间
  • Master Thread:在Master Thread中,执行merge操作的页数由srv_innodb_io_capacity的百分比决定要真正合并多少辅助索引页

两次写

doublewrite带给InnoDB的是数据页的可靠性

当InnoDB正在写入某个页到表中,如果只写了一部分就发生宕机,这种情况就叫部分写失效

doublewrite就是在应用重做日志之前,用户需要一个页的副本,在写入失效发生时,先通过页的副本还原该页,在重新重做
doublewrite

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页复制到内存中的doublewrite buffer,之后在通过doublewrite buffer分两次顺序写入共享表空间的物理磁盘上,然后马上调用fsync函数(系统调用fsync将所有已写入文件描述符fd的数据真正的写道磁盘或者其他下层设备上,Linux文件系统可以使数据在写入磁盘前先在内存中保留几秒,以此更高效率的处理磁盘I/O),同步磁盘。完成之后,再将doublewrite buffer中的页写入各个表空间文件中

如果OS在将页写入磁盘的过程中发生崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个 副本,将其复制到表空间文件,在应用重做日志

自适应哈希索引

InnoDB会监控表上各索引页的查询,如果观察到建立哈希索引可以带来速度的提升,就会建立哈希索引,这就叫自适应哈希索引(Adaptive Hash Index,AHI),AHI根据缓冲池的B+树构造起来,InnoDB会自动根据访问的频率和模式自动为某些热点页建立哈希索引

异步IO

用户可以发出一个IO请求之后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。和AIO对应的是Sync IO,即进行一次IO操作,需要等待此次操作结束才能继续接下来的操作

AIO另一个优势是进行IO merge,也就是将多个IO合并为一个IO,例如用户需要访问连续的三个页,AIO会判断这三个页是连续的,因此AIO底层会发送一个IO请求,直接读取三个页

InnoDB提供了内核级别的AIO支持,成为Native AIO,这需要libaio库支持

在InnoDB中,read ahead方式的读取都是AIO,脏页的刷新,即磁盘的写入操作也是AIO

刷新邻接页

当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,则一起刷新

启动、关闭和恢复

在关闭时,参数innodb_fast_shutdown影响InnoDB的行为

  • 0:在关闭时,InnoDB需要完成所有full purge和merge insert buffer,并且将所有脏页刷新到磁盘中
  • 1:不需要完成上述的full purge和merge insert buffer,但是在缓冲池的一些数据脏页会是会刷新回磁盘
  • 2:不需要完成上述的full purge和merge insert buffer。也不将缓冲池的数据脏页写回,但是将日志都写入日志文件

参数innodb_force_recovery影响了整个InnoDB存储引擎恢复的状态

  • 0:为默认值,代表当发生需要恢复时,进行所有的恢复操作,当不能有效恢复时,如数据页发生了corruption,MySQL就可能发生宕机,并把错误写入错误日志
  • 1:忽略检查到的corrupt页
  • 2:阻止Master Thread线程的运行
  • 3:不进行事务的回滚操作
  • 4:不进行插入缓冲的合并操作
  • 5:不查看撤销入职,InnoDB存储引擎会将未提交的事务视为已提交
  • 6:不进行回滚的操作

当该参数设置大于0后,但是insert、update、delete这类DML操作是不允许的,只能进行select、create和drop