前言:
通过一条update更新语句来引出MySQL日志系统,主要包括redo log(重做日志)模块和binlog(归档日志)模块。虽然,MySQL中还有undo log(回滚日志)的概念,但是回滚日志主要用来一致性视图中实现MVCC。
正文:
假设现在有一张表,建表语句如下:
mysql> create table T(ID int primary key, c int);
我们再来看一个简单的更新语句:
mysql> update T set c=c+1 where ID=2;
MySQL的内部逻辑架构图(详解),包括Server层的连接器,查询缓存,分析器,优化器以及执行器;存储引擎层的存储引擎
。如下图所示:
查询语句和更新语句执行的内部逻辑基本上都是一样的,但是更新语句涉及到了数据的更改,所以必不可少的需要引入日志模块,即redo log重做日志模块和binlog归档日志模块。接下来,我们依次介绍这两个重要的日志模块。
2. redo log (重做日志模块)
MysqL 磁盘写入策略之innodb_flush_log_at_trx_commit详解
[本部分内容有待重新整理和补充]
2.1 磁盘IO和内存读写效率的关系:
在介绍redo log之前,我们先来了解下磁盘和内存的区别与联系。我们知道,MySQL是一种关系型数据库,数据持久化保存在磁盘上。但是我们都知道,磁盘的IO效率很低,与之对应的是内存的IO效率远高于磁盘IO。
在MySQL中,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL采用了另外一种更新机制
,先更新保存在内存中,写入内存成功,即表示该数据更新成功,给客户端返回。随后,在一个数据库空闲的时间段或者是内存占满之后,将内存中的数据刷到磁盘上。
2.2 WAL(Write-Ahead Logging技术):
这么做有什么问题吗?当然有问题,如果发生异常重启的现象,那么内存中的数据将会丢失
,出现数据不一致的情况。那么如何解决呢?这个时候,我们的redo log重做日志就该闪亮登场了。在更新数据写入内存的同时,我们会记录redo log,并且持久化到磁盘,当数据库异常宕机之后,我们可以根据redo log重做日志来恢复数据,保证之前提交的数据不会丢失,也就是拥有了crash-safe的能力。同时这就是我们常说的WAL技术
,即Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。
redo log示意图:
InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示:
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是redo log还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示redo log满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
redo log存储的数据结构如下所示:
reod_log_type: 占用1字节,表示重做日志类型。各种不同操作有不同的重做日志格式,但有基本的格式
space:表空间的ID,采用压缩的方式,占用空间可能小于4字节
page_no:页的偏移量,同样采用压缩方式
redo_log_body:每个重做日志的数据部分,恢复时需要调用相应的函数解析。
为什么redo log写入效率比直接写磁盘效率高?
我们知道,redo log是一种日志文件,必然也是持久化到磁盘上的。那为什么先更新内存,然后写redo log会比数据直接写磁盘效率高呢?原因在于redo log可以循环并且顺序写入磁盘,数据直接写入磁盘多了查找源数据和随机写入的过程,自然效率会低下很多。
redo log和buffer pool的关系:
我们知道,更新数据其实就是先写内存,同时记录redo log,那么就会返回当前数据已经更新成功。此时,内存中的数据页和磁盘的数据页是不一致的,称此为脏页。正常情况下,数据库会在适当的时候将buffer pool
里边的脏页刷新到磁盘上。整个过程其实和redo log是没有任何关系的。只有在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容
。更新完成后,内存页变成脏页,之后会被刷新到磁盘。
总结
3. binlog(归档日志模块)
MySQL的架构包括Server层和引擎层。前者是MySQL功能层面的事情,引擎层负责存储相关的具体事宜。redo log
是InnoDB引擎特有的日志模块;binlog
是Server层自带的日志模块。
3.1 为什么我们还需要binlog日志?
最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
redo log和binlog日志的不同点如下:
- redo log是InnoDB引擎特有的日志模块;binlog是Server层自带的日志模块
- redo log是物理日志,记录了某个数据页上所做的修改;binlog是逻辑日志,记录本次修改的原始逻辑,说白了就是记录了修改数据的SQL语句
- redo log是循环写的形式,空间固定会被用完;binlog是追加写的形式,可以写多个文件,不会覆盖之前的日志。
- redo log作为异常宕机或者介质故障后的数据恢复使用;binlog可以作为恢复数据使用,主从复制搭建。
我们可以通过mysqlbinlog可以解析查看binlog日志,在MySQL中binlog的日志格式有statement,row以及mixed
三种方式,为了可以更加准确的记录归档日志,我们一般选择row格式做为binlog的日志格式
。特性对比如下:
3.2 binlog恢复数据库:
binlog是一种归档日志,只要binlog存在,我们就可以进行数据库的恢复。假如说,在今天中午12点的时候,发现上午10点执行了错误的SQL语句,想把数据库状态恢复到上午10点,错误语句执行之前。那么该怎么办呢?
数据恢复步骤如下:
- 首先你要拿到最近一次的备份库
- 拿到最近备份库开始到出错时候的所有binlog(binlog日志保留时间可以自定义)
- 使用binlog重放到错误发生之前。
4. 一条update语句在MySQL内部是如何执行的:
在简单介绍了redo log和binlog之后,我们回到文章开头,分析一条update语句在MySQL内部是如何执行的:
- 执行器要先找存储引擎找到这一行数据,从内存中或者磁盘中,返回给执行器
- 执行器拿到数据之后进行更新操作,再调用引擎接口写入这行新数据
- 引擎将这行数据更新到内存中,并且将操作记录写入redo log中,此时redo log处于prepare状态。然后告知执行器执行完成随时可以提交事务了
- 执行器生成这个操作的binlog,并且将binlog写入磁盘
- 执行器调用引擎的提交事务接口,引擎将刚刚写入的redo log改成提交(commit)状态,更新完成
update语句的执行逻辑图如下所示:
4.1两阶段提交协议:
由执行逻辑图可以看出,这里其实是使用了两阶段提交,如果不适用两阶段提交会有如下的问题:
- 先写redo log,后写binlog。会导致异常重启后redo log多了,binlog缺失。使用binlog恢复的备份库会缺少一条事务
- 先写binlog,后写redo log。会导致多一条事务出来
- 总结:不使用两阶段提交会导致数据库状态和用日志恢复出来的数据库状态不一致
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
既然两阶段提交可以保证数据的一致性,那么根据上图update执行示意图,我们来分析下异常发生的时刻对于数据一致性的影响:
- 若在写入binlog日志之前发生异常crash:由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。
- 若binlog日志写完之后发生了异常crash:
- 崩溃恢复时的判断规则:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交。
- 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
- a. 如果是,则提交事务
- b. 否则,回滚事务
- 崩溃恢复时的判断规则:
4.2 那么如何判断binlog日志是否完整?
其实每一种格式的binlog日志都是由固定格式的,并且在MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。
4.3 redo log 和 binlog 是怎么关联起来的?
它们有一个共同的数据字段,叫 XID
。崩溃恢复的时候,会按顺序扫描 redo log:
- 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
- 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
4.4 为什么处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢复:
这和数据与备份的一致性有关。在 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。
4.5 如果 redo log 写完,再写 binlog。崩溃恢复的时候,必须得两个日志都完整才可以,这种设计可以吗?
事务的持久性问题,对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。
5. 日志模块总结:
- redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log都直接持久化到磁盘。这个参数设置成 1,可以保证MySQL 异常重启之后数据不丢失。
- binlog是一种归档日志。sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘,可以保证 MySQL 异常重启之后 binlog 不丢失。