Changebuffer的主要目的是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果。
在MySQL5.5之前的版本中,由于只支持缓存insert操作,所以最初叫做insertbuffer,只是后来的版本中支持了更多的操作类型缓存,才改叫changebuffer,这也是为什么代码中有大量的ibuf前缀开头的函数或变量。为了表达方面,本文也将changebuffer缩写为ibuf。
由于历史上ibuf的数据格式曾发生过多次变化,本文讨论的相关内容基于如下
设定:版本为5.5及之后的版本,不涉及旧版本的逻辑,
innodb_change_buffering设置为ALL,表示缓存所有操作。
ibufbtechangebuffer的物理上是一颗普通的bte,存储在ibdata系统表空间中,根页为ibdata的第4个page(FSP_IBUF_TREE_ROOT_PAGE_NO)。一条ibuf记录大概包含如下列:
ibufbte通过三列(spaceid,pageno,counter)作为主键来唯一决定一条记录,其中counter是一个递增值,目的是为了维持不同操作的有序性,
例如可以通过counter来保证在merge时执行如下序列时的循序和用户操作顺序是一致的:INSERTx,DELETE-MARKx,INSERTx。
在插入ibuf记录前我们是不知道counter的值的,因此总是先将对应tuple的counter设置为0xFFFF,然后将cursor以模式PAGE_CUR_LE定位到小于等于(spaceid,pageno,0xFFFF)的位置,新记录的counter为当前位置记录counter值加1。
ibufbte最大默认为bufferpoolsize的5%,当超过5%时,可能触发用户线程同步缩减ibufbte。为何要将ibufbte的大小和bufferpool大小相关联呢?一个比较重要的原因是防止ibuf本身占用过多的bufferpool资源。
ibufbitmap由于ibuf缓存的操作都是针对某个具体page的,因此在缓存操作时必须保证该操作不会导致空page或索引分裂。
针对第一种情况,即避免空page,主要是对purge线程而言,因为只有purge线程才会去真正的删除二级索引上的物理记录。在准备插入类型为IBUF_OP_DELETE的操作缓存时,会预估在apply完该page上所有的ibufentry后还剩下多少记录(ibuf_get_volume_buffed),如果只剩下一条记录,则拒绝本次purge操作缓存,改走正常的读入物理页逻辑。
针对第二种情况,InnoDB通过一种特殊的page来维护每个数据页的空闲空间大小,也就是ibufbitmappage,该page存在于每个ibd文件中,具有固定的pageno,其文件结构如下图所示:
ibufbitmap使用4个bit来描述一个page:
IBUF_BITMAP_FREE:使用个bit来描述空闲空间大小,以16KB的pagesize为例,能表示的空闲空间范围为0(0bytes)、1(51bytes)、(bytes)、3(bytes)。很显然,能够缓存的二级索引记录最大不可能超过字节。由于只有INSERT操作才可能导致page记录满,因此只需要对IBUF_OP_INSERT类型的操作进行判断:
ibuf_insert_low:
其中ibuf_bitmap_page_get_bits函数根据spaceid和pageno获取对应的bitmappage,找到空闲空间描述信息;如果本次插入操作可能超出限制,则从当前cursor位置附近开始,触发一次异步的ibufmerge,目的是尽量将当前page的缓存操作做一次合并。在正常的对物理页的DML过程中,如果page内空间发生了变化,总是需要去更新对应的IBUF_BITMAP_FREE值。
参考函数:btr_