InnoDB存储引擎的三个关键特性:插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)。
insert/change buffer
什么是change buffer?
在MySQL5.5之前,叫插入缓冲(insert buffer),只针对insert做了优化;现在对delete和update也有效,叫做写缓冲(change buffer)。
它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(buffer changes),等未来数据被读取时,再将数据合并(merge)恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO,提升数据库性能。
flush 链表的添加元素的条件
我们说 free 链表的添加条件是什么?
- 这个页已经从磁盘读入了Buffer Pool中。
- 当我们修改了此页数据,此缓存页变为了脏页,加入到flush链表中等待刷盘。
change buffer的作用
不清楚change buffer作用的朋友,做了下面的对比应该一清二楚了。
没有change buffer时,更新一条内存中不存在的页
那么假设我们现在读取的元素不在内存中,此时有人写了一个update语句更新数据页,InnoDB引擎的工作流程如下:
- 从磁盘加载数据页到缓冲池,一次磁盘随机读操作;
- 修改缓冲池中的页,一次内存操作;
- 写入redo log,一次磁盘顺序写操作;
没有命中缓冲池的时候,至少产生一次磁盘IO,对于写多读少的业务场景,是否还有优化的空间呢?
当出现change buffer时,更新一条内存中不存在的页(和flush链表的区别)
- 在写缓冲中记录这个操作,一次内存操作;
- 写入redo log,一次磁盘顺序写操作;
可以发现,这样change buffer的出现直接减少了一次磁盘IO。
读取数据是否会出现一致性问题?
当然不会,我们change buffer中,相当于以页为单位,存储了许多数据修改的逻辑。当change buffer没有刷到磁盘时,磁盘中的数据肯定是脏数据。那么读出来的数据肯定是不对的。
解决方案也很简单,就是先把脏数据读到内存中,再根据change buffer中对此数据页修改记录,还原出最新版本的数据页信息即可。(注意,这时候change buffer相关此页的数据就没了,同步到缓存中了。之后再修改此磁盘页的数据,就会进入flush链表中了)。是不是感觉融会贯通多了?
change buffer的刷盘时机
- 如上面描述的,当change buffer中有数据的时,发生读盘操作。会进行一次磁盘读取,再配合change buffer获取到最新数据。此时change buffer中的该页信息会刷掉;
- 有一个后台线程,会判断数据库空闲时刷盘;
- 数据库缓冲池不够用时;
- 数据库正常关闭时;
- redo log写满时;(redo log几乎不会写满,否则会造成MySQL吞吐量在一段时间内严重下降)
change buffer中存在数据时发生宕机怎么办?
每次change buffer中的数据会同步到redo log中,数据库异常崩溃,能够从redo log中恢复数据。
为什么change buffer是只针对于二级索引的优化呢?
我们来对比主键索引与二级索引进行一个新增操作的区别:
即将插入的记录所在目标页在内存中
- 对于唯一索引来说,找到3和5之间的位置,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,找到3和5之间的位置,插入这个值,语句执行结束。
这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的CPU时间。
但,这不是我们关注的重点。
即将插入的记录所在目标页不在内存中
- 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
- 对于普通索引来说,则是将更新记录在change buffer,语句执行就结束了。
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。change buffer因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。(我们可能积累了一个页中的很多数据,然后一起更新整个页,从而减少IO)。
在面试过程中,可以提起解决过这么一个问题。某天发现数据库的内存命中率从99%降低到了75%,整个系统处于阻塞状态,更新语句全部堵住。而探究其原因后,我发现这个业务有大量插入数据的操作,而他在前一天把其中的某个普通索引改成了唯一索引。
change buffer和redo的对比
这俩有啥可比性啊?一个缓存数据,一个日志文件,八竿子打不着啊!
然而,有了解过redo log的朋友,会知道redo log有一个特性和change buffer共有的一个特性是:尽量减少随机读写。那么围绕着这个角度我们来分析一下change buffer与redo log的区别。
redo log的顺序写
现在,我们要在表上执行这个插入语句:
insert into t(id,k) values(id1,k1),(id2,k2);
这里,我们假设当前是以k为索引的二级B+树索引,查找到位置后,k1所在的数据页在内存(InnoDB buffer pool)中,k2所在的数据页不在内存中。如图所示是带change buffer的更新状态图。
分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。
这条更新语句做了如下的操作(按照图中的数字顺序):
- Page 1在内存中,直接更新内存;
- Page 2没有在内存中,就在内存的change buffer区域,记录下“我要往Page 2插入一行”这个信息
- 将上述两个动作记入redo log中(图中3和4)。
做完上面这些,事务就可以完成了。所以,你会看到,执行这条更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),而且还是顺序写的。(注意:上述的三个步骤是一个事务,也就是必须redo log写完,事务才算搞定,这也印证了为啥redo log一定可以恢复change buffer中的数据)
change buffer减少随机读
我们现在要执行
select * from t where k in (k1, k2)
这里,我画了这两个读请求的流程图。
如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。所以,我在图中就没画出这两部分。
从图中可以看到:
- 读Page 1的时候,直接从内存返回。
- 要读Page 2的时候,需要把Page 2从磁盘读入内存中,然后应用change buffer里面的操作日志,生成一个正确的版本并返回结果。
可以看到,直到需要读Page 2的时候,这个数据页才会被读入内存。到真正写盘时是在数据库空闲或者不得已的时候才会进行。而当刷盘的时候,可能会有多条语句多次操作磁盘,此时将整个页整体刷入磁盘,就减少了许多次与磁盘之间的交互,从而达到减少磁盘IO的目的。
总结
所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。
double write buffer
以页为单位写盘是原子操作么?
我们知道,哪怕改一条数据,但是写盘依旧会把该条数据所在的磁盘页从内存中全部写入磁盘中。当然,加载也是以页为单位的。但是以页为单位的写数据是原子性的么?
答案当然不是。一个页那么多数据,必然不是原子性的。那么问题来了。万一一个磁盘页的数据更新了一半发生宕机了,这时候该怎么保证数据不丢失呢?
这时候大家又要抢答了。我知道有个redo log日志,InnoDB就是靠他保证数据不丢失的!对,但是redo log日志中记录的是对页的物理操作。而如果发生 partial page write(部分页写 入)问题时,此时重做日志(Redo Log)无能为力。 那么这种页写了一半的情况该如何解决呢?
doublewrite buffer解决页的部分写入
doublewrite buffer 是 InnoDB 在表空间上的 128 个页(2 个区,extend1 和 extend2),大小是 2MB。为了解决部分页写入问题。
当 MySQL 将脏数据 flush 到数据文件的时候, 先使用 memcopy 将脏数据复制到内存中的一个区域(也是 2M),之后通过这个内存区域再分 2 次,每次写入 1MB 到系统表空间,然后马上调用 fsync 函数,同步到独立表空间的磁盘上。在这个过程中是顺序写,开销并不大。
当第一次看到doublewrite buffer中的buffer时,一看到buffer就觉得是内存。但在这里,doublewrite buffer 实际上也是一个文件。 写系统表空间会导致系统有更多的 fsync 操作, 而硬盘的 fsync 性能因素会降低 MySQL 的整体性能。不过在存储上,doublewrite 是在一个连续的存储空间, 所以 硬盘在写数据的时候是顺序写,而不是随机写,这样性能影响不大,相比不双写, 降低了大概 5-10%左右。
因此,如果系统表中的doublewrite buffer写入失败,那么独立表中的实际磁盘数据更不可能写入成功了。因为这俩是有严格的先后顺序的。此时就需要从redo log中,整页的恢复数据。
如果doublewrite buffer写入成功,实际磁盘数据发生部分写入问题(数据库异常关闭的情况下启动),都会做数据库恢复(redo)操作,恢复 的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到双写这个功能。
自适应哈希索引(adaptive hash index)
我们知道InnoDB是不支持Hash索引的。最大的原因是因为这个数据结构不支持范围查询,在MySQL的使用环境来说,这个是非常不友好的。
普遍来讲,Hash索引确实不满足MySQL的底层索引要求。不过其接近O(1)的查询效率一直被InnoDB开发者们觊觎。因此,InnoDB的开发者们决定,将热点数据尽可能存储到hash索引中。
Innodb存储引擎会监控对表上二级索引的查找,如果发现某二级索引被频繁访问,二级索引成为热数据,建立哈希索引可以带来速度的提升。
经常访问的二级索引数据会自动被生成到hash索引里面去(最近连续被访问三次的数据),自适应哈希索引通过缓冲池的B+树构造而来,因此建立的速度很快。
预读
在从磁盘读取数据时,InnoDB会认为读取某个磁盘页数据时,InnoDB认为大概率还会访问次磁盘页附近的磁盘页,因此提前将这些访问磁盘页附近的磁盘页一起读入内存的机制。