MySQL实战 | 02-MySQL 如何恢复到半个月内任意一秒的状态?

原文链接:MySQL是如何做到可以恢复到任意一秒状态的?

看到这个题目是不是觉得数据库再也不用担心服务器 crash 了?

那我们需要学习为什么可以这么做?以及如何做?

即为什么可以恢复到任意时间点?如何恢复到任意时间点?

为什么有了 binlog 还需要 redo log?

事务是如何提交的?事务提交先写 binlog 还是 redo log?如何保证这两部分的日志做到顺序一致性?

为了保障主从复制安全,故障恢复是如何做的?

上一次课我们学习了一条 select 语句的全部执行过程,那么今天我们就从一条 update 语句开始。

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

其实执行流程和查询流程一致,只是最后执行器执行的是找到这条数据,并进行更新。

另外,更新过程还涉及到一个重要的日志模块,即 redo log(重做日志)和 binlog(归档日志)。

我个人是只听过 binlog 的。

redo log

和大多数关系型数据库一样,InnoDB 记录了对数据文件的物理更改,并保证总是日志先行

也就是所谓的 WAL(Write-Ahead Logging),即在持久化数据文件前,保证之前的 redo 日志已经写到磁盘。

MySQL 的每一次更新并没有每次都写入磁盘,InnoDB 引擎会先将记录写到 redo log 里,并更新到内存中,然后再适当的时候,再把这个记录更新到磁盘。

这里有必要贴一下 InnoDB 的存储结构图:

[InnoDB 物理存储结构][1]

如果下面看的各种空间懵逼了,建议回来看一眼这个图。

redo log 是啥

当数据库对数据做修改的时候,需要把数据页从磁盘读到 buffer pool 中,然后在 buffer pool 中进行修改,那么这个时候 buffer pool 中的数据页就与磁盘上的数据页内容不一致,我们称 buffer pool 的数据页为 dirty page 脏数据

[dirty page][2]

这里也可以看出,所有的更新操作都是现在 dirty page 中进行的。

如果这个时候发生非正常的 DB 服务重启,那么这些数据还没在内存,并没有同步到磁盘文件中(注意,同步到磁盘文件是个随机 IO),也就是会发生数据丢失

如果这个时候,能够在有一个文件,当 buffer pool 中的 dirty page 变更结束后,把相应修改记录记录到这个文件(注意,记录日志是顺序 IO),那么当 DB 服务发生 crash 的情况,恢复 DB 的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。

这个文件就是 redo log ,用于记录数据修改后的记录,顺序记录。

我理解的,redo log 就是存放 dirty page 的物理空间。

log 何时产生 & 释放?

在事务开始之后就产生 redo log,redo log 的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo log 文件中。

当对应事务的脏页写入到磁盘之后,redo log 的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。

如何写?

Redo log 文件以 ib_logfile[number] 命名,并以顺序的方式写入文件文件,写满时则回溯到第一个文件,进行覆盖写。

[循环写][3]

如图所示:

  • write pos 是当前记录的位置,一边写一边后移,写到最后一个文件末尾后就回到 0 号文件开头;
  • checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件;

write pos 和 checkpoint 之间还空着的部分,可以用来记录新的操作。

如果 write pos 追上 checkpoint,表示写满,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

Redo log 文件是循环写入的,在覆盖写之前,总是要保证对应的脏页已经刷到了磁盘

在非常大的负载下,Redo log 可能产生的速度非常快,导致频繁的刷脏操作,进而导致性能下降。

通常在未做 checkpoint 的日志超过文件总大小的 76% 之后,InnoDB 认为这可能是个不安全的点,会强制的 preflush 脏页,导致大量用户线程 stall 住。

如果可预期会有这样的场景,我们建议调大 redo log 文件的大小。可以做一次干净的 shutdown,然后修改 Redo log 配置,重启实例。

参考:
[http://mysql.taobao.org/monthly/2015/05/01/][4]

相关配置

默认情况下,对应的物理文件位于数据库的 data 目录下的 ib_logfile1ib_logfile2

innodb_log_group_home_dir 指定日志文件组所在的路径,默认./ ,表示在数据库的数据目录下。
innodb_log_files_in_group 指定重做日志文件组中文件的数量,默认2
# 关于文件的大小和数量,由一下两个参数配置
innodb_log_file_size 重做日志文件的大小。
innodb_mirrored_log_groups 指定了日志镜像文件组的数量,默认1

其他

redo log 有一个缓存区 Innodb_log_buffer,默认大小为 8M,Innodb 存储引擎先将重做日志写入 innodb_log_buffer 中。

[写 redo log 过程][5]

然后会通过以下三种方式将 innodb 日志缓冲区的日志刷新到磁盘:

1、Master Thread 每秒一次执行刷新 Innodb_log_buffer 到重做日志文件;
2、每个事务提交时会将重做日志刷新到重做日志文件;
3、当 redo log 缓存可用空间少于一半时,重做日志缓存被刷新到重做日志文件;

有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe

CrashSafe 能够保证 MySQL 服务器宕机重启后:

  • 所有已经提交的事务的数据仍然存在。
  • 所有没有提交的事务的数据自动回滚。

binlog

如前文所讲,MySQL 整体可以分为 Server 层和引擎层。

其实,redo log 是属于引擎层的 InnoDB 所特有的日志,而 Server 层也有自己的日志,即 binlog(归档日志)。

记录了什么

逻辑格式的日志,可以简单认为就是执行过的事务中的 sql 语句。

但又不完全是 sql 语句这么简单,而是包括了执行的 sql 语句(增删改)反向的信息。

也就意味着 delete 对应着 delete 本身和其反向的 insert;update 对应着 update 执行前后的版本的信息;insert 对应着 delete 和 insert 本身的信息。

何时产生 & 释放

事务提交的时候,一次性将事务中的 sql 语句按照一定的格式记录到 binlog 中。因此,对于较大事务的提交,可能会变得比较慢一些。

binlog 的默认是保持时间由参数 expire_logs_days 配置,也就是说对于非活动的日志文件,在生成时间超过配置的天数之后,会被自动删除。

区别

1、redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现,所有引擎都可以使用;
2、内容不同:redo log 是物理日志,记录的是在数据页上做了什么修改,是正在执行中的 dml 以及 ddl 语句;而 binlog 是逻辑日志,记录的是语句的原始逻辑,已经提交完毕之后的 dml 以及 ddl sql 语句,如「给 ID=2 的这一行的 c 字段加 1」;
3、写方式不同:redo log 是循环写的,空间固定;binlog 是可以一直追加写的,一个文件写到一定大小后,会继续写下一个,之前写的文件不会被覆盖;
4、作用不同:redo log 主要用来保证事务安全,作为异常 down 机或者介质故障后的数据恢复使用,binlog 主要用来做主从复制和即时点恢复时使用;
5、另外,两者日志产生的时间,可以释放的时间,在可释放的情况下清理机制,都是完全不同的。

参考:
[http://www.importnew.com/28039.html][6]


数据更新事务流程

有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。

1、执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

2、执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

3、引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

4、执行器生成这个操作的 binlog,并把 binlog 写入磁盘

5、执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

[事务流程][7]

两阶段提交

上面处理 redo log 和 binlog 看着是不是有点懵逼?

其实这就是所谓的两阶段提交,即 COMMIT 会被自动的分成 prepare 和 commit 两个阶段。

[两阶段提交][8]

MySQL 在 prepare 阶段会生成 xid,然后会在 commit 阶段写入到 binlog 中。在进行恢复时事务要提交还是回滚,是由 Binlog 来决定的。

由上面的二阶段提交流程可以看出,通过两阶段提交方式保证了无论在任何情况下,事务要么同时存在于存储引擎和 binlog 中,要么两个里面都不存在。

这样就可以保证事务的 binlog 和 redo log 顺序一致性。一旦阶段 2 中持久化 Binlog 完成,就确保了事务的提交。

此外需要注意的是,每个阶段都需要进行一次 fsync 操作才能保证上下两层数据的一致性。

PS:记录 Binlog 是在 InnoDB 引擎 Prepare(即 Redo Log 写入磁盘)之后,这点至关重要。
另外需要注意的一点就是,SQL 语句产生的 Redo 日志会一直刷新到磁盘(master thread 每秒 fsync redo log),而 Binlog 是事务 commit 时才刷新到磁盘,如果 binlog 太大则 commit 时会慢。

参考:
[http://www.ywnds.com/?p=7892][9]

如何恢复数据?

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

1、首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;

2、然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。

这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。

当遇到 crash 时,恢复的过程也非常简单:

1、扫描最后一个 Binlog 文件,提取其中的 xid;
2、重做检查点以后的 redo 日志,搜集处于 prepare 阶段的事务链表,将事务的 xid 与 binlog 中的 xid 对比,若存在,则提交,否则就回滚;

总结一下,基本顶多会出现下面是几种情况:

  • 当事务在 prepare 阶段 crash,数据库 recovery 的时候该事务未写入 Binary log 并且存储引擎未提交,将该事务 rollback。
  • 当事务在 binlog 阶段 crash,此时日志还没有成功写入到磁盘中,启动时会 rollback 此事务。
  • 当事务在 binlog 日志已经 fsync 到磁盘后 crash,但是 InnoDB 没有来得及 commit,此时 MySQL 数据库 recovery 的时候将会读出 binlog 中的 xid,然后告诉 InnoDB 提交这些 xid 的事务,InnoDB 提交完这些事务后会回滚其它的事务,使存储引擎和二进制日志始终保持一致。

总结起来说就是如果一个事务在 prepare 阶段中落盘成功,并在 MySQL Server 层中的 binlog 也写入成功,那这个事务必定 commit 成功。

总结

介绍了 MySQL 里面最重要的两个日志,即物理日志 redo log 和逻辑日志 binlog。

redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

我还跟你介绍了与 MySQL 日志系统密切相关的「两阶段提交」。两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,即使你不做数据库内核开发,日常开发中也有可能会用到。