2、一条sql更新语句是如何执行的
前文提到,一条查询语句经过连接器、分析器、优化器、执行器功能模块,最后到达存储引擎。
那更新语句又是如何执行的呢?
mysql可以恢复到半月内任意一秒的状态,那么这又是怎么做到的呢?
例如:
create table T(ID int primary key, c int);
update T set c=c+1 where ID=2;
首先,可以确定的说,查询语句的那一套流程,更新语句也是同样会走一遍。
当一个表上有更新是,跟这个表的有关查询缓存会失效,所以更新语句会把表上所有的缓存结果清空。这也是我们一般不建议使用查询缓存的原因。
分析器通过词法、语法分析知道这是一条更新语句,优化器决定使用ID 索引,然后执行器负责执行,通过引擎找到这一行,并完成更新操作。
与查询流程不一样的是,更新流程还涉及两个重要的日志模块,这也是本文的重点:redo log(重做日志)和 binlog(归档日志)。如果接触 MySQL,那这两个词肯定是绕不过的,我后面的内容里也会不断地和你强调。不过话说回来,redo log 和 binlog 在设计上有很多有意思的地方,这些设计思路也可以用到你自己的程序里。
重要的日志模块:redo log
可以用《孔乙己》这篇文章里的记账情节作为理解,酒店掌柜有一个粉板,专门记录客户的赊账记录。如果赊账人不多,粉板也记录得下,那么他直接将客户名和账目直接写在粉板上。但是如果赊账人数多了,粉板总有记录不下的时候,掌柜就会将赊账记录转记到账本里。粉板就充当了一个中间过程。这也是计算机科学里所说的,没有什么问题不是不能通过中间件解决的。
如果有人要赊账或者还账,掌柜一般有两种做法:
- 一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
- 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。
通过粉板掌柜可以在白天忙时,实现快速记账,晚间打烊后再整理归入账本。这大大提高了白天效率,防止白天客户接待的阻塞。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?
同样,在mysql里也会存在这个问题,如果每一次更新都需要写入磁盘,然后磁盘也要找到那条记录,然后更新,整个过程IO成本、查找成本都很高。为了解决这个问题,mysql设计者就用了类似酒店粉板思路来提高效率
而粉板和账本的整合过程,其实就是MYSQL常提到的WAl技术,即write-ahead logging,提前写日志,再写磁盘,也就是先粉笔,再等不忙的时候记账本。
具体来说,当有一条记录需要更新的时候,Innodb 引擎会把记录写道 redo log 里面,更新内存,这个时候更新就算完成。同时,innodb引擎会在适当的时候,将操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,就像打样以后掌柜做得事情。
粉板有大小,redo log 也有固定大小,比如配置一组四个文件,每个文件大小1GB,从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
重要的日志模块:binlog
MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的粉板 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
为什么会有两份日志?
最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
两种日志有三点不同:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志(即实际已修改的数据结果),记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志(将要做的逻辑),记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
我们来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
- 1、执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 2、执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 3、执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 4、执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
最后几步 redo log 的写入拆成两个步骤:prepare 和 commit,这就是"两阶段提交"
两阶段提交
为什么要两阶段提交,这是为了让两份日志之间的逻辑一致
前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
好了,说完了数据恢复过程,我们回来说说,为什么日志需要“两阶段提交”。这里不妨用反证法来进行解释
由于redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者反过来,这两种方式会有什么问题。
用前面提到的 update语句的例子。假设当前 id=2 的行,字段 c 的值是0,再假设执行 update 语句过程中写完第一个日志后,第二个日志话没有写完发生 crash ,会出现什么情况?
- 1、先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
- 2、先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
所以,通过两阶段提交,保证了两份日志的同步,那么数据库的状态就有可能和他的日志恢复出来的库的状态保持一致。
当你需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用 binlog 来实现的,这个“不一致”就会导致你的线上出现主从数据库不一致的情况。简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
- redolog是循环写的,不持久保存,充当前文提到的粉板作用,减少数据库的磁盘IO,从而提高效率,binlog的“归档”这个功能,redolog是不具备的。同时,binlog 的持久保存,对于数据库的恢复具有实际意义,这也说明了,为什么使用 binlog 恢复数据。
- 有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。