MySQL学习之日志系统_MySQL

前面我们系统了解了一个查询语句的执行流程,并介绍了执行过程中涉及的处理模块。相信你还记得,一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。

那么一条更新语句的执行流程又是怎么样的呢?

有时候你听DBA的同事说,MySQL可以恢复到半个月内任意一秒的状态,有没有产生过好奇, 这是怎么做到的呢?

 

我们带着这两个问题往下看:

 

MySQL学习之日志系统_MySQL_02

(MySQL 逻辑架构图)

 

我们把MySQL的基本执行链路在拿过来进行看下,  可以确定的是, 查询的那一套流程, 更新语句也会走一遍。

在一个表上进行更新的时候,跟这个表有关的所有缓存,都会失效,这条语句会把表上的所有缓存结果都清空,所以我们建议不查询缓存的原因。

接下来分析器会知道这是一条更新语句,如果更新的字段有索引,优化器会采用这个字段的索引,执行器具体负责执行,找到这一行数据,然后更新。

与查询不一样的是,更新流程会有两个重要的日志模块。redo log (重做日志) 和 binlog (归档日志)。

 

重要的日志模块

一、redo log

 

在讲redo log 之前, 我们要来回顾一篇文章《孔乙己》。

 

在这篇文章里,有一个酒店掌柜,有一块粉板,专门用来记录客人赊账的记录。如果赊账的人不多,掌柜可以把姓名和金额记录到粉板上。但是如果人多了,粉板记不下了,掌柜就要放下手头的工作,记录到专门的账本上。

如果有人赊账,掌柜一般有两种做法:

    1、直接翻出账本,进行记录赊账人和金额;

    2、先记录到粉板,在不忙的时候记录到账本。

在生意红火的时候,掌柜会选择后者,因为前者过于麻烦,效率太低。

 

在MySQL中,MySQL的设计者们,也采用了粉板的思路来提升效率,而粉板与账本的配合过程中,其实就是MySQL中的WAL技术(write-ahead-logging),关键点就是 先写日志,再写磁盘。也就是先写粉板,再写账本。

也就是说,当一条更新语句过来时,InnoDB引擎会把记录记录到redo log(粉板)中,并更新内存,这个时候就算更新完成了。InnoDB引擎会在合适的时候,进行更新到磁盘中。

 

redo log 的大小是固定的,比如可以一组配置4个文件,每个文件大小是1G,那么这个redo log 可以记录4G。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

MySQL学习之日志系统_MySQL_03

(写redo log(粉板) 示意图 --图引自 MySQL45讲)

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

write pos 和 checkpoint 中间空着的部分,就是可以写入的部分,如果write pos 追上了 checkpoint ,代表写满了,就需要停下擦除一部分,把checkpoint推进一下。

 

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

 

二、binlog

我们之前学习知道,MySQL有两块,一块是server层,一块是引擎层。也知道,redo log 是InnoDB特有的日志,server层也有自己的日志,就是binlog(归档日志)。

为什么会有两份日志呢?

因为在最开始,MySQL并没有innodb引擎。MySQL自带的是MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

 

这两种日志有以下三点不同。

    1、redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

    2、redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。

    3、redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

 

有了对这两个日志的一些概念性理解,我们应该能知道流程是如何进行执行的了。

MySQL学习之日志系统_MySQL_04

(update 语句的执行流程图  --图引自 MySQL45讲)

先找到引擎取出这一行,引擎通过B+ TREE找到这一行。如果这个数据页本就在内存,直接返回。否则从磁盘读入内存,然后返回。

执行器拿到引擎给的数据,做上逻辑操作,调用引擎接口写入数据。

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

执行器生成这个操作的binlog,把binlog写入磁盘。调用提交事务接口,把刚刚的binlog改成提交状态。

 

二、将redo log拆为prepare 和 commit 两阶段引发的思考

 

为什么要有两阶段提交啊?

 

这是为了让两份日志之间保持逻辑一致。要说明这个问题,就要想到我们开始的那个问题,怎么恢复到半个月内任意一秒钟的数据?。

因为binlog会记录所有的逻辑操作,并且采用 “追加写”的方式。如果DBA承诺可以恢复,说明一定是备份了半个月的所有的binlog,同时系统会定期整库备份。

1. 首先找个临时库,完全空的 

2. 找最近的一份全量备份,全量备份的时间点比如为202-01-31 00:00:00 

3. 从202-01-31 00:00:00开始找出到误删前的增量binlog日志,(需要将其误操作记录删除, 不然还是会删除掉之前的记录)并执行到临时库 这样就保证了临时库和误删前的库一致了。

 

说了这么多,那为什么需要两阶段提交呢?

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。

 

1、先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。

1、先写 binlog 后写 redo log。。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

 

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。