Fork me on GitHub
0%

change buffer 的理解以及唯一索引的选择

change buffer 介绍

在介绍 change buffer 之前,先来看一个 SQL 的执行时涉及索引树的更新过程,比如下面的这个插入语句,

1
insert into users(id, identity) values(1, 4504);

上面的 SQL 很简单,就是将 id = 1,identity = 4504 数据插入到 users 表里面,这里先声明下 users 表 identity 字段上建有普通索引,id 是主键字段。

在之前的文章 <深入理解 MySQL 索引(一) > 中已经提到,InnoDB 使用的是 B+ 树索引模型,数据都是存储在 B+ 树中的,表的每一个索引在 InnoDB 里面都对应于一棵 B+ 树,B+ 树的节点存储在数据页当中,一个页里面可以存放很多行数据。

如果不考虑现有的执行流程,我们来看插入这条数据时更新 identity 索引树的过程应该是怎样的,这里假设 identity = 4504 要存放的数据页 Page1 不在内存里面,简单的来说 identity 索引树的更新需要经历下面三个步骤:

1.从磁盘上读取出 identity = 4504 将要存放的数据页 Page1 到内存

2.将 identity = 4504,id = 1 的数据存放到相应的数据页 Page1上(内存操作)

3.将内存中更新之后的数据页 Page1 写回到磁盘上

上面三个步骤其中 1 和 3 是比较耗时的,涉及到从磁盘中随机读数据和随机写数据到磁盘,而步骤 2 是在内存中的操作,耗时基本可以忽略不计。既然上面提到不考虑 MySQL 现有的执行流程,那实际情况肯定不是这样的,对于步骤 3,在前面的文章 <CRUD 也可以更进一步——更新语句>里面介绍了 redo log,更新数据前先记录 redo log,再将数据写入内存,redo log 是顺序写,可以节省步骤 3 中随机写的时间,同时还能做到崩溃恢复的功能。

那就还剩步骤一里面的随机读,这时候 change buffer 就派上用场了,当要更新的数据页不在内存里面的时候(在内存中的时候直接更新内存,不需要用到 change buffer),可以直接将步骤二的操作记录在 change buffer 里面,然后记录 redo log 后直接返回。整个数据更新的流程就变成下面这样:

1.直接在 change buffer 记录对数据页 Page1 更新

2.将步骤 1 的操作记录 redo log

优化之后就只剩下步骤 1 的内存操作和步骤 2 里面的顺序写磁盘操作了,相比上面三个步骤执行时间明显减少。

从上面前后流程对比可以看到,change buffer 是在要更新的数据页不在内存里面时,直接将对数据页的修改记录在 change buffer 里面,而不需要从磁盘上读取数据页进行修改后再写入磁盘,减少了随机读取和写磁盘的时间,同时由于没有将数据页读入内存,也间接提高了内存的利用率。

需要注意的是 change buffer 里面的数据页修改记录并不只是存放在内存里面,它也是需要写到磁盘上的,只不过是在后台异步写到磁盘上,同样的对 change buffer 的操作也会记录 redo log,由于记录了 redo log,因此不需要担心数据页修改丢失的问题。

上面将对数据行的修改记录放在 change buffer 里面,而没有从磁盘上读取这部分数据做修改,并不是说就不去做随机读写的操作了,最终数据肯定都是要写到磁盘上的,只不过将这两个耗时的操作放到后台异步去执行,后台会有定时任务去执行这两个操作。还有一种情况是定时任务还没执行前就需要访问这一数据页时,这时会先将数据页读取到内存,然后应用 change buffer 中和这个数据页有关的修改记录,再将应用之后的数据返回,这样就能保证返回的是修改之后的数据。

将原有数据页应用 change buffer 里面的修改记录的这一过程称为 merge 的过程,总的来说触发 merge 的过程有下面几种情况:

1.定时任务执行前访问的数据页有变动,先将数据页读取到内存,然后应用 change buffer

2.后台定时任务定时执行

3.数据库正常关闭的过程

4.数据库异常关闭后再启动,通过 redo log 恢复数据时也可能会产生 merge 的操作

change buffer 和 redo log 的对比

1.redo log 优化的是更新数据页后随机写磁盘的过程,将随机写转变为顺序写,而 change buffer 优化的则是随机读磁盘的过程,直接将对数据页的修改放在 change buffer 里面,把随机读磁盘的操作从更新的流程里面省去了,使得更新语句的执行速度得到明显提升。

2.change buffer 里面的数据页修改记录并不只是存放在内存里面,它会被写到磁盘上,并且将对数据页的修改放在 change buffer 里面的这一动作需要记录 redo log,由于记录了 redo log,因此没有数据修改丢失的问题。

3.使用到 change buffer 时,真正对磁盘数据页的修改每次都是将磁盘上的数据页读取到内存,然后应用 change buffer 的修改,再将内存中更新后的数据页回写到磁盘上,而不是根据 redo log 来完成的。

4.redo log 里面包含了两种日志记录,一种是普通数据页的修改,也就是读取到内存里面的数据页变动,还有一种就是我们这里提到的对 change buffer 里面的操作,使用到 change buffer 说明数据页不在内存里面,因此不存在内存里面的数据页变动,只是在 change buffer 里面记录了一下数据页的变动而已,后续将 change buffer 里面记录的数据页变动应用到磁盘数据页上时,会将磁盘上的数据页读取到内存,然后在内存里面应用 change buffer 里面的修改,这一步会再次记录 redo log,这时记录就是第一种。

change buffer 的局限性

从上面 change buffer 的介绍来看,change buffer 的确大大提高了数据的更新速度,但也并不是任何时候都能使用 change buffer,比如说下面这些场景就没有办法使用,或者说没必要使用 change buffer。

1.要更新的数据行所在数据页本身就在内存里面,这时就没必要使用 change buffer 了,直接将内存里面的数据页修改即可

2.要更新的索引字段也是 where 后面的条件字段,这时也不需要使用 change buffer,因为 where 后面的条件字段本身就要将数据页读取到内存来判断是否符合条件,这时都已经读取出来了,更新的时候直接更新内存就可以

3.更新后的数据页紧接着就需要进行查询,这时也没有必要去使用 change buffer,因为在你更新之后立马查询,这时需要去读取磁盘上的数据页,然后应用 change buffer 里面的修改再返回,还不如在更新时将数据页读取到内存进行修改,紧接着的查询就可以直接将内存更新后的数据返回即可,因此一般对于那种写多读少的数据就很适合使用 change buffer,比如说日志数据

4.要更新的索引字段是唯一索引,这时也不适用,具体原因在看完下面的内容之后就知道了

唯一索引和普通索引的区别

对于唯一索引和普通索引,它们两者有什么区别呢,可能我们会说唯一索引表明该字段是全局唯一的,而普通索引则没有这个要求,当然这么说肯定没错,但这则是表面上的区别,唯一索引字段全局唯一这个特性相对普通索引会给查询和更新带来哪些变化呢。

查询时的区别

首先我们来看针对唯一索引和普通索引查询时的区别,比如下面这个 SQL 语句:

1
select id, identity from users where identity = 4504;

上面的 SQL 查询 users 表里面 identity = 4504 的记录,假设 identity 字段上建立的是普通索引,并且当前 identity 字段上的索引树结构如下:

在 <深入理解 MySQL 索引(二)> 文章中我们已经提到了索引字段的查询方式,查询过程是下面这样的:

1.通过 idx_identity 索引树定位到第三个叶子节点

2.将第三个叶子节点所在数据页读取到内存,取出第一个值 4504,判断满足条件取出主键 id = 4(这里因为只需要查询 id 和 identity 字段的值,所以不存在回表的过程)

3.接着再取下一个值 4505,大于 4504 不满足条件,查询结束返回

上面是 identity 字段为普通索引的查询过程,而如果是唯一索引,则是在上面步骤 2 取出第一个值 4504 判断满足条件取出 id = 4 之后直接查询结束返回,而不会执行步骤 3,这是普通索引和唯一索引在查询时的区别。

更新时的区别

再来看数据更新时的区别,比如下面这个更新语句:

1
update users set identity = 4504 where id = 3;

上面 SQL 将 users 表中 id = 3 的记录 identity 字段更新成 4504,这个 SQL 首先查询到 id = 3 的记录,除了将主键索引树叶子节点里面的 data 更新,还涉及到 identity 索引树的更新,需要将 identity 索引树上原来 id = 3 的 identity = 4503 更新成 4504(这里先不考虑索引树的页合并以及分裂过程),假设原来 id = 3 的 identity = 4503 所在的数据页是在 Page3 上,结合上面提到的 change buffer,identity 索引树的更新过程如下:

第一种情况,数据页 Page3 当前已经在内存里面:

1.如果 identity 字段上建立的是普通索引,直接将内存里面 Page3 数据页上的 identity = 4503 更新成 4504

2.如果 identity 字段上建立的是唯一索引,则会先进行判断当前是否已经存在 identity = 4504 的记录,不存在则更新

第二种情况,数据页 Page3 当前不在内存里面:

1.如果 identity 字段上建立的是普通索引,直接在 change buffer 记录对数据页 Page3 更新后返回

2.如果 identity 字段上建立的是唯一索引,则会先将 Page3 数据页读入内存,判断当前是否已经存在 identity = 4504 的记录,不存在则更新

可以看到由于唯一索引字段全局唯一的特性,没有办法使用到 change buffer 的特性,因为使用到了 change buffer 根本不会去读取磁盘上的数据,也就无法确认磁盘上是否还存在其他 identity = 4504 的数据,直接记录数据页修改可能违反字段全局唯一的特性,这也就回答了我上面提到的如果要更新的索引字段是唯一索引,不适用 change buffer。

唯一索引和普通索引如何选择

在一个字段上可以建立普通索引也可以建立唯一索引的时候,我们该如何进行选择呢,看到这里或许你已经有了答案了,但还是有一些我们需要注意的地方。

首先如果一个字段上可以是普通索引也可以是唯一索引,说明这个字段我们在业务代码层面就已经能够保证不会插入相同值,否则我们肯定还是要建立唯一索引。

如果两者都可以的时候,使用普通索引在更新时可以利用到 change buffer,极大提高更新速度,但在查询方面其实都差不多,没有太大区别,参考上面提到的普通索引和唯一索引查询时的区别,无非就是多了一个步骤 3 的判断,这一判断大概率是内存中执行的,因为从磁盘上读取数据都是一页一页的读取,极端情况下下一个值刚好在下一个数据页,这时才可能会涉及到从磁盘上再读取一次数据的过程,但概率一般很小

还有一个要注意的是使用 change buffer 的局限性,如果我们的更新都紧伴随着对该数据的查询,那么这时候就没有必要去使用 change buffer 了,相反还需要关闭 change buffer,因此这种情况下选择唯一索引和普通索引也就没什么区别了。

 wechat
扫描上面图中二维码关注微信公众号