揭秘InnoDB插入缓冲:提升非唯一辅助索引插入性能的秘密武器

本文为《MySQL归纳学习》专栏的第七篇文章,同时也是关于《MySQL缓存》知识点的第二篇文章。

相关文章:

InnoDB缓冲池揭秘:MySQL中的数据缓存利器

想要提高数据库的插入性能吗?本文揭示了InnoDB存储引擎的秘密武器------插入缓冲(Insert Buffer)。通过插入缓冲,InnoDB存储引擎可以显著提升非唯一辅助索引的插入性能。本文详细解析了插入缓冲的原理和内部实现,带你深入了解这项性能优化技术。同时,我们也会探讨插入缓冲可能带来的问题,并介绍其升级版------变更缓冲(Change Buffer)。

接下来我们进入正文。

insert buffer 是InnoDB存储引擎所独有的功能。通过insert buffer,InnoDB存储引擎可以大幅度提高数据库中非唯一辅助索引的插入性能。

数据库对于自增主键值的插入是顺序的,因此插入能有较高的性能。

SQL 复制代码
create table t (
   id int auto_increment,
   name varchar(30),
   primary key (id))
}

id列式自增长的,即当执行插入操作时,id列会自动增长,页中行记录按id顺序存放,不需要随机读取其它页的数据。因此,在这样的情况下(即聚集索引),插入操作效率很高。

但是实际生产环境中,用户表中主键仅有并且只能有1个,然而表中可能存在多个辅助索引。如下所示:

SQL 复制代码
create table t (
   id int auto_increment,
   name varchar(30),
   primary key (id),
   key (name));
}

在进行插入操作时,数据页的存放还是按主键 id 进行顺序存放的,但是对于非聚集索引叶子节点的插人不再是顺序的了,这时就需要离散地访问非聚集素引页,由于随机读取的存在而导致了插入操作性能下降。当然这并不是 name 字段上索引的错误,而是因为 B+树的特性决定了非聚集索引插人的离散性。

为什么说非聚集索引插入操作会导致性能下降?

非聚集索引的离散写操作涉及到在索引页中插入新的键值对。由于非聚集索引页的分散存储,插入操作通常会导致非聚集索引页的分裂或调整,以保持B+树的平衡和有序性(在前文我们学习自增主键时,提到过自增主键可以防止页分裂)。这种非顺序的写入会引发随机写入,而随机写入的性能通常较差。

插入缓冲原理

为了解决这个问题,InnoDB设计出了插入缓冲技术,对于非聚集类索引的插入和更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先将插入的记录放到insert buffer中,然后根据一些算法将insert buffer 缓存的记录通过后台线程慢慢的合并(merge)回辅助索引页中。这样做的好处是:

(1)减少磁盘的离散读取;

(2)将多次插入合并为一次操作。

例如name字段的插入顺序为:

SQL 复制代码
('Maria',10), ('David',7), ('Tim', 11), ('Jim', 7), ('Monty', 10), ('Herry', 7), ('Heikki', 7) 

后面的数字表示原先插入的辅助索引对应的数据页码 page_no,可以看到页的访问是完全无序的,然而当插入到insert buffer中时,记录根据应插入辅助索引的叶子节点 page_no 进行排序,故上述记录在insert buffer中的状态应为:

SQL 复制代码
('David',7), ('Jim', 7), ('Herry', 7), ('Heikki', 7) , ('Maria',10), ('Monty', 10), ('Tim', 11)

当要进行合并时,页page_no为7的记录有4条,可以一次性将这4条记录插入到辅助索引中,从而提高数据库的整体性能。

insert buffer的使用需要满足以下两个条件:

(1)索引是辅助索引(secondary index)

(2)索引是非唯一的

若是唯一索引,那么在插入时需要判断插入的记录是否是唯一,这需要读取辅助索引页,而 insert buffer 的设计就是避免读取insert buffer,这会导致失去insert buffer 的设计意义。

通过如下命令可以查看插入缓冲的信息:

SQL 复制代码
show engine innodb status;

插入缓冲的内部实现

insert buffer 的数据结构是一棵B+树。在 MySQL4.1之前的版本中每张表都有一棵 insert buffer B+树。而在现在的版本中,全局只有一棵insert buffer B+树,负责对所有的表的辅助索引进行 insert buffer。这棵B+树存放在共享表空间中,默认也就是ibdata1中。因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致check table 失败。这是因为表的辅助索引中的数据可能还在insert buffer中,也就是共享表空间中。所以通过idb文件进行恢复后,还需要进行repair table 操作来重建表上所有的辅助索引。

insert buffer是一棵B+树,因此其也由叶子节点和非叶子节点组成。非叶子节点存放的是查询的search key(键值)。其构造包括三个字段:

search key一共占9字节,其中space占4字节,marker占1字节、offset占4字节。space表示待插入记录所在的表空间id,在InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表,marker是用来兼容老版本的insert buffer,offset表示插入缓冲中的页在磁盘数据文件中的偏移量。

当一个辅助索引需要插入到页(space,offset)时,如果这个页不在缓冲池中,那么InnoDB存储引擎首先根据上述规则构造一个search key,接下来查询insert buffer这棵B+树,然后再将这条记录插入到insert buffer B+树的叶子节点中。

对于插入到insert buffer B+树叶子节点的记录,需要根据如下规则进行构造:

space、marker、page_ no 字段和之前非叶节点中的含义相同,一共占用9字节。第 4个字段 metadata 占用4字节,其存储的内容如下图所示。

IBUF_REC_OFFSET_COUNT 是保存两个字节的整数,用来排序每个记录进入 Insert Buffer 的顺序。因为从 InnoDB1.0.x 开始支持 Change Buffer,所以这个值同样记录进入 Insert Buffer 的顺序。通过这个顺序回放(replay)才能得到记录的正确值。

Secondary index record 记录的是插入的数据,相较于原插入数据,insert buffer B+树叶子节点额外多了 13个字节的开销。为何要如此设置呢?

因为启用 insert buffer索引后,辅助索引页(space、page_no)中的记录可能被插入到insert buffer B+树中,所以为了保证每次merge insert buffer页必须成功,还需要有一个特殊的页来标记每个辅助索引页(space、page_no)的可用空间。这个页的类型为insert buffer bitmap。

每个 Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区 (Extent )。每个 Insert Buffer Bitmap 页都在16384个页的第二个页中。关于 Insert Buffer Bitmap 页的作用会在下一小节中详细介绍。

相关知识点:

  • 每个区64页,一页16KB,一个区所占大小为: 16 * 64 = 1024KB = 1M
  • 256个区,一个区64页,则一个BiMap可以追踪的页为:256 * 64 = 16384

每个辅助索引页在 Insert Buffer Bitmap 页中占用4位(bit),由三个部分组成。

Merge insert buffer

merge insert buffer的操作可能发生在以下几种情况:

(1)辅助索引页被读取到缓冲池时;

(2)insert buffer bitmap页追踪到该辅助索引页已无可用空间时;

(3)master thread。

第一种情况好理解,如果该辅助索引页是否有记录存放于Insert Buffer B+树中,即存在脏页,辅助索引页被读取到缓冲池时,那么就需要进行数据同步。可以通过检查 Insert BufferBitMap页中的 IBF_BITMAP_BUFFERED,最终将脏页数据更新到辅助索引页中。某些情况下,对该页多次的记录操作可以通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。

第二种情况,insert buffer bitmap 追踪到该辅助索引页已无可用空间时(启用insert buffer后,辅助索引页中的记录可能被插入到insert buffer B+树中,为了保证每次的merge insert buffer页成功,通过insert buffer bitmap类型的特殊页来记录辅助索引页的可用空间),并至少得有1/32 的可用空间。若往辅助索引页插入一条记录后,发现空间少于 1/32 ,则会强制读取辅助索引页,将Insert Buffer B+树中,该页的记录插入到辅助索引页中

为什么辅助索引页的可用空间要至少大于 1/32 的空间,这个阈值是如何确定的?

个人理解如下:

按照一般情况来说,一页为16K,16 * 1024 / 32 = 512 B。从磁盘的物理结构来看存取信息的最小单位是扇区,一个扇区大小为 512 B。

在MySQL的内部实现中,数据被组织为数据页,每个数据页通常由多个连续的扇区组成。当MySQL需要读取或写入数据时,它会以扇区为单位进行磁盘IO操作。读取或写入的最小单元是一个或多个扇区。

这个阈值的选择是基于性能和空间利用的考虑。当辅助索引页的空间少于1/32时,可能意味着辅助索引页的空间已经相对较小,无法容纳更多的记录。如果不及时进行合并操作,后续插入操作可能会带来页分裂等问题。

第三种情况,此 master thread 线程中每秒或者每10秒进行一次merge insert buffer操作,不同之处在于每次merge的数量不一样。

疑问

1、插入缓冲的节点中offset表示插入缓冲中的页在磁盘数据文件中的偏移量,在执行插入操作时,是如何得知这一信息的?是否需要进行I/O操作?

2、插入缓冲的叶子节点中IBUF_REC_OFFSET_COUNT 字段表示每个记录进入 Insert Buffer 的顺序,既然叶子节点有序,为什么还需要该字段呢?

插入缓冲带来的问题

插入缓冲主要带来如下两个坏处:

1、可能导致数据库宕机后实例恢复时间变长。如果应用程序执行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出现宕机,这时就有大量内存中的插入缓冲区数据没有合并至索引页中,导致实例恢复时间会很长。

2、在写密集的情况下,插入缓冲会占用过多的缓冲池内存(innodb_buffer_pool),默认情况下最大可以占用1/2,这在实际应用中会带来一定的问题。

插入缓冲的升级:change buffer

InnoDB从1.0.x版本开始引入了change buffer,可以将其视为insert buffer的升级。从这个版本开始,InnoDB存储引擎可以对DML操作------insert、delete、update都进行缓冲,它们分别是:insert buffer、delete buffer、purge buffer。

和insert buffer一样,change buffer适用的对象依然是非唯一的辅助索引。

对一条记录进行update操作可以分为两个过程:

(1)将记录标记为删除;

(2)真正将记录删除。

因此delete buffer对应update操作的第一个过程,即将记录标记为删除。purge buffer对应update操作的第二个过程,即将记录真正的删除。同时InnoDB存储引擎提供了参数innodb_change_buffering,用来开启各种buffer选项。该参数的可选值为:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况。changes表示启用inserts和deletes,all表示启用所有,none表示都不启用。

从 InnoDB 1.2.x版本开始,可以通过参数 innodb_change_buffer_max_size 来控制 Change Buffer 最大使用内存的数量:

SQL 复制代码
mysql> show variables like 'innodb_change_buffer_max_size';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
+-------------------------------+-------+
1 row in set (0.18 sec)

innodb_change_buffer_max_size 值默认为25,表示最多使用1/4 的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50。

关于 change buffer 的状态信息,仍然通过下面命令查看:

SQL 复制代码
show engine innodb status;

参考文献

《MySQL技术内幕 InnoDB存储引擎 第2版》

相关推荐
wn53143 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1231 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper2 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文3 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people3 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
这孩子叫逆8 小时前
6. 什么是MySQL的事务?如何在Java中使用Connection接口管理事务?
数据库·mysql
罗政8 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师10 小时前
spring获取当前request
java·后端·spring
掘根10 小时前
【网络】高级IO——poll版本TCP服务器
网络·数据库·sql·网络协议·tcp/ip·mysql·网络安全
Java小白笔记11 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis