image

MySQL 有哪些日志?

  • 错误日志
  • 查询日志
  • 慢查询日志
  • 事务日志
  • 二进制日志

undo log

什么是 undo log?

undo log 是一种「逻辑日志」,用于记录数据修改前的信息

例如,当我们执行一次 insert 语句,undo log 中就会出现一条 delete 语句

undo log 的使用场景?

undo log 主要用于:

  • 事务回滚:原子性保障
  • MVCC 的版本链

redo log

什么是 redo log?

redo log 是一种物理日志,是事务的 持久性保障

当 MySQL 或者 OS 宕机而重新启动时,就可以使用 redo log 作数据恢复

在执行增删改操作时,会先将对应的操作记录到 redo log buffer 中,然后在「合适时机」将 redo log buffer 刷到磁盘中

image

刷盘时机

那什么时候会触发 redo log 的刷盘呢?

  • 事务提交:在事务提交的时候,会刷新 redo log buffer 到磁盘,具体策略通过 innodb_flush_log_at_trx_commit 选项控制
  • 后台线程:InnoDB 会启动一个后台线程,每 1s 将 redo log buffer 的数据刷到磁盘
  • Log Buffer 容量到达一半
  • 正常关闭 MySQL Server

可以发现,在 事务提交前,redo log 也是 有可能被刷新 到磁盘的

下面来看看 innodb_flush_log_at_trx_commit 选项:

innodb_flush_log_at_trx_commit = 0

image

当 innodb_flush_log_at_trx_commit = 0 时:

  • 在事务执行期间,事务线程将 redo log 写到 redo log buffer
  • 后台线程每隔 1s 调用 write,将 redo log buffer 写到 OS 的 page cache,并 立即调用 fsync,保证数据的落地
  • 事务提交后,事务线程不需要执行任何操作

这个选项提供了 最好的性能,因为数据真正刷新到磁盘的操作是由后台线程执行的

但是,这个选项存在 数据丢失 的风险:如果在后台线程在调用 write 前,MySQL 挂了,那么这 1s 产生的 redo log 就丢失了

innodb_flush_log_at_trx_commit = 1

image

innodb_flush_log_at_trx_commit = 1 时,与之前不同,事务提交时,事务线程主动调用 write,将 redo log buffer 的数据写到 OS 的 page cache,并立即调用 fsync,完成数据的落地,最后再通知客户端事务已经提交

这个选项提供了 最高的可靠性:只要事务提交,就能保证 redo log 已经刷新到磁盘

就算 MySQL 挂了,由于事务并没有提交,丢失了 redo log 也没关系

同时,这个选项的 性能是最差 的:每次提交事务,都涉及磁盘 IO

innodb_flush_log_at_trx_commit = 2

image

innodb_flush_log_at_trx_commit = 2 时,事务提交时,事务线程 只会调用 write,将 redo log buffer 写到 OS 的 page cache,然后通知客户端事务已经提交

这个选项的可靠性相较于等于 0 时,更高:事务提交,只要 OS 没挂,就能保证 redo log 的落地

同时性能也不错:只是调用了 write,并没有涉及磁盘 IO,只涉及将用户态的 redo log buffer 写到内核态的 page cache

总结:

  • 如果需要绝对的性能,即使丢失 1s 的数据也没关系,设置为 0
  • 如果需要绝对的可靠性,设置为 1
  • 如果对可靠性要求不太高,同时对性能有一定要求,设置为 2

日志文件组

redo log file 实际上并不只有一个文件,而是多个,以日志文件组的形式出现

redo log file 是循环写入的:多个 redo log file 之间构成一个「循环」,就像一个环形数组:

image

数据从 buffer pool 刷到磁盘后,对应的 redo log 也就没有作用了

日志文件组通过 write poscheckpoint 来标记两个特殊的位置

image

  • 图中绿色的部分是还可以写入的部分
  • 图中红色的部分是 buffer pool 中,还没来得及写到磁盘的数据对应的 redo log

写入数据到 redo log 时,write pos 会顺时针移动

随着数据从 buffer pool 刷到磁盘,checkpoint 也会顺时针移动

当 write pos 追上 了 checkpoint,说明没有位置写入 redo log 了,此时,MySQL 会 停下来,等待 buffer pool 中的数据写入到磁盘,以此释放 redo log 的空间

从 MySQL 8.0.30 后,redo log file 的数量被确定为 32 个,可以修改配置文件中的 innodb_redo_log_capacity 来控制 redo log 的总大小

为什么要使用 redo log

也许你会问:搞这么复杂干嘛,提交事务时,直接将修改后的数据写到磁盘不就能保证持久性吗?还要 redo log 干什么呢?

理论上的确可以,但是存在性能问题:

修改的数据可能原本存放在磁盘的不同位置,如果每次修改都要直接写到磁盘(跳过 buffer pool 以及 redo log),会产生大量 随机 IO

而随机 IO 是很慢的

因此为了保证写入性能,在修改数据时:

  • 先将修改的数据写到 buffer pool
  • 再写到 redo log file(防止 MySQL 崩溃,buffer pool 的数据丢失,保证持久性)

那你可能会问:写 redo log 不也是磁盘 IO 吗,为什么性能会更好呢?

根据 上面的分析 :写入 redo log file 这个过程是 append(追加) 的,即 顺序 IO,性能更好(这个技术又叫做 WAL:write-ahead logging)

注意:

虽然写入 redo log 是顺序 IO,但是 最终的修改操作,还是随机 IO

因此,使用 redo log 的目的:

  • 将事务执行期间涉及到的随机 IO 转化为顺序 IO,提高读写性能
  • 防止 MySQL 崩溃后,buffer pool 的数据丢失,保证持久性(重启后可以用 redo log 恢复数据)

binlog

binlog 是物理日志,记录了执行过的 SQL 语句,用于 数据备份主从同步,保证数据一致性

记录格式

binlog 有三种记录格式:

  • Statement
  • Row
  • Mixed

Statement 记录的是 原始的 SQL 语句,例如:update users set update_at = now();

记录原始的 SQL 语句存在一个问题:如果有「动态函数」,如 now(),会 存在数据不一致问题

为了解决 Statement 数据不一致问题,可以使用 Row 格式,记录的不单单是原始 SQL 语句,还 包括实际值,例如:update users set update_at = '2024-02-24-19-05-12';

但是采用这种格式,会 占用比较多的空间,在数据恢复和主从同步时,会占用较多 IO 资源

于是可以使用 Mixed 格式:MySQL 会 自行选择 某一个 SQL 语句该使用 Statement 还是 Row 格式来记录

然而,现在很多场景下,都将记录格式设置为 Row,而不是 Mixed,因为 binlog 还可以做 数据恢复 使用

为什么不用 redo log?

redo log 只适合灾难恢复,循环写入机制决定了文件会被覆盖

所以使用 Row 格式记录 binlog(接受文件大的缺点以换取恢复能力)

写入机制

事务需要保证 原子性,无论事务多大,都需要确保一次性写入,因此,Server 会给 每一个事务线程 分配一个 binlog buffer

如果 binlog buffer 不够,会暂时将 binlog 写到磁盘

当一个事务提交时,工作线程会调用 write,将 binlog buffer 的内容写到 OS 的 page cache

image

那什么时候将 page cache 的内容刷到磁盘呢?

这个由 sync_binlog 参数控制:

  • 如果为 0,那么由 OS 来决定什么时候刷到磁盘
  • 如果为 1,那么事务结束后,除了调用 write 外,还要主动调用 fsync 将 page cache 的内容写到磁盘
  • 如果为 N(N > 1),每次事务结束后,都会调用 write,但是只有累积了 N 个事务,才调用 fsync

默认值为 1,保证事务提交后,binlog 一定持久化到磁盘,如果对数据有一定可靠性需求,并且还要性能,那么可以将 N 的值设置的稍大一些

与 redo log 的区别

  • redo log 是在 InnoDB 实现的,而 binlog 是在 Server 层实现的,不论什么引擎都能用
  • redo log 记录的是 “在某个数据页,做了什么修改”,而 binlog 记录的是 “数据修改的原始逻辑”
  • redo log 有可能在事务提交前就部分持久化到磁盘了,而 binlog 只有在事务提交后,才有可能持久化到磁盘

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

你可能会问:为什么有了 redo log,还要有 binlog?redo log 不是已经可以保证数据的持久化了吗,binlog 是不是有点多余 ?

redo log 是在 InnoDB 引擎实现的,而早期的 MySQL 采用 MyISAM 作为默认存储引擎,不支持 redo log,也就无法保证 crash safe

因此 MySQL 在 Server 层实现 binlog,保证在任何存储引擎下的 crash safe

此外,由于 binlog 是在 Server 层实现的,更有利于用于主从同步

反过来问:为什么有了 binlog,还要有 redo log?

binlog 没有一个类似「锚点」的功能,即 crash 后,无法得知应该从哪里恢复数据,此外 binlog 比较重,恢复起来也比较慢

binlog 的这个特点决定了不太适合做 crash recovery

还有一个历史因素:InnoDB 是作为一个「插件」用于 MySQL 的,redo log 作为 InnoDB 自己的 crash recovery,性能更好

总结一波:redo log 与 binlog 可以共同协作,专心的做自己的事

  • redo log 负责 crash recovery,数据的持久化保障
  • binlog 负责数据备份,以及主从同步(集群数据一致性保障)

两阶段提交

前面提到了:“redo log 有可能在事务提交前就部分持久化到磁盘了,而 binlog 只有在事务提交后,才有可能持久化到磁盘”

那么这个是否会存在问题呢?

来看一下这个场景:

image

图片来自 JavaGuide

在 redo log 持久化之后,binlog 写入磁盘之前,MySQL 挂了,重新启动后,InnoDB 根据 redo log 恢复数据

注意,这里恢复的数据就是 更新后的数据 了,但是 binlog 并没有写入成功,于是造成了 主从不一致问题

image

MySQL 采用 两阶段提交 来避免主从不一致问题

两阶段提交将 事务提交过程 分为两个阶段:

  • 准备阶段(prepare)
  • 提交阶段(commit)

使用两阶段提交时,MySQL 内部开启一个 XA 事务:

在 prepare 阶段,将 XID 写入 redo log,并 将 redo log 状态设置为 prepare,然后调用 write、fsync(类似于 innodb_flush_log_at_trx_commit = 1),将 redo log 持久化到磁盘

在 commit 阶段,将 XID 写入 binlog,然后调用 write、fsync(类似于 sync_binlog = 1),将 binlog 持久化到磁盘,并 将 redo log 状态设置为 commit

image

来看看两阶段提交是如何解决主从不一致问题的:

还是假设 redo log 持久化到磁盘,在 binlog 持久化到磁盘之前,MySQL 挂掉

那么重启以后,MySQL 开始做 crash recovery,读到 redo log,检查 redo log 状态:

  • 处于 prepare 状态:寻找 binlog,看看有没有与其 XID 一致的 binlog
    • 如果有,说明 binlog 成功持久化,提交事务
    • 如果没有,说明 binlog 持久化失败,回滚事务
  • 处于 commit 状态:说明 binlog 成功持久化,提交事务

在这个假设中,是没有的,于是,MySQL 会 回滚事务,这样,主从一致性保证

总结

undo log

回滚、MVCC 的基础,事务原子性保障

redo log

redo log 以 日志文件组 的形式存储的,是事务持久性保障,用于灾难恢复

在事务提交阶段,可以通过 innodb_flush_log_at_trx_commit 来控制刷盘策略

binlog

数据备份,主从同步 都离不开 binlog,两阶段提交保证了主从一致性