type
status
date
slug
summary
tags
category
password

1、概述

InnoDB 下的更新语句的流程会涉及到 undo log(回滚日志)、redo log(重做日志) 、binlog (归档日志)这三种日志:
  • undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC
  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复。
  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制。

2、undo log

2.1 undo log是什么

我们在执行执行一条“增删改”语句的时候,虽然没有输入 begin 开启事务和 commit 提交事务,但是 MySQL 会隐式开启事务来执行“增删改”语句的,执行完就自动提交事务的,这样就保证了执行完“增删改”语句后,我们可以及时在数据库表看到“增删改”的结果了。
那么,考虑一个问题。一个事务在执行过程中,在还没有提交事务之前,如果 MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?
如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。
实现这一机制就是 undo log(回滚日志),它保证了事务的 ACID 中的原子性(Atomicity)
undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。如下图:
notion image
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里。不同的操作,需要记录的内容也是不同的,所以不同类型的操作(修改、删除、新增)产生的 undo log 的格式也是不同的。
undo log 通常包含以下主要部分:
  1. Header 信息
      • 事务ID (TRX_ID):创建该undo记录的事务标识符,通过 trx_id 可以知道该记录是被哪个事务修改的。
      • 回滚指针 (ROLL_PTR):指向更早版本的undo记录,通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链。
      • 日志序列号 (LSN):日志的唯一序列号
      • 操作类型:INSERT/UPDATE/DELETE等
      • 日志长度
  1. 数据部分
      • 对于 INSERT 操作:记录插入的主键值,这样之后回滚时只需要把这个主键值对应的记录删掉
        • 对于 DELETE 操作:记录被删除行的完整内容,这样之后回滚时再把由这些内容组成的记录插入到表中。
          • 对于 UPDATE 操作:记录被修改前的旧值,这样之后回滚时再把这些列更新为旧值
        undo log 的两大作用:
        • 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
        • 实现 MVCC(多版本并发控制)。通过 ReadView + undo log 实现 MVCC(多版本并发控制)。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
        基于 ReadView + undo log 实现 MVCC
        MVCC 是数据库中常用的并发控制机制,用于控制并发事务访问同一个记录时的行为。他的实现原理是:
        1. 写操作创建新版本,而不是直接修改现有数据。
        1. 为每行数据维护多个历史版本,形成版本链。
        1. 读操作根据隔离级别的可见性,查看某个时间点的数据快照版本。
        通过 MVCC 读写都避免加锁操作,读操作不会被写操作阻塞,写操作也不会被读操作阻塞,减少了传统锁机制带来的性能瓶颈。
        不同数据库实现 MVCC 的方式有所不同,InnoDB 通过通过 ReadView + undo log 链实现 MVCC首先明确 MVCC 只在「读提交」和「可重复读」两个隔离级别下生效。读未提交和串行化不需要 MVCC 的原因是:
        • 读未提交:其他事务可以看到当前行的最新值,那就直接读取当前行上的最新值,不需要维护多个版本。
        • 串行:通过锁机制保证事务串行执行,所以也不存在事务并发读写问题。
        MVCC 的核心要素:
        • undo log 版本链。每条记录包含多个版本,通过隐藏字段 roll_pointer 指针链接形成版本链。如下所示:
          • notion image
        • Read View:事务在某一时间点看到的数据库的一个一致性快照,它决定了该事务能看到哪些版本的数据。简单说,Read View 定义了事务的"可见性规则"。一个典型的 Read View 包含以下关键信息:
            1. m_ids:创建 Read View 时活跃(未提交)的事务 ID 列表
            1. min_trx_id:m_ids 中的最小事务 ID
            1. max_trx_id:创建 Read View 时系统应该分配给下一个事务的 ID 值
            1. creator_trx_id:创建该 Read View 的事务 ID
        对于「读提交」和「可重复读」隔离级别的事务来说,它们的区别在于创建 Read View 的时机不同:
        • 「读提交」隔离级别是在事务内的每个 select 都会生成一个新的 Read View。也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
        • 「可重复读」隔离级别是启动事务时生成一个 Read View。然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
        快照读与当前读:
        • 当前读:读取最新数据,可能需要加锁
        • 快照读:读取历史版本数据
        总结:「读提交」和「可重复读」两个隔离级别在快照读的时候通过「事务的 Read View 」和「记录中的两个隐藏列(trx_id 和 roll_pointer)」的比对,如果不满足可见行,就会顺着 undo log 版本链里找到满足其可见性的记录。

        2.2 undo log的实现

        undo log 存在于系统表空间中的一个特殊段 Rollback Segment,链表中的页面都是从这个回滚段里边申请的。undo log 默认是存在共享表空间文件 ibdata1 文件中的。每个表空间支持 1 ~ 128 个 Rollback Segment。默认配置下,2 个 undo 表空间总共有 256 个Rollback Segment。分配回滚段的逻辑是按照 undo 表空间、回滚段轮流着来。
        notion image
        • 每个 Rollback Segment 对应个一个 Rollback Segment Header,一个 Rollback Segment Header 页面中包含1024个 undo slot。
        • 每个 undo slot 存放了 undo 链表头部的 undo 页的页号。
        • 多个 undo 页组成一个 undo 链表。链表有两个种类,一个称之为 insert undo 链表 ,另一个称之为 update undo 链表。一条链表只能有一类undo页面。链表中的第一个 Undo页面称为 first undo page ,其余的 Undo页面称为 normal undo page 。
        • 一个事务中最多可能会有4个 undo 页面链表。因为InnoDB规定对普通表和临时表修改产生的 undo log 要分开存储,每种都包括 insert undo 链表和 update undo 链表。
        notion image
        上面提到 undo log 有 insert undo log 和 update undo log 两种类型,两者的区别是:
        • Insert Undo Log:一般由 INSERT 语句产生,或者在 UPDATE 更新主键的时候也会产生。因为事务隔离性,insert undo log 对其他事务不可见,所以 insert undo log 可以在事务提交后直接删除。
        • Update Undo Log:一般由 DELETE、UPDATE 语句产生。该类型的 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除,而是放入 undo log 链表,未来通过 purge 线程来进行判断并删除。
        notion image
        undo log 的重用机制
        如果有多个并发事务执行,为了提高 undo log 的写入效率,不同事务执行过程中产生的 undo log 会被写入到不同的 undo 页面链表中。但其实大部分事务都是一些短事务,产生的 undo log 很少,这些 undo log 只会占用一个页少量的存储空间,这样就会很浪费。于是 InnoDB 设计在事务提交后,在某些情况下可以重用这个事务的 undo 页面链表。
        • 对于TRX_UNDO_INSERT类型的 insert undo 页面链表,这些 undo log 在事务提交之后就没用了,可以被清除掉。所以在某个事务提交后,重用这个链表时,可以直接覆盖掉之前的 undo log。
        • 对于TRX_UNDO_UPDATE类型的 update undo 页面链表,这些 undo log 在事务提交后,不能立即删除掉,因为要用于MVCC。所以重用这个链表时,只能在后面追加 undo log,也就是一个页中可能写入多组 undo log。
        undo log 执行过程:
        1. 开启一个读写事务,或者一个只读事务转换为读写事务时,InnoDB 会为事务分配一个 Rollback Segment。
        1. 检查此 Rollback Segment 的 cached 链表,寻找是否有缓存起来的undo slot(其对应的undo链表可重用),若有则直接分配,若没有则找一个新的,也是循环分配。
        1. 找到可用的 undo slot 后,若为缓存的,则说明是重用undo 链表,已经分配了 Undo Log Segment(包含一个undo链表),若不是缓存的,则需要重新分配一个 Undo Log Segment 然后申请一个页面作为 undo 链表头,即 first undo page。
        1. 然后事务就可以把 undo 日志写入到上边申请的 Undo 页面链表了。在向 Undo 页面 中写入 undo 日志时的方式是十分简单暴力的,就是写完一条紧接着写另一条,各条 undo 日志 之间是亲密无间的。写完一个 Undo页面后,再从段里申请一个新页面,然后把这个页面插入到 Undo 页面 链表中,继续往这个新申请的页面中写。

        3、redo log

        3.1 redo log是什么

        redo log 是一种物理日志,记录的是数据库中物理页面的变化。当数据库发生修改时,会先将这些修改记录到redo log中,然后再应用到实际的数据文件中。redo log 用来保证数据库的持久性(Durability),实现数据库的崩溃恢复能力。
        redo log 的格式如下:
        1. 日志头(Header):包含元数据信息
            • 日志序列号(LSN)
            • 时间戳
            • 校验和
            • 日志文件标识
        1. 日志记录(Log Records):实际记录数据变更的部分,简单来说记录了某个数据页做了什么修改,而不是记录完整的事务语句。每个记录都有特定的格式和类型。比如物理日志记录记录的 表空间号+数据页号+偏移量+修改的长度+具体的值。每当执行一个事务就会产生一条或者多条物理日志,记录的是 对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新
        1. 日志尾(Trailer):可能包含校验信息
        一个典型的物理 redo log 记录可能包含以下字段:
        其中:
        • LSN:日志序列号,唯一标识该记录
        • Type:记录类型(插入、更新、删除等)
        • Space ID 和 Page No:标识被修改的页面
        • Offset和Length:页面内修改的位置和长度
        • Before Image:修改前的数据(用于undo)
        • After Image:修改后的数据(用于redo)
        redo log 结合了 WAL (Write-Ahead Logging)技术。WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。redo log 的工作过程:
        1. 当更新一条记录时,InnoDB 就会先更新内存(Buffer Pool)的数据页,同时标记为脏页。
        1. 将对这个页的修改以 redo log 的形式记录下来,写入 redo log buffer。
        1. 事务提交时,将 redo log buffer 的内容持久化到磁盘,事务就算提交完成了。
        1. 后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里。
        1. 系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,MySQL 重启后可以根据 redo log 的内容将所有数据恢复到最新的状态。
        notion image
        redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?
        redo log 采用追加写入的方式,顺序写入磁盘。而页数据写入是随机写,需要先找到写入位置,然后才写到磁盘。
        磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小,可以提升语句的执行性能。

        3.2 redo log的实现

        默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 :ib_logfile0ib_logfile1
        notion image
        在重做日志组中,每个 redo log File 的大小是固定且一致的,假设每个 redo log File 设置的上限是 1 GB,那么总共就可以记录 2GB 的操作。
        重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。
        所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。
        notion image
        我们知道 redo log 是为了防止 Buffer Pool 中的脏页丢失而设计的,那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。
        redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:
        notion image
        图中的:
        • 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(内存中的缓冲区),然后在特定时机触发将 buffer 中的日志写入磁盘文件。
        notion image
        redo log 触发刷盘的时机有:
        • 事务提交:事务提交时确保对应的 redo log 已持久化到磁盘。这个策略由 innodb_flush_log_at_trx_commit 参数控制。有三个值:
          • 1:默认值,最安全的设置,每次事务提交时都会将缓存在 redo log buffer 里的 redo log 持久化到磁盘。确保即使系统崩溃也不会丢失任何已提交的事务,但是性能较差,需要频繁的磁盘 I/O。
          • 0:事务提交时不会主动触发写入操作,还是将 redo log 留在 redo log buffer 中,后台线程每秒把 redo log buffer 的内容刷新到磁盘。如果 MySQL 服务器崩溃,最多丢失 1 秒的事务数据。性能最好,但安全性最差。
          • 2:事务提交时,缓存在 redo log buffer 里的 redo log 写到 redo log 文件(存储在操作系统的 Page Cache 缓存),后台线程每秒把 redo log 文件的内容刷新到磁盘。如果 MySQL 服务器崩溃,不会丢失数据,但是如果操作系统崩溃,最多丢失 1s 的事务数据
        • 缓冲区满时:当 redo log buffer 达到一定大小时触发刷盘。redo log buffer 默认大小 16 MB,可以通过 innodb_log_Buffer_size 参数调整大小。
        • 定时刷盘:InnoDB 的后台线程定期(如每秒)将缓冲区内容刷到磁盘
        • 检查点触发:检查点机制会触发 redo log 的刷盘
        innodb_flush_log_at_trx_commit 参数的三个参数值的刷盘时机如下图所示:
        notion image
        innodb_flush_log_at_trx_commit 为 0 和 2 的时候,什么时候才将 redo log 写入磁盘?
        InnoDB 的后台线程每隔 1 秒:
        • 针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;
        • 针对参数 2 :调用 fsync(),将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失
        加入了后台现线程后,innodb_flush_log_at_trx_commit 的刷盘时机如下图:
        notion image
        这三个参数的数据安全性和写入性能的比较如下:
        • 数据安全性:1 > 2 > 0
        • 写入性能:0 > 2 > 1
        应用场景:
        • 需要最高数据安全性(如金融系统),使用默认值 1
        • 可以容忍少量数据丢失,追求高性能(如日志系统、分析系统):考虑使用 0 或 2
        • 平衡安全性和性能:使用 2

        4、binlog

        Binlog 是 MySQL 的一种二进制日志,用来记录数据库变化的 SQL 语句(包括 DML 和 DDL,但不包括查询语句)或者行变更记录
        Binlog 主要应用于以下场景:
        • 主从复制:从服务器通过读取主服务器的 Binlog 实现数据同步
        • 数据恢复:通过 Binlog 可以实现时间点恢复(Point-in-Time Recovery)
        • 审计:记录所有对数据库的修改操作
        Binlog 是开发接触最多的一种数据库日志,这里特地使用独立的篇章介绍,参考

        5、常见问题

        undo log、redo log 和 binlog 的区别

        • undo log:记录数据修改前的状态,实现事务的原子性和 MVCC(多版本并发控制)。事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务。
        • redo log:记录数据修改后的状态,实现事务的持久性和崩溃恢复。事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务。
        • binlog:记录所有修改数据的 SQL 语句或行变更,实现主从复制和崩溃恢复。数据库发生崩溃后,重启后会通过 binlog 恢复数据。
        特性
        Undo Log
        Redo Log
        Binlog
        用途
        事务回滚/MVCC
        崩溃恢复
        主从复制/数据恢复
        层级
        InnoDB引擎层
        InnoDB引擎层
        MySQL Server层
        内容
        修改前的数据
        物理修改记录
        逻辑变更记录
        持久性
        生命周期
        事务结束后可能清理
        循环写入
        按配置策略清理
        必需性
        事务必需
        事务必需
        可选(但复制需要)
        redo log 和 binlog 恢复数据有什么区别?
        • redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。所以只能用于恢复最近写入的事务。
        • binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

        undo log、redo log、binlog 的生成过程

        例如执行 SQL UPDATE t_user SET name = 'xiaolin' WHERE id = 1 ,过程如下:
        1. 开启事务
        1. 写入 undo log。生成 undo log 记录修改前的数据,并写入 undo log buffer。
        1. 更新内存页。更新内存的数据页,把 name 字段更新为 xiaolin,同时标记为脏页。
        1. 记录 redo log。成 redo log 记录修改后的数据,并写入 redo log buffer。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
        1. 提交事务(两阶段提交)。
          1. prepare 阶段:将 redo log 写入磁盘。此时 redo log 处于"prepare"状态,事务还没有真正提交。
          2. 记录 binlog:将事务的 SQL 语句或行变更写入 binlog,binlog 写入磁盘。这一步完成后,事务实际上已经持久化到磁盘。
          3. commit 阶段:将 commit 标记写入到 redo log,将 redo log 的状态从"prepare"改为"commit"状态,事务正式完成提交。
        至此,一条更新语句执行完成。

        日志持久化导致磁盘 I/O 高的解决方案

        现在我们知道事务在提交的时候,需要将 binlog 和 redo log 持久化到磁盘,那么如果出现 MySQL 磁盘 I/O 很高的现象,我们可以通过控制以下参数,来 “延迟” binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:
        • 设置组提交的两个参数: binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache 里的 binlog 就会被持久化到磁盘。
        • sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。
        • innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存,然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。
        Mysql存储引擎篇:InnoDB缓冲池Buffer PoolMysql存储引擎篇:InnoDB数据文件存储(表空间、数据页、行记录)
        Loading...