一条 SQL 查询语句的一般执行流程是经过连接器、分析器、优化器、执行器等功能模块,然后到达存储引擎。
接下来我们来看下,一条更新语句的执行流程:我们经常会听到 DBA 同事说,MySQL可以恢复到一定时间内的任意一秒的状态,下面我们就来探秘一下它是怎么做到的呢?
我们先从一条更新语句开始看起:
- 下面是一个创建语句,创建一个表 Test ,一个主键 id,和一个整型字段 c,建表语句为:create table Test(id int primary key, c int);
- 如果想要将 id=2 这行的字段 c 加 1,可以这么写 SQL 语句:update Test set c=c+1 where id=2;
- 执行这条语句要先连接数据库,然后分析器分析出这是一条更新语句,优化器决定使用 id 这个索引,然后执行器负责具体执行找到这一行,然后进行更新操作。
下面我们的重点就来了:更新流程涉及WAL机制和两个重要的日志模块,它们就是我们今天要学习的主角:
① WAL机制
② redo log(重做日志)
③ binlog(归档日志)
1、WAL机制:
- 我们 MySQL 的更新操作,先在内存中对数据进行更改,再异步写入磁盘。但是内存总是不可靠,万一断电重启,还没来得及落盘的内存数据就会丢失,所以还需要加上写日志这个步骤,万一断电重启,还能通过日志中的记录进行恢复。
- 如果每次都要写进磁盘,然后磁盘还要对应的查到那条记录才能更新。这整个过程 IO 成本和查找成本都很高。
- 为了解决这个问题, MySQL 引入了 WAL( Write-Ahead Logging)技术,也称为日志先行技术。它的关键点就是先写日志,再写磁盘,也就是像是做生意的记账,先临时找个地记下来,等不忙的时候再写入账本。
2、核心日志模块
上面说到了 MySQL 的更新操作执行流程,不只是写内存写磁盘还需要写日志。
2.1 redo log
- 当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(临时记录的地方)里面,并更新内存,这个时候更新就算完成了。
- 与此同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,就是不忙的时候再从临时记录的地方写入到账本里。
- InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1 GB,那么这块“临时记录的地”总共就可以记录 4GB 的操作。
写的大概模式可以简化为图下图所示:从头开始写,写到末尾就又回到开头循环写
write pos 是当前记录的位置,一边写一边后移,写到 logFile 3 的末尾时就回到了 logFile 0 的位置。
check point 是当前要擦出的位置,也是向后推移并且循环的,查出记录前要把记录更新到数据文件。
write pos 和 check point 之间的空白记录是用来记新的操作。
如果write pos 循环移动到 check point 表示中间没有空白记录可供记新的操作了,即不能再执行新的更新操作了,这时候得停下来擦一些记录,把 check point 循环往后推进。
- redo log 也称为事务日志,由 InnoDB 存储引擎层产生。记录的是数据库中每个页的修改,而不是某一行或某几行修改成怎样,可以用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置,因为修改会覆盖之前的)。
- 有了 redo log ,InnoDB 引擎就可以保证即使数据库发生了异常重启,之前的提交记录也不会丢失,这个能力成为 crash-safe。即在 InnoDB 存储引擎中,事务提交过程中任何阶段,MySQL 突然奔溃,重启后都能保证事务的完整性,已提交的数据不会丢失,未提交完整的数据会自动进行回滚。crash-safe 主要体现在事务执行过程中突然奔溃,重启后能保证事务完整性。
- redo log 就是 WAL 的典型应用。
- MySQL 在有事务提交对数据进行更改时,只会在内存中修改对应的数据页和记录 redo log 日志,完成后即表示事务提交成功。至于磁盘数据文件的更新则由后台线程异步处理。
- 由于 redo log 的加入,保证了 MySQL 数据一致性和持久性,即使数据刷盘之前MySQL奔溃了,重启后仍然能通过 redo log 里的更改记录进行重放,重新刷盘。
- 此外还能提升语句的执行性能,写 redo log 是顺序写,相比于更新数据文件的随机写,日志的写入开销更小,能提升语句的执行性能,提高并发量,所以 redo log 是必不可少的。
2.2 binlog
从 MySQL 整体来看,其实就有两块:
① 一块是Server层,它主要做的是MySQL功能层面的事情;
② 还有一块是引擎层,负责存储相关的具体事宜。
- redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
- 被称为是归档日志,是因为 它不像 redo log 一样擦掉之前的记录循环写,而是一直记录,超过有效期才会被清理,如果超过单日志的最大值(默认1G,可以通过变量 max_binlog_size 设置),则会新起一个文件继续记录。但由于日志可能是基于事务来记录的(如 InnoDB 表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好 binlog 日志文件达到了最大值但事务还没有提交则不会切换新的文件记录,而是继续增大日志,所以 max_binlog_size 指定的值和实际的 binlog 日志大小不一定相等。
- 由于 binlog 有归档的作用,所以 binlog 主要用作主从同步和数据库基于时间点的还原。主从模式下,从库的数据同步依赖的就是 binlog ,所以这种模式下 binlog 是必不可少的。
总结一下他俩:
- ① redo log 是 InnoDB 引 擎特有的;
binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。 - ② binlog 记录的是操作的逻辑日志,是行为;
redo log 记录的是逻辑执行完之后的结果日志,是结果。 - ③ redo log 是循环写的,空间固定会用完;
binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 - 理解完这俩日志概念之后,我们接下来再看看执行器和 InnoDB 引擎在执行上面的更新语句时的流程: ① 执行器先找引擎取 id=2 这一行,id 是主键,引擎直接用树搜索找到这一行。
如果 id=2 这一行所在的数据页本来就在内存中,则直接返回给执行器;
如果没在,则先从磁盘中读入内存,然后再返回。
② 执行器获取到引擎给的行数据,执行 c+1 ,把这个值加 1,得到新的一行数据,再调用引擎接口写入这行数据。
③ 引擎将这行数据更新到内存中,同时将这个更新操作记录到 redo log 里面。
这时候 redo log 处于 prepare 状态。然后告知执行器执行完成,随时可以提交事务。
④ 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
⑤ 执行器调用引擎的提交事务接口,引擎把之前写入的 redo log 改成 commit 状态,更新完成。
图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
3、两阶段提交
上图你可以看到,将 redo log 的写入分成了两个步骤:prepare 和 commit 这就是“两阶段提交。
为什么要有“两阶段提交”呢?
这就回归到:怎样让数据库恢复到一定时间内任意一段时间内的状态呢?
- 上面我们知道了 binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。
- 如果我们的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
- 当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,可以怎么做呢?
① 首先,找到最近的一次全量备份,从这个备份恢复到临时库;
② 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样我们的临时库就跟误删之前的线上库一样了,然后我们可以把表数据从临时库取出来,按需要恢复到线上库去。 - 当我们需要扩容的时候,也就是需要再多搭建一些从库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用 binlog 来实现的,这个“不一致”就会导致我们的线上出现主从数据库不一致的情况。
- 简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
- 两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,即使不做数据库内核开发,日常开发中也有可能会用到。