MySql--Redo Log 和 Undo Log

摘要

redo logundo log

  • 前文关于事务的介绍中,我们提到过,mysql的Innodb引擎在事务的具体实现机制上采用的是WAL(Write-ahead logging,预写 式日志)机制来实现的,具体来说就是 redo logundo log

  • 在机器掉电重启之后Mysql系统需要知道之前的操作是成功了,还是只有部分成功或者是失败了(为了恢复状态)。如果使用了WAL,那么在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是撤销操作。

redo log

redo log 称为重做日志,每当有操作时,在数据变更之前(此时事务并没有提交,所以可能需要回滚,回滚是通过undo log记录的)将操作写入 redo log, 这样当发生掉电之类的情况时系统可以在重启后继续操作。
redo log 用来在系统 Crash 重启之类的情况时修复数据,以此来保证事务的持久性
binlog 记录完成后会在 redo log中加入一个commit标记,表示redo与binlog数据一致

redo log的作用
我们知道,InnoDB的增删改查都是在Buffer Pool完成的,如果我们只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。那么如何保证这个持久性呢?

我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。
所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,因为这样做效率太低(还是那个原则,把随机写改为顺序写),我们只需要把修改了哪些东西记录一下就好,
比方说某个事务将系统表空间中的第 100号 页面中偏移量为 1000 处的那个字节的值 1 改成 2 ,我们只需要记录一下:
将第 0 号表空间的 100 号页面的偏移量为 1000 处的值更新为 2。
这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。

理论上,redo log的容量足够恢复因掉电等原因导致Buffer Pool没有及时刷入磁盘的内容。否则redo log因容量不够而被覆盖,而此时Buffer Pool尚未刷入磁盘,就不能保证事务的持久性了

这样做与如下好处:
1、redo 日志占用的空间非常小,存储表空间ID页号偏移量 以及 需要更新的值 所需的存储空间是很小的。
2、redo 日志是顺序写入磁盘的,在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁盘的,也就是使用 顺序IO

redo日志格式

InnoDB 们针对事务对数据库的不同修改场景定义了多种类型的redo日志,但是绝大部分类型的 redo 日志都有下边这种通用的结构:
type:该条 redo 日志的类型,redo 日志设计大约有 53 种不同的类型日志。
space ID:表空间 ID。
page number:页号。
data:该条 redo 日志的具体内容。

  • type类型有很多种,根据type的不同,redo日志格式中还会增加其它结构

    • 比如简单的类型中会包含修改位置的偏移量或者修改数据的长度,等等,如修改单条记录
    • 复杂一些的类型,如表中包含多少个索引,那么执行一条insert语句就会修改非常多的页面(聚簇索引和二级索引对应的B+树),针对某一棵 B+树来说,既可能更新叶子节点页面,也可能更新非叶子节点页面,也可能创建新的页面(在该记录插入的叶子节点的剩余空间比较少,不足以存放该记录时,会进行页面的分裂,在非叶子节点页面中添加目录项记录)。在语句执行过程中,INSERT 语句对所有页面的修改都得保存到 redo 日志中去。这个实现起来是非常麻烦的,我们这里不做详细说明。
  • redo日志格式类型非常多,如果不是为了写一个解析 redo 日志的工具或者自己开发一套 redo 日志系统的话,那就不需要去研究 InnoDB 中的 redo 日志具体格式。只要记住:redo 日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。

redo log buffer

写入 redo 日志时也不能直接直接写到磁盘上,
实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,翻译成中文就是 redo 日志缓冲区,
我们也可以简称为 log buffer。

我们可以通过启动参数 innodb_log_buffer_size 来指定 log buffer 的大小,该启动参数的默认值为 16MB。

1
2
3
4
5
6
mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
  • redo log buffer 何时刷入磁盘

    • 1、log buffer 空间不足时

      log buffer 的大小是有限的(通过系统变量innodb_log_buffer_size 指定),如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。InnoDB 认为如果当前写入 log buffer 的 redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。

    • 2、事务提交时

      我们前边说过之所以使用 redo 日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo 日志刷新到磁盘。

    • 3、后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。

    • 4、正常关闭服务器时等等。

    • 5、MySQL8.0.30之前的版本,redo日志的大小由两个变量控制

      • 1)innodb_log_files_in_group:REDO 日志磁盘上的文件个数,默认为2。文件名称:ib_logfile0,ib_logfile1
      • 2)innodb_log_file_size:REDO 日志磁盘上单个文件的大小,默认为48M。
      • 3)当前的日志大小为单个48M,两个组,也就是一共96M。
    • 6、MySQL8.0.30引入了一个新特性:动态调整redo日志的大小,默认redo日志文件位于datadir下的一个目录#innodb_redo下,redo log 文件存放的位置由参数 innodb_log_group_home_dir 控制,这里要注意,该目录下必须创建#innodb_redo目录,否则会启动失败

      • redo动态日志总大小通过参数innodb_redo_log_capacity设置,默认100M,最大重做日志容量为 128GB,InnoDB维护了32个redo日志文件,每个文件的大小是1/32 * innodb_redo_log_capacity
      • 该版本以及之后的版本,参数 innodb_log_file_sizeinnodb_log_files_in_group 已经被废弃
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      mysql> show variables like 'innodb_redo_log_capacity';
      +--------------------------+-----------+
      | Variable_name | Value |
      +--------------------------+-----------+
      | innodb_redo_log_capacity | 104857600 |
      +--------------------------+-----------+

      # 设置为2G
      mysql> set persist innodb_redo_log_capacity=2*1024*1024*1024;

      # 查看redo log是否resize成功
      mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_resize_status';
      +-------------------------------+-------+
      | Variable_name | Value |
      +-------------------------------+-------+
      | Innodb_redo_log_resize_status | OK |
      +-------------------------------+-------+

      # 查看redo log resize后的值
      mysql> SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_capacity_resized';
      +----------------------------------+-----------+
      | Variable_name | Value |
      +----------------------------------+-----------+
      | Innodb_redo_log_capacity_resized | 2147483648 |
      +----------------------------------+-----------+
      • 有两种类型的redo日志文件:

        • ordinary:指被使用的redo日志文件,命名规则是:#ib_redoN,这里的N是日志文件号
        • spare:指等待被使用的redo日志文件,命名规则是:#ib_redoN_tmp,这里的N是日志文件号
      • 可以通过查询 performance_schema.innodb_redo_log_files 来查看有关活动重做日志文件的信息

      1
      2
      3
      4
      5
      6
      mysql> SELECT FILE_ID, START_LSN, END_LSN, SIZE_IN_BYTES, IS_FULL, CONSUMER_LEVEL  FROM performance_schema.innodb_redo_log_files;
      +---------+-----------+-----------+---------------+---------+----------------+
      | FILE_ID | START_LSN | END_LSN | SIZE_IN_BYTES | IS_FULL | CONSUMER_LEVEL |
      +---------+-----------+-----------+---------------+---------+----------------+
      | 21 | 348829696 | 352104448 | 3276800 | 0 | 0 |
      +---------+-----------+-----------+---------------+---------+----------------+
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      [root@ip-10-250-0-214 #innodb_redo]# ll
      总用量 102400
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo21
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo22_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo23_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo24_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo25_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo26_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo27_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo28_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo29_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo30_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo31_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo32_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo33_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo34_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo35_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo36_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo37_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo38_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo39_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo40_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo41_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo42_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo43_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo44_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo45_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo46_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo47_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo48_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo49_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo50_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo51_tmp
      -rw-r----- 1 mysql mysql 3276800 10月 13 07:50 #ib_redo52_tmp
  • innodb_flush_log_at_trx_commit 该变量有 3 个可选的值:
    0:当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步redo日志,这个任务是交给后台线程做的。这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。
    1:当该系统变量值为 1 时,表示在事务提交时需要将 redo 日志同步到磁盘,可以保证事务的持久性。1 也是 innodb_flush_log_at_trx_commit 的默认值。
    2:当该系统变量值为 2 时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了

  • 崩溃后的恢复为什么不用 binlog?

    1、这两者使用方式不一样
    binlog 会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要用于人工恢复数据,而 redo log 对于我们是不可见的,它是 InnoDB 用于保证crash-safe 能力的,也就是在事务提交后 MySQL 崩溃的话,可以保证事务的持久性,即事务提交后其更改是永久性的。
    一句话概括:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用,用于保证在数据库崩溃时的事务持久性。
    2、redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
    3、redo log 是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2这的 c 字段加 1 ” ;
    4、redo log 是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。
    5、最重要的是,当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经入表(写入磁盘),哪些数据还没有。

    比如,binlog 记录了两条日志:

    给 ID=2 这一行的 c 字段加 1
    给 ID=2 这一行的 c 字段加 1
    在记录 1 入表后,记录 2 未入表时,数据库 crash。重启后,只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 ID=2 这行数据来说,都不对。

    但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。

undo log

undo log 称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。
undo log 用来保证事务的原子性

为了实现事务的原子性,InnoDB 存储引擎在实际进行增、删、改一条记录时,都需要先把对应的 undo 日志记下来。一般每对一条记录做一次改动,就对应着一条 undo 日志,但在某些更新记录的操作中,也可能会对应着 2 条 undo 日志。

一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的 undo 日志,这些 undo 日志会被从 0 开始编号,也就是说根据生成的顺序分别被称为第 0 号 undo 日志、第 1 号 undo 日志、…、第 n 号 undo日志等,这个编号也被称之为 undo NO。

我们前边说明表空间的时候说过,表空间其实是由许许多多的页面构成的,页面默认大小为 16KB。这些页面有不同的类型,其中有一种称之为FIL_PAGE_UNDO_LOG 类型的页面是专门用来存储 undo 日志的。也就是说 Undo page 跟储存的数据和索引的页等是类似的。
FIL_PAGE_UNDO_LOG 页面可以从系统表空间中分配,也可以从一种专门存放undo日志的表空间,也就是所谓的 undo tablespace 中分配。

  • undo log 与更新操作对应的日志记录条数

    • insert: 记录1条日志,主要是把这条记录的主键信息记上,回滚这个插入操作时把这条记录删除就好了
    • delete: 记录1条日志,undo log日志类型为 TRX_UNDO_DEL_MARK_REC ,将记录的 delete_mask 值被设置为 1,回滚这个删除操作时改回0就好了
    • update: 分两种情况,更新记录主键与不更新记录主键
      • 更新记录主键: 在对该记录进行 delete mark 操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo 日志;之后插入新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo 日志,也就是说每对一条记录的主键值做改动时,会记录 2 条 undo 日志。
      • 不更新记录主键: 记录1条日志,类型为 TRX_UNDO_UPD_EXIST_REC

Redo 日志和 Undo 日志的关系

数据库崩溃重启后,需要先从 redo log 中把未落盘的脏页数据恢复回来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要把未提交的事务进行回滚操作。由于回滚操作需要 undo log 日志支持,undo log 日志的完整性和可靠性需要 redo log 日志来保证,所以数据库崩溃需要先做 redo log 数据恢复,然后做 undo log 回滚。

事务进行过程中,每次 sql 语句执行,都会记录 undo log 和 redo log,然后更新数据形成脏页。事务执行 COMMIT 操作时,会将本事务相关的所有 redo log 进行落盘,只有所有的 redo log 落盘成功,才算 COMMIT 成功。然后内存中的 undo log 和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用 redo log 恢复数据。

知识面拓展
Commit LoggingShadow Paging
事务的日志类型的实现除了 WAL(Write-ahead logging,预写式日志)外,还有Commit Logging(提交日志),这种方式只有在日志记录全部都安全落盘,数据库在日志中看到代表事务成功提交的“提交记录”(Commit Record)后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完成持久化。两者的区别是,WAL 允许在事务提交之前,提前写入变动数据,而 Commit Logging 则不行。阿里的 OceanBase 则是使用的 Commit Logging 来实现事务。

实现事务的原子性和持久性除日志外,还有另外一种称为Shadow Paging”(有中文资料翻译为“影子分页”)的事务实现机制,常用的轻量级数据库 SQLite Version 3 采用的事务机制就是 Shadow Paging。
Shadow Paging 的大体思路是对数据的变动会写到硬盘的数据中,但并不是直接就地修改原先的数据,而是先将数据复制一份副本,保留原数据,修改副本数据。在事务过程中,被修改的数据会同时存在两份,一份是修改前的数据,一份是修改后的数据,这也是“影子”(Shadow)这个名字的由来。当事务成功提交,所有数据的修改都成功持久化之后,最后一步是去修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本,最后的“修改指针”这个操作将被 认为是原子操作,现代磁盘的写操作可以认为在硬件上保证了不会出现“改了半个值”的现象。所以 Shadow Paging 也可以保证原子性和持久性。Shadow Paging 实现事务要比 Commit Logging 更加简单,但涉及隔离性与并发锁时,Shadow Paging 实现的事务并发能力就相对有限,因此在高性能的数据库中应用不多。