InnoDB存储引擎的关键特性包括插入缓冲、两次写(double write)、自适应哈希索引(adaptive hash index)。这些特性为InnoDB存储引擎带来了更好的性能和更高的可靠性。
问题引入
比如说我们按下列SQL定义的表:
create table t
(id int auto_increment,
name varchar(30),
primary key(id));
id列是自增长的,这意味着当执行插入操作时,id列会自动增长,页中的行记录按id执行顺序存放。一般情况下,不需要随机读取另一页执行记录的存放。因此,在这样的情况下,插入操作一般很快就能完成。
但是,不可能每张表上只有一个聚集索引,在更多的情况下,一张表上有多个非聚集的辅助索引(secondary index)。比如,我们还需要按照name这个字段进行查找,并且name这个字段不是唯一的。
表是按如下的SQL语句定义的:
create table t
(id int auto_increment,
name varchar(30),
primary key(id),key(name));
这样的情况下产生了一个非聚集的并且不是唯一的索引。在进行插入操作时,数据页的存放还是按主键id的执行顺序存放,但是对于非聚集索引,叶子节点的插入不再是顺序的了。这时就需要离散地访问非聚集索引页,插入性能在这里变低了。然而这并不是这个name字段上索引的错误,因为B+树的特性决定了非聚集索引插入的离散性。
InnoDB存储引擎开创性地设计了插入缓冲,对于非聚集索引的插入或更新操作,不是每一次直接插入索引页中,而是先判断插入的非聚集索引页是否在缓冲池中。
插入缓冲
在进行数据插入时必然会引起索引的变化,聚集索引不必说,一般都是递增有序的。而非聚集索引就不一定是什么数据了,其离散性导致了在插入时结构的不断变化,从而导致插入性能降低。
所以为了解决非聚集索引插入性能的问题,InnoDB引擎 创造了Insert Buffer。
插入缓冲,并不是缓存的一部分,而是物理页,对于非聚集索引的插入或更新操作,不是每一次直接插入索引页.而是先判断插入的非聚集索引页是否在缓冲池中。
如果在,则直接插入,如果不在,则先放入一个插入缓冲区中.然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作.
插入缓冲的使用需要满足以下两个条件:
1.索引是辅助索引。
2.索引不是唯一的。
先解释一下第一点,辅助索引指的是所有除了主键索引之外的其他索引。而主键索引的插入一般都是有序插入,如id、流水单号、交易单号等等,这种插入一般带来的都是顺序写,io写入性能高。而辅助索引下的写入是有可能是顺序写,但大部分情况下是随机写,若每次插入数据最坏情况下都执行一次随机写,那将耗费比较多的时间。
第二点该索引是非唯一的,因为根据insert buffer的概念,我们的数据会先自己在缓存池中先构造一个B+树插入索引,然后在一定的时间内由master thread进行插入。若该索引必须要唯一的,那么我们的数据在插入缓存池的B+树时候就会访问一次磁盘,查询当前索引是否满足唯一性,这样和不经过缓存池直接插入到磁盘中没有什么分别,无法节省磁盘IO,故该索引不是唯一的。
插入缓冲是InnoDB存储引擎关键特性中最令人激动的。不过,这个名字可能会让人认为插入缓冲是缓冲池中的一个部分。其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是Insert Buffer和数据页一样,也是物理页的一个组成部分。
主键是行唯一的标识符,在应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。
当满足以上两个条件时,InnoDB存储引擎会使用插入缓冲,这样就能提高性能了。不过考虑一种情况,应用程序执行大量的插入和更新操作,这些操作都涉及了不唯一的非聚集索引,如果在这个过程中数据库发生了宕机,这时候会有大量的插入缓冲并没有合并到实际的非聚集索引中。如果是这样,恢复可能需要很长的时间,极端情况下甚至需要几个小时来执行合并恢复操作。
辅助索引不能是唯一的,因为在把它插入到插入缓冲时,我们并不去查找索引页的情况。如果去查找肯定又会出现离散读的情况,插入缓冲就失去了意义。
查看插入缓冲的信息:
show engine innodb statusG
Ibuf: Size 7545,free list len 3790,
seg size 11336,
8075308 inserts,7540969 merged recs,
2246303 merges
seg size显示了当前插入缓冲的大小为11336*16KB,大约为177MB。free list len代表了空闲列表的长度,size代表了已经合并记录页的数量。
inserts代表了插入的记录数字,merges recs代表合并的插入数,merges代表合并的次数,也就是实际读取页的次数。由该数据可以表明,merges:merged recs=1:3,插入缓存对于非聚集索引的离散IO请求降低了2/3.
潜在问题:
目前插入缓冲存在一个问题是,在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认情况下最大可以占用1/2的缓冲池内存。
聚簇索引的插入
首先我们知道在InnoDB存储引擎中,主键是行唯一的标识符。我们平时插入数据一般都是按照主键递增插入,因此聚集索引都是顺序的,不需要磁盘的随机读取。
CREATE TABLE test(
id INT AUTO_INCREMENT,
name VARCHAR(30),
PRIMARY KEY(id)
);
如上我创建了一个主键 id,它有以下的特性:
- Id列是自增长的
- Id列插入NULL值时,由于AUTO_INCREMENT的原因,其值会递增,同时数据页中的行记录按id的值进行顺序存放
- 一般情况下由于聚集索引的有序性,不需要随机读取页中的数据,因为此类的顺序插入速度是非常快的。
聚簇索引的插入一定是顺序的吗?
不一定,如果你把列 Id 插入UUID这种数据,那你插入就是和非聚集索引一样都是随机的了。这会导致你的B+ tree结构不停地变化,那性能必然会受到影响。
非聚簇索引的插入
很多时候我们的表还会有很多非聚集索引,比如我按照b字段查询,且b字段不是唯一的。如下表:
CREATE TABLE test(
id INT AUTO_INCREMENT,
name VARCHAR(30),
PRIMARY KEY(id),
KEY(name)
);
这里我创建了一个test表,它有以下特点:
- 有一个聚集索引 id
- 有一个不唯一的非聚集索引 name
- 在插入数据时数据页是按照主键id进行顺序存放,辅助索引 name的数据插入不是顺序的
- 非聚集索引也是一颗B+树,只是叶子节点存的是聚集索引的主键和name 的值。
因为不能保证name列的数据是顺序的,所以非聚集索引这棵树的插入必然也不是顺序的了,会带来离散IO的发生。
当然如果name列插入的是时间类型数据,那其非聚集索引的插入也是顺序的。
频繁的随机IO插入会造成性能下降
首先对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中。
若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中。
给外部的感觉好像是树已经插入非聚集的索引的叶子节点,而其实是存放在其他位置了
以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,通常会将多个插入操作一起进行merge,这就大大的提升了非聚集索引的插入性能。
总结
其实Insert Buffer的数据结构就是一棵B+树。
在MySQL 4.1之前的版本中每张表有一棵Insert Buffer B+树
目前版本是全局只有一棵Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer
如果在,则直接插入;如果不在,则先放入一个插入缓冲区中,好似欺骗数据库这个非聚集的索引已经插到叶子节点了,然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对非聚集索引执行插入和修改操作的性能。
实验表明,引入插入缓存之后,整个插入效率:原始插入效率为1:3,减少了2/3的IO次数。
作者Jerry