之前我学习了undo log,redo log 的相关知识,在学习一种比较重要的数据变更日志--InnoDB Change Buffer。Change Buffer 是一种用于缓存二级索引页变化的特殊数据结构,物理上是一颗普通的btree,是缓冲池中独立的一个区域,存储在idbata系统表空间中。当需要修改二级索引页不在缓冲池中而在磁盘中时,会将这些索引页变化缓存在Change Buffer 中。用于在对数据变更时,如果数据所在的数据页没有在 buffer pool 中的话,在不影响数据一致性的前提下,InnoDB 引擎会将对数据的操作缓存在 Change Buffer 中。在MySQL 5.5 之前的版本中,由于只支持缓存insert操作,所以最初叫做 Insert Buffer,在后来的版本中支持了更多的操作类型缓冲,改叫 Change Buffer(写缓存)
当需要更新一条数据的时候,并不是只是将需要更新的那一条记录从磁盘中读出来,而是以页为单位,将整个页读入内存(在InnoDB中,最小存储单元是页,每个数据页的大小是16KB)。也就是说,如果引擎找到这一行数据,它所在的数据页就都在内存里。将数据页从磁盘读入内存中涉及到随机IO访问,这里也是数据库中成本最高的操作之一,而利用 Change Buffer 可以减少 随机IO 访问操作,从而提升数据库性能。
(InnoDB 存储引擎体系结构 内存中 和 磁盘中的结构 示意图 -- 来自MySQL官网)
从图中可以看出,change buffer 是 buffer pool 中的一部分,虽然 Change Buffer 的名字叫做 buffer。但是他是可以进行持久化的,在右边的磁盘中的 System Tablespace (系统表空间 -- idbata1)可以看到持久化 Change Buffer 的空间以及把这个 Change Buffer 页 的改动记录在 redo log 里。触发 Change Buffer 持久化操作有以下几种情况:
(InnoDB Change Buffer 示意图 -- 来自MySQL官网)从图上可以看到 Change Buffer 的功能,Change Buffer 中的数据最终还是会刷回到数据原始数据页中。把 Change Buffer 应用到原始的数据页,得到新的数据页的过程称之为merge。merge 过程中 只会将 Change Buffer 中与原始数据页有关的数据应用到原始数据页上。原始数据页加载到buffer pool时(访问数据页会触发merge)
Change Buffer 是可以通过命令参数进行控制的,MySQL 数据库提供了两个对 change buffer 的参数。
innodb_change_buffer_max_size
innodb_change_buffer_max_size 表示 Change Buffer 最大大小占 Buffer Pool 的百分比,默认为 25%。最大可以设置为 50%。
innodb_change_buffering
innodb_change_buffering 参数用来控制对哪些操作启用 Change Buffer 功能,默认是:all。
innodb_change_buffering 参数有以下几种选择:
all 默认值。开启buffer
none 不开启change buffer
inserts: 只是开启buffer insert操作
deletes: 只是开delete-marking操作
changes: 开启buffer insert操作和delete-marking操作
purges: 对只是在后台执行的物理删除操作开启buffer功能
了解了 change buffer,那change buffer 是什么时候使用呢?是所有的情况下都可以使用吗?(情况指的是 所有索引类型)。先了解一下普通索引 和 唯一索引之间的区别。
对于普通索引来说,查询满足条件的第一个记录后,需要查找下一个记录,一直到碰到第一个不满足条件的记录。
对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
上边的查询区别对于性能而言都是微乎其微的。上面也说了,因为innodb读写数据是按照页进行读取的,也就是说找到了需要的那条记录时,他所在的数据页也在内存里了,只需要判断下一条数据是不是满足条件即可,这些都在内存中操作的,所以性能基本没有。
但是读到这里的时候,我会有一个想法,因为普通索引是需要对比下一个记录是不是满足条件。如果当前记录是当前数据页的最后一条记录,那是不是就必须要获取下一个数据页了?因为这里看书中说的是,对于整型字段,一个数据页可以近千个key,出现这种的概率很低。我就在想出现的概率很低,也就是会出现。个人理解:如果真的出现这种情况,需要在进行读取下一个数据页的信息,拿出紧接着的下一条记录进行判断是否满足条件,这里会产生额外的操作成本。这样通过 change buffer ,减少了随机访问,更新性能会有所提升。
这里我不知道大家会不会想到一个问题,就是我使用的普通索引,然后进行更新数据,但是我更新完成之后,立刻进行读取数据,性能还会好吗?
这里的答案必然是不好的,反而会差了。我们在了解change buffer 的时候,知道change buffer 只是在下次用到该数据页的时候,会在这个时候 进行 merge 操作。如果说这样访问的话,随机IO访问并不会减少,反而增加了 change buffer 的维护代价。
所以在以下几种情况开启 Change Buffer 是会使得 MySQL 有明显提升:
读到这里还有一个疑问点,就是在学习日志系统时,redo log 也是也是为了减少重复读取磁盘,他俩有什么区别呢?
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
假设当前K索引树的状态,查找到位置后,k1所在的数据页在 内存 (buffer pool)中,k2所在的数据页不在内存中。
(带change buffer的更新过程 示意图 -- 图片来自 MySQL 45讲)
分析一下可以看到,设计了四个操作部分,内存(buffer pool),redo_log,数据表空间(t.ibd,就是一个个的表数据文件,对应的磁盘文件就是“表名.ibd”),系统表空间(ibdata1,用来放系统信息,如数据字典等,对应的磁盘文件是“ibdata1”)。
2、page 2 不在内存中,就在内存的 change buffer 区域,记录一下 “需要往page 2 插入一条数据”。3、将上述两个动作顺序记录到redo log 中。
做完这些,事务就可以完成了。所以看到执行更新语句成本很低,就是写了两处内存。然后写了一处磁盘,因为两次操作合在一起写了一次磁盘,而且还是顺序的写入redo log 中。图中的虚线箭头指的是后台持久化操作,不影响更新时间。
在此之后的读请求,如果说发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与 系统表空间(ibdata1)和 redo_log 无关了。(带change buffer的读过程 示意图 -- 图片来自 MySQL 45讲)
2、读page2的时候,需要把page2从磁盘中读入到内存中,然后应用change buffer里的操作日志,生成一个正确的版本进行返回结果,这里会有一次merge操作。从此可以看出,更新的时候,并不急着将数据刷回磁盘。而是先用change buffer缓存起来。当你要执行SELECT的时候,那么再才将磁盘上的数据页缓存到内存中,然后应用这个页面相关的change buffer日志,可以返回可客户端,并且将最新值merge回磁盘。所以,简单对比这两个机制在提升更新性能上,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
解释:
redo log 与 change buffer 这两个机制,不同之处在于--优化了整个变更流程的不同阶段。先不考虑 redo log、change buffer 机制,简化抽象一个变更(insert、update、delete)流程:1、从磁盘读取带变更的行所在的数据页,读取至内存页中可以从中看到,步骤 1 涉及了随机读磁盘IO,步骤 3 涉及了随机写磁盘IO
Change Buffer 优化了步骤 1,延时了随机读磁盘的IO操作;redo log 优化了步骤 3 ,延时了随机写磁盘的IO操作,将随机写变成顺序写。
这里经过上面的一些案例,能看出来,在MySQL innodb 中:change buffer 机制不是一直都会被应用到的,仅仅当操作的数据页不在内存中,需要先读取磁盘加载数据页时,change buffer 才有用武之地。redo log 为了确保 crash-safe, 始终都会用到。
是否用到 change buffer 机制,对于 redo log 这一步的区别在于,用到了 change buffer 机制的时候,在redo log 中记录的是 new change buffer item 相关的信息,而不是直接的记录物理页变更。
如果在此时,机器断电重启了,会不会导致 change buffer 丢失呢?change buffer 丢失可不是小事情,再从磁盘读出来的话就没有办法进行merge了,相当于数据丢失了?
是不会丢失的。虽然只是更新内存,但是在事务提交的时候,change buffer 的操作也会记录到 redo log 里面,所以崩溃回复的时候,change buffer 也是可以找回来的。
先看下merge流程是什么样子的吧:
1、从磁盘读入数据页到内存中(老版本的数据页)
2、从change buffer 里找出这个数据页的 change buffer 记录,可能是多个,以此按照顺序进行应用,得到最新的版本数据页。
3、写redo log。这个redo log 包含了 数据的变更 和 change buffer 的 变更。
在此时 merge 的操作就结束了。这时候数据页 和 内存中的 change buffer 对应的磁盘位置都还没有进行修改, 属于脏页。之后各自在刷回自己的物理数据,就是另一个过程。
解释:
更新先是在内存中的数据页更新;完了 Commit 的时候,将更新写入 Redo Log;Redo Log 的写走的是两阶段写协议,保证 Redo Log 和 Binlog 对更新达成共识;Redo Log 的 Commit 阶段执行成功,再响应客户端;但是!!!脏页刷盘的时候,是将内存中数据页的最新值,写入磁盘中的数据文件(表文件),而不是 Redo Log 中;正是由于 Redo Log 的存在,可以让内存中出现大量的脏页。
尽可能的使用普通索引。两类索引在查询上没有什么差别,只要考虑更新性能的影响。因为普通索引可以很好的使用到 change buffer。
如果笔记有什么错误的地方或者哪里有问题的话,麻烦各位指点,给我留言就好。
最后,求关注。每天进步一点点,欢迎关注我的公众号「白砂」。
如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,非常感谢!