前言

在上一节,虽然也是讲一个"流程",但更像是在讲update语句不同情况的执行策略(主要还是因为百度搜不到,觉得还是有必要讲讲),还未涉及到内存、日志等方面的问题。那么这些问题,就放到本章来讲一讲。

其实如果你在百度又或者其它搜索引擎搜"update语句的执行流程",我相信绝大部分的文章,都能给你一个比较满意的答案。

但是看完那些文章后,可能也只是在你脑海中留下了一点印象。对于一些执行顺序、不同日志间的联系、到底是谁来保证原子性?等细节问题可能还比较模糊。

而本篇就来试图摸索一些平常不太注意的地方。(也可能只有我自己没注意到)


本篇会提到redo log,undo log,buffer pool等概念,如果你对这些名词感到陌生,建议先去多看看相关资料补补,在这里篇幅拉得太长我自己也受不鸟~

PS:我博客中的MYSQL版本都是5.7.21,引擎、事务没有具体讲到的话都是指默认的。


一条UPDATE语句的执行流程

首先,先初始化一下。

mysql> create table T(ID int primary key, c int);

mysql> insert into T(ID, c) values(2, 1);

mysql> update T set c=c+1 where ID=2;

然后,对于上面update语句,我会放一张被网友搬运/微调无数次的流程图

update执行流程简图

这张图其实出自极客时间的《MYSQL实战45讲》(本文也有参考这个专栏),网上有很多相似的,但它才是鼻祖。

不过你可能会发现一个问题,就是为什么图里面没有提到undo日志?对于这点我也感到很奇怪,结果我一查,发现undo日志的机制远比我想象的复杂,暂时无法全部消化。就我目前的阅历,这部分我暂时不打算深究,目前只懂undo日志是用于回滚以及如何为mvcc服务的。

如果你不了解mvcc,重点推荐掘金的MYSQL小册子(作者:小孩子),那是我看过关于mvcc描述讲得最易理解、通透的书。

如果你想更加深入了解undo和redo日志,我会在文章底部推荐一些博客。


一些需要注意的细节问题

  1. 在默认配置下,执行一条update语句其实是会触发隐式事务的,出现了事务也就意味着undo日志必定参与了这个过程,只是可能因为undo日志的流程过于复杂,所以即便是书作者也没有对"undo在这个过程发生了什么"进行进一步讲解。
  2. 在【写入redolog处于prepare阶段】,其实redo日志的信息是保存在内存中的,这个内存称为redo log buffer(redo日志缓冲区)。而这个缓冲区是有大小限制的,默认为16Mb,那如果在commit前这个缓冲区不够了/达到阈值要怎么办?答案视具体情况而定(跟设定参数也有关),有可能被写到文件系统的page cache之后进入等待,也有可能被另一个事务提交时带着一起持久化到磁盘了。

page cache其实是指文件系统的buffer区,也可以理解为是一个"临时文件",但这个"临时文件"并不属于持久化文件,说白点就是并没有真正落盘,算是一种中间状态。

另外,将内存的数据刷到page cache实际上也是相当快,跟内存间拷贝数据的速度差不多。

虽说page cache和持久化文件是两个东西,但是日志持久化之前是必须经过page cache的。


扩展

在上面流程图的前半部分,可以看到有一个步骤是【磁盘中读入内存】,其实就是指把磁盘的数据页先加载到buffer pool中,但这一步骤其实并非适用于所有情况(即便数据页不在内存中),它在这适用主要是因为update语句中使用了唯一索引,下面来解释下这个问题。

首先,要讲清楚这个问题,必须要了解change buffer是个什么东东,关于这块我觉得官方文档解释得就挺好,在这里就贴下官文的传送门 -> 官文

在这里我就先借用一下官网的图:

change buffer简图

从图中我们能得到两个关键信息:

  1. change buffer其实是在buffer pool里面的一块区域。
  2. change buffer是在数据页被加载到buffer pool,才主动刷新的。

关于第一点,其实没什么好说的,就是告诉你change buffer的内存和buffer pool内存是绑一块的,调整buffer pool的时候需要注意下。另外官方文档中也有介绍到innodb_change_buffer_max_size,原文如下:

The innodb_change_buffer_max_size variable permits configuring the maximum size of the change buffer as a percentage of the total size of the buffer pool. By default, innodb_change_buffer_max_size is set to 25. The maximum setting is 50.

意思就是可以通过innodb_change_buffer_max_size这个参数控制change buffer在buffer pool中占用的百分比,且不能超过50%。


现在我们已经了解了change buffer是个什么东东,那接下来就是要了解为什么需要它?或者,它能给我们带来什么好处?

关于这个问题,其实官方文档也有简单的解释,我们不妨先来看下原文:

Buffering secondary index changes when secondary index pages are not in the buffer pool avoids expensive random access I/O operations that would be required to immediately read in affected index pages from disk. Buffered changes can be applied later, in batches, as pages are read into the buffer pool by other read operations.

注意这里的secondary index指的是二级索引(非唯一),官文中也有对应解释:Unlike clustered indexes(聚簇/主键索引), secondary indexes are usually nonunique.

翻译一下,大概意思就是:

当buffer pool中没有要更新的二级索引的数据页时,先把更新操作写在change buffer,等之后数据页有机会加载到buffer pool中后,再将change buffer中的更新批量同步到buffer pool中,这样一来可以有效避免随机I/O。

看到这里,你可能会产生一个新的问题:唯一索引为什么不能用change buffer?

  • 原因就在于它是唯一索引,插入/更新数据时,唯一索引需要判断当前索引是否是唯一的,这需要将数据加载到buffer pool中进行比较。这时候数据是必定被加载到buffer pool的,那么直接在buffer pool上更新即可,在这里用change buffer显得多此一举了。
  • 从上面一点我们不难再推出一种结论:如果记录在更新之后被立即访问,这种情况使用change buffer反而会拉低性能。因为访问时需要将数据页加载到buffer pool,那还不如去掉change buffer直接在buffer pool中更新,change buffer又显得多此一举了。
  • 上面两点中"多此一举"体现在:因为数据必定被加载到buffer pool,所以原本只需要【将数据页加载到buffer pool中】->【直接在buffer pool上修改】,没必要变成【在change buffer中记录】->【等待数据页被用上,数据页被加载到buffer pool】->【将change buffer的记录改写到buffer pool中】。
  • change buffer是可关闭的。

一些对于update流程的提问

以下问题一部分出自极客的专栏,一部分来自其它地方。

  • Q:Buffer Pool如果有多个instances的话,不同的instance会不会有同样的“数据页”?
    A:不会的,hash算法是固定的(表空间号 + 页号)。

  • Q:假如数据写了redo log(commit前),但是此时断电了,内存脏页还没刷盘该怎么办?
    A:首先你要明确保证原子性是redo log和undo log的工作,buffer pool主要是为了提高查询&顺序落盘的效率存在。假如断电后我们重启,因为已经写了redo log,而redo log中存在未commit的记录必定是脏页数据的修改记录,所以此时系统只要将这些未commit的记录在buffer pool中重做一次,就能恢复到崩溃前的状态了。

  • Q:既然change buffer能给我们带来这些好处,那为什么阿里开发手册中建议优先使用唯一索引而不是二级索引呢?
    A:阿里巴巴的开发规范是适用于大多数场景而不是所有场景。手册会这么写,说明大多数情况下,我们的数据都是更新之后被立即访问。如果索引不具有唯一性质,且更新后大概率不会马上访问,那自然还是适用二级索引比较好。


下面推荐一些博客:

redo log和undo log详解 -> 传送门
undo log漫游 -> 传送门
更多change buffer的Q/A -> 传送门