前言

最近在阅读小林coding的时候,看到了一些关于mysql日志的知识,便记录了这篇文章,仅作学习总结

一、有哪些日志

首先,我们能接触到的,一般我们排查慢查询时,会去看慢查询日志。如果做过数据备份会恢复的,可能接触或用过BinLog。那还有其他的吗?对MySQL原理比较了解的,应该知道还有Redo Log和Undo Log。这些都是比较常见:慢查询日志、Binlog、Redo Log、Undo Log。

其实,MySQL中还有:错误日志(errorlog)、一般查询日志(general log)、中继日志(relay log),只是这些大家接触少点。

总结下,MySQL中一共七种日志,分别为:慢查询日志、Binlog、Redo Log、Undo Log、错误日志(errorlog)、一般查询日志(general log)、中继日志(relay log)。

本文,我们重点看下Redo Log、Undo Log以及Binlog

二、更新语句执行

首先,我们了解下,一条更新语句是如何执行的,在更新的插入的过程中,各个日志的写入顺序是怎样的。MySQL的更新语句执行过程如下所示:

一条更新语句的执行过程为:

1、MySQL执行器负责执行,首先查询待更新的记录:

1)如果在Buffer Pool中,那么返回这条记录;

2)如果没在,那么从磁盘中读出数据,加载进Buffer Pool;

2、开启事务,首先记录Undo Log;

3、InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面;

4、然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache;

5、事务提交。采用两阶段的方式:

1)prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;

2)commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);

三、Redo日志

3.1 为什么要 redo log

对于数据库,一般具有ACID四个特性,这里,我们关注的是持久性,那么如何保证数据库的持久性了?

简单直接一点,就是每次新增更新数据,我们都直接刷到磁盘上就好了,只要刷到了磁盘上,那么持久性一般就得到了保障。但是,这里会有一个问题,刷盘是一个速度比较慢的过程,尤其这种随机IO的场景,如果我们每次更新都直接刷盘,那么性能肯定不高。那么如何提高性能了?

可以先把数据刷到Buffer Pool缓冲区中,然后再从Buffer Pool中刷到磁盘中(可以是个异步的过程),这样写的性能就提上去了。但是,Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。那么怎么做到既保证性能,又能保障持久性了?

为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术

WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上

后续当故障发生导致内存数据丢失后,InnoDB会在重启时,通过重放redo ,将数据恢复到崩溃前的状态。

3.2 什么是 redo log

redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 来恢复数据。

默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 :ib_logfile0 和 ib_logfile1 。每个 redo log File 的大小是固定且一致的,假设每个 redo log File 设置的上限是 1 GB,那么总共就可以记录 2GB 的操作。

重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。

所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:

图中的:

  • write pos 和 checkpoint 的移动都是顺时针方向;

  • write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;

  • check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录;

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。

所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。

3.3 redo log是直接写入磁盘吗

不是,redo log 也有自己的缓存,称为redo log buffer,默认大小是16MB,每当产生一条 redo log 时,会先写入到 redo log buffer,后续再持久化到磁盘。

3.4 redo log 什么时候刷盘

缓存在 redo log buffer 里的 redo log 还是在内存中,它什么时候刷新到磁盘?

主要有下面几个时机:

  • MySQL 正常关闭时;

  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;

  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘;

  • 由 innodb_flush_log_at_trx_commit 参数控制。

刷盘策略

innodb_flush_log_at_trx_commit 参数,可取的值有:0、1、2(默认值为 1),

这三个值分别代表的策略如下:

  • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。

  • 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。

  • 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache(如果你想了解 Page Cache,可以看这篇),Page Cache 是专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存。

3.5 redo log的作用

  • 实现事务的持久性,让 MySQL 有 crash-safe(崩溃恢复) 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;

  • 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。

四、Undo 日志

4.1 为什么要 undo log

我们在执行执行一条“增删改”语句的时候,虽然没有输入 begin 开启事务和 commit 提交事务,但是 MySQL 会隐式开启事务来执行“增删改”语句的,执行完就自动提交事务的,这样就保证了执行完“增删改”语句后,我们可以及时在数据库表看到“增删改”的结果了。执行一条语句是否自动提交事务,是由 autocommit 参数决定的,默认是开启。所以,执行一条 update 语句也是会使用事务的。

那么,考虑一个问题。一个事务在执行过程中,在还没有提交事务之前,如果 MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?

如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。

实现这一机制就是 undo log(回滚日志),它保证了事务的 ACID 特性 (opens new window)中的原子性(Atomicity)

undo Log中记录操作前的数据值。数据库以顺序IO的方式连续写入Undo Log,当事务需要回滚时,就通过undo Log来回滚事务。InnoDB就是这样做的。每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:

  • 插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;

  • 删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;

  • 更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。

在发生回滚时,就读取 undo log 里的数据,然后做相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。

一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:

  • 通过 trx_id 可以知道该记录是被哪个事务修改的;

  • 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

版本链如下图:

版本链

另外,undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制)

对于「读提交」和「可重复读」隔离级别的事务来说,它们的快照读(普通 select 语句)是通过 Read View + undo log 来实现的,它们的区别在于创建 Read View 的时机不同:

  • 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

  • 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列(trx_id 和 roll_pointer)」的比对,如果不满足可见行,就会顺着 undo log 版本链里找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。

4.2 undo log的作用

  • 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。

  • 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。

五、binlog日志

5.1 为什么要binlog

MySQL 在完成一条更新操作后,Server 层会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

binlog一般用户主从复制和备份恢复。

5.2 主从复制原理

具体详细过程如下:

  1. MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。

  2. 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。

  3. 从库会创建SQL线程去读取 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

会发现,上述这个是异步复制的模式,它会存在不一致以及数据丢失的问题?其实在分布式架构中,这种问题很多,比如Kafka,怎么保障kafka数据不丢了?我们重点看Producer,Producer发送消息,需要ISR都确认收到才算成功。类比到MySQL这里,怎么保障不丢了,我们可以这样,当所有从库都同步完成了,才算成功,这样就不会有问题了,这就是全同步复制。

全同步复制过于严苛了,将严重影响写入性能,我们可以退一步,只要至少一个同步完成就好,这样不需要等待所有从库完成,性能大大提高,这就是半同步复制。

总结一下同步方式,主要有三种:

  • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。

  • 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。

  • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险

六、对比分析

6.1 redo log 和 undo log有什么区别

这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:

  • redo log 记录了此次事务「修改后」的数据状态,记录的是更新之后的值,主要用于事务崩溃恢复,保证事务的持久性

  • undo log 记录了此次事务「修改前」的数据状态,记录的是更新之前的值,主要用于事务回滚,保证事务的原子性

事务提交之前发生了崩溃(这里的崩溃不是宕机崩溃,而是事务执行错误,mysql 还是正常运行的),重启后会通过 undo log 回滚事务。

事务提交之后发生了崩溃(这里的崩溃是宕机崩溃),重启后会通过 redo log 恢复事务

6.2 redo log 和 binlog 有什么区别?

1、适用对象不同:

  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用

  • redo log 是 Innodb 存储引擎实现的日志

2、写入方式不同:

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志

  • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志

3、用途不同:

  • binlog 用于备份恢复、主从复制

  • redo log 用于掉电等故障恢复

如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?

不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

七、总结

具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:

    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;

    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。

  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:

    • 如果一样的话就不进行后续更新流程;

    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;

  3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页

  4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。

  5. 至此,一条记录更新完了。

  6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。

  7. 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):

    • prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;

    • commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);

  8. 至此,一条更新语句执行完成。

参考文章

https://zhuanlan.zhihu.com/p/652252941

https://www.xiaolincoding.com/mysql/log/how_update.html