当前位置: 首页 > article >正文

MySQL 日志:undo log、redo log、binlog 概述

在 MySQL 中一条完整的更新语句会涉及到多个日志文件,即 undo log(回滚日志)、redo log(重做日志)以及 binlog(归档日志),下面对这三种日志进行介绍。

1.undo log

1.1为什么需要 undo log?

我们知道数据库事务具有 ACID 特性,其中 A(Atomicity)指的是原子性,即事务中的一组操作要么都成功还要都失败。这就引出了一个问题:在事务还没提交之前,如果 MySQL 发生了崩溃,或者用户手动进行了回滚操作,那 MySQL 怎么知道事务执行之前的数据是什么样的呢?

如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志文件里。那么我们就可以通过这个日志将数据回滚成事务执行之前的样子。这个日志就是 undo log(回滚日志),它保证了事务的原子性。

每当 InnoDB 引擎对一条记录进行写操作(插入、删除、更新)时,都会把回滚时需要的信息记录到 undo log 里,比如:

  • 插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
  • 删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
  • 更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。

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

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

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

版本链如下图:

image-20241221201646407

另外,undo log 还有一个作用,通过 Read-View + undo log 实现 MVCC(多版本并发控制)。ReadView 决定了当前事务对 trx_id 为哪些的记录是可见的,而 undo log 帮助 MySQL 找到这些记录。

还是以上面的版本链图为例,最初是由 trx_id 为 80 的事务新增了一条记录。然后 trx_id 为 100 的事务(记为 T1) 和 trx_id 为 200 的事务(记为 T2)分别对这条记录进行了修改。此时在事务 T1 中进行一致性读(普通的 select),那么它只能看到 trx_id 为 80 或 100 的记录,而这些记录有哪些值就是通过 undo log 版本链去拿到的。

更多 MVCC 的理解可参考:MVCC 详解

因此,undo log 有两大作用:

  • 实现事务回滚,保障事务原子性。
  • 与 Read-View 一起构成了 MVCC 的基础。

1.2undo log 的类型与清理时机

不同类型的写操作需要记录的内容也是不同的,所以产生的 undo log 格式也是不同。在 InnoDB 中,可分为:

  • insert undo log

    在 insert 操作中产生的 undo log 被称为 insert undo log。由于 insert 操作的记录只对当前事务本身可见,对其他事务不可见(否则就是幻读了),因此该 uodo log 可以在事务提交后直接删除。不需要通过 purge 线程去清理。

  • update undo log

    在 delete 或 update 操作时产生的 undo log 被称为 update undo log。由于 MVCC 中可能会用到该 undo log,因此不能再事务提交后立刻删除。因此该 uodo log 会被加入到一个链表中,等待 purge 线程去清理。

1.3undo log 生命周期

事务执行期间,在记录发生更新前,首先要记录相应的 undo log。如果是 update 操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,并根据该 undo log 去修改 Buffer Pool 中的 Undo 页面。

在修改该 Undo 页面后,也是需要记录对应的 redo log,因为 undo log 需要基于 redo log 来实现持久化

此外,内存中的 undo log 是会被删除清理的,例如 insert 操作在事务提交之后就可以清除掉了对应的 undo log;update 或 delete 操作则由后台线程 purge 进行清理。

2.redo log

2.1为什么需要 redo log?

为了提高数据库的读写能力,MySQL 引入了 Buffer Pool:

  • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
  • 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘。

但 Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。

为了避免这个问题,一种做法是:在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘?(记为方法一)

但 MySQL 的设计者并没有使用这种做法,因此这个简单粗暴的做法有两大问题:

  • 修改量与刷新磁盘工作量严重不成正比

    我们知道 MySQL 内存和磁盘交互的最小单位是,这意味着在上面的做法中,即使我们只修改了一条记录,也需要把整个页都刷到磁盘(刷脏页),这就好比快递员送快递时每次只送一个包裹,效率是很低的。

  • 随机 IO 速度慢

    一个事务可能会修改多个数据页,加入这些页面并不相邻,就意味着将某个事务修改的 Buffer Pool 中的脏页刷新到磁盘时,会进行很多的随机 IO,而随机 IO 比顺序 IO 慢很多,尤其是对于传统的机械硬盘来说。

另一种做法是采用 redo log。redo log 是一种物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个读写事务就会产生这样的一条或者多条这样的日志。当 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎就会使用 redo log 恢复数据,保证数据的持久性与完整性。

image-20241221212830266

那么 redo log 是如何被记录的呢?

以一个更新事务为例,redo log 的流转过程如下图所示:

image-20241222123240534
  1. 先将原始数据从磁盘读入到内存,并对数据进行修改(标记为脏页);
  2. 将本次修改记录到 redo log buffer;
  3. 当事务 commit 时,将 redo log buffer 中的内容刷新到 redo log file**(该过程为顺序 IO,效率很高);**
  4. 在合适的时机将 pool buffer 中的脏页数据刷新到磁盘数据文件中;

这种先写日志,再刷磁盘的策略,即所谓的 WAL(Write-Ahead Logging)。

由于 redo log 日志写入数据量小(仅记录对某个数据页做了什么修改),且是顺序 IO(写入效率高),且不会有频繁的刷脏页操作。因此相比方法一性能更优。

2.2redo log 刷盘策略(非刷脏页)

事实上 redo log 刷盘时,并不是直接由 redo log buffer 刷到 redo log file,而是先写入到 page cache(文件系统缓存),再调用 fsync 方法同步到 redo log file 中。

image-20241222142735926

而什么时候写入到 page cache,什么时候同步到 redo log file,是由我们接下来要说的 redo log 刷盘策略决定的。

在 InnoDB 中通过 innodb_flush_log_at_trx_commit 参数来控制事务提交时 redo log 的刷盘策略:

  • **设置为 0:**设置为 0 的时候,表示每次事务提交时即不写到 page cache,也不同步到 redo log file。由 InnoDB 后台线程每隔 1 秒去写入 page cache,然后调用 fsync 同步到 redo log file。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
  • **设置为 1:**设置为 1 的时候,表示每次事务提交时会写到 page cache,并同步到 redo log file。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
  • 设置为 2:设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 里的 redo log 内容写入 page cache。再由操作系统(os)决定什么时候同步到 redo log file 中(依赖于操作系统后台线程)。这种方式的性能和安全性都介于前两者中间。

下面是不同刷盘策略的流程图:

1.innodb_flush_log_at_trx_commit=0

image-20241222143944954

参数为 0 时,如果 MySQL 挂了或宕机,可能会丢失 1 秒内的数据。

为什么是 1 秒?因为 InnoDB 有个后台线程每隔 1 秒将 redo log buffer 中的内容写到 page cache,然后调用 fsync 刷到磁盘。

2.innodb_flush_log_at_trx_commit=1

image-20241222144139716

为 1 时, 只要事务提交成功,redo log 记录就一定在硬盘里(这点实际上是有二阶段提交来保证的),不会有任何数据丢失。

如果事务执行期间 MySQL 挂了或宕机,会导致这部分日志丢失。但由于事务并没有提交,所以日志丢了也不会有损失,直接回滚即可。

3.innodb_flush_log_at_trx_commit=2

image-20241222145808058

为 2 时, 只要事务提交成功,redo log buffer 中的内容只写入文件系统缓存(page cache)。

如果仅仅只是 MySQL 挂了不会有任何数据丢失(会有操作系统的后台线程去把 page cache 中的 redo log 日志同步到 redo log file),但是宕机可能会有 1 秒数据的丢失

小贴士:

InnoDB 和操作系统(os)都有后台线程负责刷盘,不要把两者搞混了。

2.5redo log 日志文件组

硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的 redo log 日志文件大小都是一样的。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录 4G 的内容。它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示:

image-20241222150102634

在这个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint

  • write pos 是当前记录的位置,一边写一边后移
  • checkpoint 是当前要擦除的位置,也是往后推移

每次刷盘 redo log 记录到日志文件组中,write pos 位置就会后移更新。

每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录,并把 checkpoint 后移更新。

write pos 和 checkpoint 之间的还空着的部分可以用来写入新的 redo log 记录。

image-20241222150226325

如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。

image-20241222150308008

2.4undo log + redo log 完成读写事务

undo log 和 redo log 的写入时机和刷盘时机如下图所示:

image-20241222151954587

从上图可以看到,undo log 的写入时机是在数据更新到内存前,而 redo log 的写入时机是在数据更新到内存后。

为了更好地理解这个过程,我们通过一个例子来进行说明:

对于某个读写事务,假设有 2 个数值,分别为 A = 1 和 B = 2,然后将 A 修改为 3,B 修改为 4。整个过程可简化为:

步骤操作
步骤 1begin;
步骤 2记录 A = 1 到 undo log;
步骤 3update A = 3;
步骤 4记录 A = 3 到 redo log;
步骤 5记录 B = 2 到 undo log;
步骤 6update B = 4;
步骤 7记录 B = 4 到 redo log;
步骤 8将 redo log 刷新到磁盘;
步骤 9commit;
  • 在步骤 1-7 中的任意一个步骤发生系统宕机,由于事务未提交,因此可以借助 uodo log 直接回滚数据,不会对事务的原子性有任何影响。
  • 在步骤 8 发生宕机,MySQL 重启后可以选择借助 undo log 回滚数据,也可以选择继续完成事务提交,因为此时 redo log 已经完成了持久化。
  • 在步骤 9 之后宕机,即使内存中的脏页还没来得及刷到磁盘,在 MySQL 重启后,可以根据 redo log 把数据刷到磁盘,完成数据的恢复。

3.binlog

3.1为什么需要 binlog?

binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于 “给 ID=2 这一行的 c 字段加 1”。binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写,属于 MySQL Server 层。

不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。

那 binlog 到底是用来干嘛的?

可以说 MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。另外,如果需要恢复某个时间节点后的数据,也需要借助 binlog。

image-20241222171620485

3.2binlog的记录格式

binlog 日志有三种格式,可以通过 binlog_format 参数指定。

  • statement(默认格式):记录 SQL 原文,但有动态函数的问题;
  • row:记录操作和具体数据,但占用空间,且消耗 IO 资源;
  • mixed:折中方案,为前两者的混合;

指定 statement,记录的内容是 SQL 语句原文,比如执行一条 update T set update_time=now() where id=1,记录的内容如下:

同步数据时,会执行记录的 SQL 语句,但是有个问题,update_time=now() 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。

为了解决这种问题,我们需要指定为 row,记录的内容不再是简单的 SQL 语句了,还包含操作的具体数据,记录内容如下:

image-20241222172208527

row 格式记录的内容看不到详细信息,要通过 mysqlbinlog 工具解析出来。

update_time=now() 变成了具体的时间 update_time=“1627112756247”,条件后面的@1、@2、@3 都是该行数据第 1 个 - 3 个字段的原始值(假设这张表只有 3 个字段)。

这样就能保证同步数据的一致性,通常情况下都是指定为 row,这样可以为数据库的恢复与同步带来更好的可靠性。

但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。

所以就有了一种折中的方案,指定为 mixed,记录的内容是前两者的混合。

MySQL 会判断这条 SQL 语句是否可能引起数据不一致,如果是,就用 row 格式,否则就用 statement 格式。

3.3binlog 刷盘策略(非刷脏页)

binlog 的写入时机非常简单,事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为 binlog cache。

我们可以通过 binlog_cache_size 参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘。

binlog 日志刷盘流程如下:

image-20241222172554663

  • 上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快
  • 上图的 fsync,才是将数据持久化到磁盘的操作;

write 和 fsync 的时机,可以由参数 sync_binlog 控制:

  • **设置为 0:**表示每次提交事务都只 write;
  • **设置为 1(默认):**每次事务提交时即 write 又 fsync;
  • **设置为 N:**每次提交事务都 write,但累计 N 个事务后才 fsync;

和 redo log 很像吧~

为 0 的时候,表示每次提交事务都只 write,由系统自行判断什么时候执行fsync。

image-20241222172708636

虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。

为了安全起见,可以设置为 1,表示每次提交事务都会执行 fsync,就如同 redo log 日志刷盘流程 一样。

最后还有一种折中方式,可以设置为 N(N>1),表示每次提交事务都 write,但累积 N 个事务后才 fsync。

image-20241222172807261

在出现 IO 瓶颈的场景里,将 sync_binlog 设置成一个比较大的值,可以提升性能。

同样的,如果机器宕机,会丢失最近 N 个事务的 binlog 日志。

4.两阶段提交

事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。

举个例子,假设 id = 1 这行数据的字段 name 的值原本是 ‘jay’,然后执行 UPDATE t_user SET name = ‘xiaolin’ WHERE id = 1; 如果在持久化 redo log 和 binlog 两个日志的过程中,出现了半成功状态,那么就有两种情况:

  • 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。MySQL 重启后,通过 redo log 能将 Buffer Pool 中 id = 1 这行数据的 name 字段恢复到新值 xiaolin,但是 binlog 里面没有记录这条更新语句,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了这条更新语句,从库的这一行 name 字段是旧值 jay,与主库的值不一致性;
  • 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 id = 1 这行数据的 name 字段还是旧值 jay,而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么这一行 name 字段是新值 xiaolin,与主库的值不一致性;

可以看到,在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

MySQL 为了避免出现两份日志之间的逻辑不一致的问题,使用了两阶段提交来解决,两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。

原理很简单,将 redo log 的写入拆成了两个步骤 preparecommit。即成功写入 redo log 后将其设置为 prepare 阶段,等待 binlog 写入后在将 redo log 设置为 commit 状态,完成整个事务的提交。

image-20241222183631292

使用两阶段提交后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于 prepare 阶段,并且没有对应 binlog 日志,就会回滚该事务。

image-20241222184217052

再看一个场景,redo log 设置 commit 阶段发生异常,那会不会回滚事务呢?

image-20241222184255407
并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于 prepare 阶段,但是能通过事务 id 找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。

6.三种日志的对比与总结

日志类型:

  • undo log:记录的是逻辑日志,比如对某一行数据进行了 insert 操作,那么 undo log 就记录一条与之相反的 delete 操作;
  • redo log:记录的是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
  • binlog:记录的是逻辑日志,记录内容是语句的原始逻辑,类似于 “给 ID=2 这一行的 c 字段加 1”;

日志的使用场景:

  • undo log:①用于事务的回滚,保证事务的原子性;②用于 MVCC,实现事务的隔离性
  • redo log:用于崩溃恢复(crash safe),保证事务的持久性
  • binlog:实现 MySQL 数据库的数据备份、主备、主主、主从,并保证数据一致性。以及用于恢复丢失的数据;

日志生成的位置:

  • undo log、redo log 是**存储引擎层(InnoDB)**生成的日志;
  • binlog 是 Server 层生成的日志;

写日志的时机:

  • undo log 记录了此次事务修改前的数据状态,记录的是更新之前的值,因此在更新操作前写到内存中,再借由 redo log 刷到磁盘;
  • redo log 记录了此次事务修改后的数据状态,记录的是更新之后的值,因此在更新操作后写到 redo log buffer中,刷盘时机受 InnoDB 后台线程、os 后台线程以及 redo log 刷盘策略等因素影响;
  • binlog 记录了此次事务修改后的数据状态,记录的是更新之后的值,因此在更新操作后写到 binlog cache 中,刷盘时机受 InnoDB 后台线程、os 后台线程以及 binlog 刷盘策略等因素影响;

MySQL 通过引入 undo log、redo log、binlog 日志、 MVCC、锁、两阶段提交等机制来保证了事务的 ACID 属性。

7.拓展:磁盘 IO 突然很高怎么办?

现在我们知道事务在提交的时候,需要将 redo log和 binlog 持久化到磁盘,那么如果出现 MySQL 磁盘 IO 很高的现象,我们可以通过控制以下参数,来 “延迟” redo log 和 binlog 刷盘的时机,从而降低磁盘 IO 的频率:

  • 将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到 redo log 文件并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入 redo log 文件意味着写入到了操作系统的文件缓存(page cache),然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。

  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。

  • 设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache 里的 binlog 就会被持久化到磁盘。

8.参考

  • 《MySQL 实战 45 讲》

  • 《MySQL 是怎样运行的:从根儿上理解 MySQL》

  • MySQL 三大日志详解

  • MySQL 日志:undo log、redo log、binlog 有什么用?


http://www.kler.cn/a/522419.html

相关文章:

  • MySQL中的读锁与写锁:概念与作用深度剖析
  • Acwing94递归实现排列型枚举
  • FreeRTOS从入门到精通 第十四章(队列集)
  • 【机器学习】自定义数据集 使用pytorch框架实现逻辑回归并保存模型,然后保存模型后再加载模型进行预测
  • docker安装emqx
  • spring中解决循环依赖的方法
  • java基础——专题一 《面向对象之前需要掌握的知识》
  • 一文大白话讲清楚webpack基本使用——18——HappyPack
  • react页面定时器调用一组多个接口,如果接口请求返回令牌失效,清除定时器不再触发这一组请求
  • 【浏览器 - Chrome调试模式,如何输出浏览器中的更多信息】
  • 如何根据壁纸主题选择合适的主色调?
  • 对海康威视工业相机进行取图
  • 产业园管理系统提升企业综合管理效率与智能化水平的成功案例分析
  • 若依路由配置教程
  • 图像处理篇---图像压缩格式编码格式
  • 3.5.3 基于横盘结构的分析体系——缠论(线段)
  • 力扣-链表-24 两两交换链表中的节点
  • 16.Word:石油化工设备技术❗【28】
  • oracle 19C RAC打补丁到19.26
  • linux 环境安装 dlib 的 gpu 版本
  • HTML(快速入门)
  • WPS数据分析000010
  • Vue.js组件开发-Vue实现上传word模版打印设置自定义样式和布局
  • 【JAVA项目】基于ssm的【宠物医院信息管理系统】
  • 【论文阅读】Equivariant Diffusion Policy
  • 1.Template Method 模式