MySQL 日志 - undo log, redo log, binlog

MySQL 需要了解的有三种日志:undo log/回滚日志、redo log/重做日志、binlog/归档日志

  • undo log: 是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC
  • redo log: 是 Innodb 存储引擎层成成的日志,实现了事务中的持久性,主要用于系统崩溃后的数据恢复
  • binlog: 是 Server 层生成的日志,主要用于数据备份(恢复)和主从复制

undo log

undo log (回滚日志) 是用于撤销回退的日志。在事务提交之前,MySQL 会记录更新前的数据到 undo log 日志文件中,当事务回滚时,可以利用 undo log 来进行回滚

每当 InnoDB 对一条记录进行增删改操作时,都要把回滚时需要的信息记录到 undo log 中,在发生回滚时,就读取 undo log 中的数据,执行相反操作即可

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

  • 通过 trx_id 可以知道该记录是被哪个事务修改的
  • 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链

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

对于读提交和可重复读隔离级别的事务来说,他们的快照读是通过 Read View 和 undo log 实现的。他们的区别在意穿件 Read View 的时机不同

  • 读提交:每个 select 都会生成一个新的 ReadView,也意味着,事务期间的多次读取同一条数据,前后两次读取的数据可能不一致
  • 可重复读:会在启动事务时生成一个 Read View,整个事务期间都使用这个 Read View

这两个隔离级别是通过事务的 ReadView 中的字段和记录中的两个隐藏列的对比,如果不满足,就会顺着 undo log 版本链找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为。

因此 undo log 有两大作用:

  • 实现事务回滚,保证事务原子性
  • 作为实现 MVCC 的关键因素之一

buffer pool

MySQL 的数据存在磁盘中,那么在更新记录时,需要先从磁盘读取该记录,然后再内存后修改。修改完之后,如果选择缓存起来,那下次有查询语句命中这条记录时就可以直接读取,而不需要再次访问磁盘

因此 InnoDB 设计了一个缓冲池,来提高数据库的读写性能,既 Buffer Pool

  • 当读取数据时,如果数据存在于 Buffer Pool 中,就会直接读取 Buffer Pool 的数据
  • 当修改数据时,如果数据存在于 Buffer Pool 中,那就直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页数据与磁盘数据已经不一致)。为了减少磁盘 IO,不会立即将脏页写入磁盘,而是由后台线程选择一个合适的时机将脏页写入磁盘

Buffer Pool 缓存什么?

InnoDB 把数据分为很多个「页」,以页作为磁盘与内存交互的基本单位。一个页的默认大小为 16KB,因此,Buffer Pool 同样是以页来划分

在 MySQL 启动时,InnoDB 回味 Buffer Pool 申请一片连续的内存空间, 然后按照默认的 16KB 大小划分出一个个的页,Buffer Pool 中的页就叫做缓存页。

Buffer Pool 除了缓存「索引页」、「数据页」,还包括了 Undo 页、插入缓存、自适应哈希索引、锁信息等

Undo 页:开启事务后,InnoDB 层更新记录前,首先要记录响应的 undo log,如果是更新操作,需要把被更新的列旧值记录下来,也就是生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面

查询一条记录时,InnoDB 会把记录所在页整个加载到 Buffer Pool 中,之后再通过页内的页目录去定位某条具体的记录

redo log

Buffer Pool 提高了读写效率,但是由于 Buffer Pool 是基于内存的,在遇到断电崩溃等问题时比哦那个不能很好的保存数据。因此为了防止数据丢失的问题,当一条记录需要更新时,InnoDB 会先更新内存,同时标记为脏页,然后将本次修改以 redo log 的形式记录下来。

后续 InnoDB 会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷到磁盘里,这就是 WAL 技术(Write-Ahead Logging)

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

redo log 用于记录某个数据页做了什么修改,例如“对 xx 表空间中的 yy 数据页 zz 偏移量的地方做了 aa 更新”,每执行一个事务就会产生这样的一条或多条日志

在事务提交时,只要先将 redo log 持久化到磁盘即可,不需要将 Buffer Pool 中的数据持久化到磁盘

当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化了,在系统重启后,可以根据 redo log 将数据恢复到最新状态

undo 页面被修改,需要记录对应 redo log 吗

需要,在内存修改 Undo 页面后,也需要记录对应的 redo log,因为 undo log 也需要实现持久化

redo log 和 undo log 的区别

  • redo log 记录了此次事务修改后的数据状态,主要用于崩溃恢复,保证持久性
  • undo log 记录了此次事务修改前的数据状态,主要用于事务回滚,保证原子性

redo log 要写磁盘,数据也要写磁盘,为什么要多此一举

redo log 是追加操作,磁盘操作为顺序写。数据写磁盘是随机写。因此 redo log 的磁盘写入更加高效,同时保证了 MySQL 有崩溃恢复的能力

redo log 是产生后直接写磁盘的吗

不是,实际有一个 redo log buffer,产生的 redo log 会先写入 buffer,再写入磁盘

redo log 什么时候刷盘

  • MySQL 正常关闭时
  • redo log buffer 中的写入量大于其内存空间的一半时
  • InnoDB 的后台线程每隔一秒触发一次
  • 每次事务提交时(由 innodb_flush_log_at_trx_commit 控制)

redo log 文件写满了怎么办

默认情况下,InnoDB 有 1 个 redo log group,由两个 redo log 文件组成。在组中,每个文件的大小是固定且一致的。组内是以循环写的方式工作的,相当于构成了一个环

binlog

以上的 redo log 和 undo log 都是 InnoDB 引擎生成的,而 binlog 是 server 层生成的

binlog 是记录了所有数据库表结构和表数据变更的日志,不会记录查询操作。

为什么有了 binlog 还要有 redo log

最开始 MySQL 没有 InnoDB 引擎,自带引擎是 MyISAM,它并没有崩溃恢复能力,binlog 只用于归档。之后另一个公司以插件形式引入了 InnoDB

binlog 和 redo log 的区别

  • 适用对象不同
    • binlog 是 server 层的实现,与存储引擎无关
    • redo log 是 InnoDB 实现的
  • 文件格式不同
    • binlog 有三种格式
      • STATEMENT: 每一条修改数据的 SQL 都会记录到 binlog 中,存在动态函数问题。例如 sql 中使用了 uuid 或 now 等函数,多次执行时数据不一致
      • ROW:记录行被修改后的数据。执行 update 时,如果修改了多行数据,则会记录多行数据的变化,会导致 binlog 文件过大
      • MINED:根据情况自动选择以上两种模式
    • redo log 是物理日志,记录的是某个数据页做了什么修改
  • 写入方式不同
    • binlog 是追加写,写满一个文件后会创建新的文件,不会覆盖以前的日志,保存的是全量日志
    • redo log 是循环写,日志空间大小固定
  • 用途不同
    • binlog 用于备份恢复、主从复制
    • redo log 用于故障恢复

不小心删除数据库的数据,可以使用 redo log 恢复?

不可以,redo log 不是全量数据,可以使用 binlog

主从复制怎么实现?

MySQL 主从复制依赖 binlog,整个复制过程可以分为三个阶段:

  • 写入 Binlog:主库写 binlog 日志
  • 同步 BinLog:把 binlog 复制到所有从库,每个从库把 binlog 写入暂存日志
  • 重放 Binlog:从库回放 binlog,并更新存储引擎的数据

从库越多越好?

不是,从库数量增加,主库收到的连接也会增加,对主库资源消耗比较多。因此实际使用中,一个主库一般配 2-3 个从库

MySQL 主从复制还有哪些模型?

  • 同步复制:主库提交事务的线程要等所有从库复制成功,才返回客户端结果。
  • 异步复制(默认):提交事务的线程不会=等待 binlog 同步到各从库,直接返回客户端响应
  • 半同步复制:MySQL 5.7 新增,事务线程不会等待所有从库返回结果,只要数据复制到任意一个从库,主库就会返回响应

binlog 什么时候刷盘

事务执行过程中,先把日志写入 binlog cache,事务提交时,再把 binlog cache 写入 binlog 文件

一个事务中的 binlog 是不能被拆开的,因此无论事务中有多少执行语句,也要保证一次性写入

MySQL 给每个线程分配了一片内存用于缓冲 binlog,该内存叫 binlog cache

binlog cache 什么时候写入 binlog 文件?

在事务提交时,会把 binlog cache 中的完整事务写入 binlog 中,并清空 binlog cache

整个写入过程包含两个过程:

  • 写入文件系统 page cache
  • 通过 fsync 持久化到磁盘

MySQl 提供了 sync_binlog 参数来控制binlog 刷新到磁盘的频率

  • sync_binlog = 0:每次提交都只写入 page cache,后续由操作系统决定何时写入磁盘
  • sync_binlog = 1:每次提交事务都写入 page cache 并立即写入磁盘
  • sync_binlog = N (>1): 每次提交事务都写入 page cache,累积 N 个事务之后写入磁盘

Update 语句的执行过程

优化器分析出成本最小的执行计划之后,执行期将按照执行计划进行更新操作

假设语句为 update users set age = 20 WHERE id = 1;

  1. 执行器调用存储引擎接口,通过主键索引搜索获取 id = 1 的记录;
    • 如果此行数据存在于 buffer pool,则直接返回
    • 如果不存在 buffer pool 中,则读入 buffer pool,并返回
  2. 执行器获取到记录,查看数据与语句执行后的数据是否一样
    • 如果一样则不执行后续流程
    • 如果不一样则把更新前后的记录都作为参数传给 InnoDB,让 InnoDB 真正执行更新操作
  3. InnoDB 执行前,需要记录相应的 undo log,把被更新列的旧值记录下来。undo log 会写入 Buffer Pool 的 Undo 页,在内存修改该 Undo 页之后,需要记录对应的 redo log
  4. InnoDB 开始更新记录
    • 先更新 Buffer Pool 并标记为脏页,然后将记录写入 redo log
    • 为了减少磁盘 IO,脏页不会立即写入磁盘,而是先写入 redo log
  5. 在 InnoDB 执行完成后,Server 层记录 binlog,此时 binlog 将保存在 binlog cache
  6. 事务提交,两阶段提交

两阶段提交

事务提交之后,redo log 和 binlog 都需要持久化到磁盘,但由于两个过程是独立的,因此可能会出现两份日志不一致的情况:

  • redo log 刷入磁盘之后,MySQL 宕机导致 binlog 没来得及写入:重启后可以通过 redo log 将更新数据恢复,但是由于 binlog 中没有记录,会导致从库没有同步这一条数据修改
  • binlog 刷入磁盘之后,MySQL 宕机导致 redo log 没来得及写入:重启后无法通过 redo log 恢复这条数据,但是由于 binlog 中存在这条语句,因此从库会通过 binlog 执行,导致数据不一致

为了避免这种情况,MySQL 使用了「两阶段提交」来解决,两阶段提交其实是分布式事务一致性协议,用来保证多个操作逻辑要么全部成功,要么全部失败

MySQL 使用「内部 XA 事务」来保证两个日志的一致性,由 binlog 作为协调者,存储引擎作为参与者。执行分为两个阶段:

  • prepare 阶段
    • 将 XID(内部 XA 事务的 ID)写入 redo log,同时将 redo log 对应的事务状态设置为 prepare
    • 将 redo log 持久化到磁盘
  • commit 阶段
    • 将 XID 写入 binlog,然后将 binlog 持久化到磁盘
    • 调用引擎的提交事务接口,将 redo log 状态设置为 commit
    • 此时 redo log 的状态不需要持久化到磁盘,只需 write 到文件系统的 page cache 即可。因为只要 binlog 写磁盘成功,就算 redo log 的状态仍为 prepare 也会视为事务已经执行成功

两阶段提交的问题

  • 磁盘 IO 次数高。在 redo log 和 binlog 的刷盘设置都为 1 时,每个事务提交都要进行两次 fsync,一次是 redo log刷盘,一次是 binlog 刷盘
  • 锁竞争激烈。两阶段提交可以保证单个事务的两个日志内容一致,但在多事务并行的情况下不能保证两者提交顺序一致。因此需要加一个锁来保证提交的原子性
    • 一个事务获取到锁之后才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁

组提交

MySQL 引入了「组提交」机制来解决这个问题。当有多个事务提交的时候,会将多个 binlog 刷盘操作合并为一个,从而减少磁盘 IO 的次数

引入组提交之后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

  • flush 阶段:多个事务按进入顺序将 binlog 从 cache 写入文件(不刷盘)
  • sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘)
  • commit 阶段:各个事务按顺序做 InnoDB commit 操作

上述每个阶段都有一个队列,每个阶段有锁进行保护,从而保证了事务写入的顺序。第一个进入队列的事务成为 leader,leader 领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束

对每个阶段引入队列后,锁就只针对每个队列进行保护,不在锁住提交事务的整个过程。锁粒度变小之后就可以使多个阶段并发执行,从而提高效率

redo log 有组提交吗

MySQL 5.6 没有

NySQL 5.7 有

在 5.6 的组提交逻辑中,每个事务各自执行 prepare 阶段,也就是各自将 redo log 刷盘,这样就没有办法组提交

5.7 版本中做了改进,在 prepare 阶段不再执行 redo log 刷盘,而是推迟到组提交的 flush 阶段。通过延迟写 redo log 的方式,为 redo log 做了一次组写入

Licensed under CC BY-NC-SA 4.0
最后更新于 Oct 06, 2020 00:00 UTC