前面我们分析过一个查询语句的执行流程,并且解释了执行过程中涉及的模块。一条查询语句一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。老铁们可以点击链接 查询语句在 MySQL 如何执行 学习上篇内容。
这次,我们来深入学习一条更新语句在 MySQL 中的执行流程。通过此文我们可以充分了解 什么是 Redo Log
表结构创建
首先我们先创建一张表,只有主键 ID,以及 int 类型字段 c。
create table T(ID int primary key, c int);
现在我们要更新一条数据,语句如下:
update T set c=c+1 where ID=2;
更新语句其实也跟查询语句的流程类似,只不过多了 redo log、undo log 以及 binlog 日志。
上一篇查询语句的执行流程我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句会把整个 T 表的缓存结果都清空。这也是为何我们不建议使用查询缓存的原因。
账本与记账板
假如您当了小超市老板,自然会有一个账本记录交易记录,但是可能还要一个赊账记录。因为村里有个姑娘叫小芳,长得美丽又善良。有时候会到你这里白嫖,额,不是,是赊账。你先把记录写在小粉板上,等夜深人静的时候就把粉板的数据同步到归档的账本中。当然粉板也有满的时候,所以当粉板满了就要对账写入账本中,
所以,如果有人要来赊账,或者还账的时候,通常有两种做法:
- 直接把账本翻出来,把这次的赊账加上去或者扣除。
- 先在粉板上记下这次的帐,等打烊后再把账本翻出来核算。
在生意忙的时候,我们肯定选择后者,因为前者操作太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可 能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。这个时候小芳来赊账,等半天。以后还怎么约小芳到小树林呢?
在 MySQL
中也有这个问题,如果每一次操作都要写进磁盘,然后磁盘也要找到对应的记录,然后再更新。整个过程的 IO 成本,查询成本都很高,为了解决这个问题,MySQL
的设计者就用了类似小超市老板粉板的思路来提升更新效率。
而粉板和账本配合的整个过程,其实就是 MySQ
L 里经常说到的 WAL
技术,WAL
的全称是Write-Ahead Logging
,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
redo log
首先我们要明确的是binlog
日志是在 server 层的,而redo log
是 InnoDB
特有的。
当有一条记录需要更新的时候,InnoDB
引擎就会先把记录写到 redo log
(粉板)中,并更新内存,这个时候就算完成了。同时 引擎会在适当的时候将这个记录更新到磁盘里,而更新往往是系统比较闲的时候,这就是打样以后掌柜做的事情。
类似的,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
binlog
MySQL 的整体架构其实有两块:一块是 Server 层,还有一块是 引擎层,负责存储相关。前面我们提到的 redo log
是InnoDB
引擎持有的,而 Server 层也有自己的日志,叫 binlog(归档日志)。
那为何会有两份日志呢?
因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力(因为是 Server 层与引擎层是两个独立的模块),binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
假如只有 binlog,当 Server 层 binlog 日志写完后 引擎层还没有同步到磁盘就断电了。这个时候重启后 binlog 记录了更新操作,但是引擎层并没有写入磁盘中就导致了从库使用该 binlog 同步数据不一致。
redo log、 binlog 的 差异
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
Update 语句执行流程
有了对两个日志的概念理解,我们就可以继续理解执行器与 InnoDB 引擎执行 update 语句时的内部流程。
- Server 层的执行器先调用引擎取出 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到数据把这个值 + 1,得到新一行的数据,再调用存储引擎接口写入这行新数据。
- InnoDB 引擎将这行数据更新到内存中,同时将这个更新操作所影响的页日志记录到 redo log 中,此时日志处于 prepare 状态,然后会告知 执行器完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog ,并把 binlog 写入磁盘。
- 执行器继续调用引擎的的提交事务接口,引擎收到请求就把刚刚写入的 redo log 的状态改成提交(commit),更新完成。
最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
如下图所示,绿色代表执行器执行,白色代表 InnoDB 引擎执行:
两阶段提交
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。要说明这个问题,我们得从文章开头的那个问题说起:怎样让数据库恢复到半个月内任意一秒的状态?
前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。(会造成数据不一致)
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
先写 redo log 后写 binlog
假如在引擎 写完 redo log 后,bin log 没有写完,异常重启,依然可以根据 redo log 日志把数据恢复,但是 binlog 没有记录这个语句。所以从库 通过 binlog 同步数据就导致没有把这个这行数据同步过来,丢失了这个事务操作造成数据不一致。
先写 binlog 再写 redo log
如果写完 binlog 后 崩溃,由于 redo log 还没有写,崩溃恢复后这个事务无效,但是 binlog 却有记录。从库根据 这个 binlog 日志就会导致多处一个事务,与主库不一致。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。(敲黑板了同学们)