MySQL-redo log 介绍

前言

我们将从缓冲池开始介绍 ,然后后面开始介绍了 redo log 的底层到底是如何记录物理日志的.

问题

  • redo log 长什么样 ?
  • redo log 刷到哪去 ?

redo log 的动机

redo log 具有以下功能:

  1. 提高事务的性能

    redo log 可以将事务对数据的修改操作缓存在内存中的 redo log buffer 中,从而减少磁盘 IO 操作的次数,提高事务的性能。如果没有 redo log,每次事务提交时都需要将修改操作写入磁盘,会导致大量的磁盘 IO 操作,从而影响事务的性能。

  2. 确保数据的一致性

    redo log 可以保证事务对数据的修改操作不会丢失。当一个事务对数据进行修改时,MySQL 会将修改操作先写入 redo log buffer 中,然后再将修改操作写入 redo log 文件中。在事务提交时,MySQL 会将 redo log 文件中的修改操作写入磁盘,从而保证数据的一致性。

  3. 支持事务的回滚

    redo log 可以支持事务的回滚。当一个事务回滚时,MySQL 会使用 redo log 中记录的修改操作来撤销该事务对数据的修改操作,并将数据恢复到修改前的状态。

  4. 支持数据恢复

    redo log 可以支持数据恢复。当 MySQL 重启时,会先恢复 redo log 中未提交的事务,然后再恢复 binlog 中的操作,从而保证数据的一致性和完整性。

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。

这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成,即redo log和undo log。

  • redo log用来保证事务的持久性

  • undo log用来帮助事务回滚及MVCC的功能。

redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

redo log buffer

redo log buffer 是一个内存缓冲区,用于暂存事务对数据的修改操作。当一个事务对数据进行修改时,MySQL 会将修改操作先写入 redo log buffer 中,然后再将修改操作写入 redo log 文件中。在事务提交时,MySQL 会将 redo log buffer 中的修改操作写入 redo log 文件中,并将事务提交的信息写入 binlog 文件中。

redo log buffer 的作用是缓解磁盘 IO 压力,将多个小的修改操作合并成一个大的修改操作,从而减少磁盘 IO 操作的次数,提高数据库的性能。如果没有 redo log buffer,每次事务提交时都需要将修改操作写入磁盘,会导致大量的磁盘 IO 操作,从而影响数据库的性能。

在 MySQL 中,redo log buffer 的大小可以通过参数 innodb_log_buffer_size 来配置,通常建议设置为 8MB 或者 16MB。如果 redo log buffer 的大小过小,可能会导致事务无法提交或者回滚,因此需要合理规划 redo log buffer 的大小。

redo 的刷盘

和大多数关系型数据库一样,InnoDB记录了对数据文件的物理更改,并保证总是日志先行,也就是所谓的WAL,即在持久化数据文件前,保证之前的redo日志已经写到磁盘。

redo log 的刷盘策略分为三种 , 通过 innodb_flush_log_at_trx_commit 进行设置 :

  • 表示事务提交时不进行写人重做日志操作,这个操作仅在master thread中完成,而
    在master thread中每1秒会进行一次重做日志文件的fsync操作。

  • 刷到操作系统 , page cache , 不做 fsync

  • 同步刷到磁盘 , 做 fsync

    不做 fsync 无疑是速度最快了, 只在内存上更新, 而不持久化 , 依赖后台线程做持久化 , 代价就是出现down 机有可能丢失一部分事务 ;

    刷到操作系统 速度次之 , 这个设置下 , 数据库down 机 , 而操作系统没有down 机 , 则不会丢失这一部分的事务 ; 假如是操作系统down机, 那么这部分事务会丢失

fsync落盘 这种肯定是最慢的, 因为有IO , 虽然慢 , 但是发生崩溃的时候不会丢失事务.

下图是一本书中的例子, 用50万条数据插入表中 , 分别测的3种配置下的时间差异 .

img

LSN

LSN(log sequence number) 用于记录日志序号,它是一个不断递增的 unsigned long long 类型整数。LSN 代表的含义 :

  • 重做日志的写入总量
  • checkpoint 的位置
  • 页的版本

LSN表示事务写入重做日志的字节的总量。例如当前重做日志的LSN为1000,有一个事务T1写人了100字节的重做日志,那么LSN就变为了1100,若又有事务T2写入了200字节的重做日志,那么LSN就变为了1300。可见LSN记录的是重做日志的总量,其单位为字节。

LSN 不仅记录在重做日志中,还存在于每个页中。在每个页的头部,有一个值FLPAGE LSN,记录了该页的LSN。在页中,LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。(这里可能怎么和页有关系 , 我们会在后面 redo 底层写了什么内容 章节介绍 )

例如,页P1的LSN为10000,而数据库启动时,InnoDB检测到写入重做日志中的LSN为13000,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到 P1 页中 . 同样的 , 对于重做日志中 LSN 小于 P1 页的 LSN , 不需要进行重做 , P1页中的LSN表示页已经被刷新到该位置。

(其实从这里也可以看到拿 redo log 恢复 , 实际恢复的是 page , 颗粒度是 page )

然后我们再来看一下 LSN 与 checkpoint
img

Log sequence number表示当前的LSN , Log flushed up to表示刷新到重做日志文件的LSN,Last checkpoint at表示刷新到磁盘的LSN。毫无疑问 ,要是请求多了(QPS 高了) , 肯定刷盘的数据跟不上当前的 LSN .

恢复

假如Mysql 运行过程中 , 崩溃了, 需要进行恢复了 ,此时的 redo log 是这样的 :

img

崩溃那一瞬间的LSN 是 13000 , 而落盘的是 10000 , 也就是说需要恢复检查的 redo-log 在 10000 到 13000 (检查bin log 是不是页落盘了,即事务是否提交了)

redo log 与回滚

当一个事务回滚时,MySQL 会使用 redo log 中记录的修改操作来撤销该事务对数据的修改操作,并将数据恢复到修改前的状态。

具体来说,当一个事务提交时,MySQL 会将事务对数据的修改操作写入 redo log 文件中,然后再将修改后的数据写入磁盘。如果事务回滚,则会使用 redo log 中记录的修改操作来撤销该事务对数据的修改操作,并将数据恢复到修改前的状态。

当一个事务回滚时,MySQL 会按照相反的顺序使用 redo log 中记录的修改操作来撤销该事务对数据的修改操作。例如,如果一个事务将某个字段的值从 1 修改为 2,那么 MySQL 会将这个修改操作记录在 redo log 中。如果该事务回滚,则 MySQL 会使用 redo log 中记录的修改操作将该字段的值恢复为 1。

redo log 可以支持事务的回滚,是因为它记录了事务对数据的修改操作,包括修改前的数据和修改后的数据。当事务回滚时,MySQL 可以使用 redo log 中记录的修改前的数据来恢复数据的原始状态,从而实现事务的回滚操作。

缓冲池

在介绍 redo log 之前我们将会先介绍缓冲池 ,方便后续知道 redo log 的动机 .
InnoDB存储引擎是基于磁盘存储的 ,并将其中的记录按照页的方式进行管理 ,这一点有点像超级系统的 page cache ,我们之前也发过这张图片 ,是一个 SQL 语句的执行过程 ,其中可以看到是否命中内存就是命中数据库的记录是否在 page 中 .

图例

那么我们可以想一下 ,假如我们需要读取更改某个页, 就想操作系统一样 ,操作系统当发生缺页的时候 ,操作系统就需要等待数据加载到 page 之后才能进行操作 ,所以 MySQL 采用 WAL (Write Ahead Log) 的方式来加快写 ,先将SQL 操作写入到 redo log ,再修改 page , 也就是说redo log 和 page 里面的数据是最新的数据 ,而磁盘里的数据是最旧 ,那么什么时候把脏数据刷新到磁盘中去呢? MySQL 回刷的时间点称之为 : Checkpoint

1297993-20211007214710930-2009572723.png

img

到了 checkpoint 就会将脏页回刷到磁盘 ,回刷的时机 :

(1) InnoDB 的 redo log 写满了,即是 write position 的位置追到了 checkpoint 的位置,这时候系统就会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间继续写。
(2) 系统内存不足,因为 MySQL 的数据都缓存在内存中,当系统的内存不足,那么就会有一部分数据会刷到磁盘中去
(3) MySQL 空闲的时候把数据进行刷盘
(4) 关闭数据库的时候,回刷数据回磁盘

而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:

  • 如果要淘汰的是一个干净页,就直接释放出来复用;
  • 但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。

从上面我们可以看到redo log 的数据结构是一个环形的 ,这样 WAL 就变成了顺序写 ,提升了性能 .

redo 底层写了什么内容

redo log block

在InnoDB存储引擎中,重做日志都是以512字节进行存储的。这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redolog block),每块的大小为512字节。

若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术(就是保证每一次写入都是原子性的技术--要不写入成功 ,要不写入失败 ,失败了有备份 ,要不怎么叫 doublewrite)。

重做日志块除了日志本身之外,还由日志块头(log block header)日志块尾(logblock tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节。故每个重做日志块实际可以存储的大小为492字节(512-12-8)。

img

head 的结构如下
img

头部四个字段分别表示 :

  • LOG_BLOCK_HDR_NO : 编码 , 用来标记该 block 这个数组中的位置, 第一位用来判断是否是 flush bit .
  • LOG_BLOCK_HDR_DATA_LEN : 占用大小
  • LOG_BLOCK_FIRST_REC_GROUP : 第一个事务开头的位置
  • LOG_BLOCK_CHECKPOINT_NO : 该 block 最后被写入时的检查点第4字节的值.

LOG_BLOCK_FIRST_REC_GROUP 有点难理解 , 下面的例子中 , LOG_BLOCK_FIRST_REC_GROUP 为 (270 + 12)

img

redo 记录格式

下面图片和描述来自参考资料 ,非原创

我自己称 redo log 为结果日志 ,因为它只记录结果 , 而不是记录过程 , 而 bin log 我自己称之为过程日志 ,相反 , 不记载结果 ,只记录过程 .

redo 记录物理日志,记录的是**“在某个数据页上做了什么修改” **

这里需要说明一下 , 由于InnoDB存储引擎的存储管理是基于页的,什么意思呢 ? InnoDB 并不管你 ,你那一行的数据是哪个地方 , 它最小的管理颗粒度是 page , 比如说修改某一行 , 对于 InnoDB 来说便是修改某一页的某个偏移量的内容.

例如下面的语句 :

update table set a = 1 where id = 1;

那么翻译成物理日志类似于这样 :

把第10表空间的第90号页面的偏移量为1024处的值更新为1

下面是大部分类型的redo log的通用结构:

1297993-20211007164148585-646595941.png

  • type:redo log的类型,目前redo log的类型很多
  • Space ID:表空间ID
  • page number:页号
  • data:一条redo log的内容

展示一下源码的数据结构的样子

struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {
    atomic_sn_t sn;                       // 目前log buffer申请的空间大小
    aligned_array_pointer<byte, OS_FILE_LOG_BLOCK_SIZE> buf;  // log buffer的内存区
    Link_buf<lsn_t> recent_written;               // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
    Link_buf<lsn_t> recent_closed;        // 解决并发插入flush_list后确认checkpoint_lsn的问题
    atomic_lsn_t write_lsn;           // write_lsn之前的数据已经写入系统的Cache, 但不保证已经Flush
    atomic_lsn_t flushed_to_disk_lsn;         // 已经被flush到磁盘的数据
    size_t buf_size;                  // log buffer缓冲区的大小
    lsn_t available_for_checkpoint_lsn;      // 在此lsn之前的所有被添加到buffer pool的flush list的log数据已经被flsuh, 下一次checkpoint可以make在这个lsn. 与last_checkpoint_lsn的区别是该lsn尚未被真正的checkpoint.
    lsn_t requested_checkpoint_lsn;     // 下次需要进行checkpoint的lsn
    atomic_lsn_t last_checkpoint_lsn;       // 目前最新的checkpoint的lsn
    uint32_t write_ahead_buf_size;      // write ahead的Buffer大小
    lsn_t current_file_lsn;         // 
    uint64_t current_file_real_offset;      //
    uint64_t current_file_end_offset;       // 当前ib_logfile文件末尾的offset
    uint64_t file_size;             // 当前ib_logfile的文件大小
}

redo log类型

先看一下 redo log 的基础类型
redo log类型主要是通过上面记录中的type体现的。比较基础的有以下几个(基础的类似于java里面的基本类型):

  • MLOG_1BYTE:type字段对应的十进制为1,表示在页面的某个偏移量处写入一个字节
  • MLOG_2BYTES:type字段对应的十进制为2,表示在页面的某个偏移量处写入两个字节
  • MLOG_4BYTES:type字段对应的十进制为4,表示在页面的某个偏移量处写入四个字节
  • MLOG_8BYTES:type字段对应的十进制为8,表示在页面的某个偏移量处写入八个字节
  • MLOG_WRITE_STRING::type字段对应的十进制为30,表示在页面的某个偏移量处写入一串数据

现在举一个例子。我们大部分情况下用的自增主键id都是int型或者是long型的,int为四个字节,long为八个字节,现在如果插入一条数据的话,这条数据实际是修改在buffer pool中的,然后通过redo log记录下当前的修改情况。那么这个时候,插入一条id(int)为9的数据的redo log应该是这样子的。

1297993-20211007164644410-1745186052.png

插入数据后 , 含义: 在90表空间,编号为10页面,偏移量为1000处,写入四个字节,具体数据为0000 0000 0000 1001
其他类型的 redo log 这里不再深入 ,这里仅做抛砖引玉 ,方便大家理解 redo log 记录的内容是什么

问题

脏页 和 redo log 的关系 ?

当客户端第一次开始查询数据的时候,数据由于不存在 buffer pool ,那么数据会从磁盘加载数据到
buffer pool ,然后返回给客户端; 当客户端第二条语句是更改语句,那么MySQL 此时会

  • 更新内存中的数据
  • 写 redo log 日志
    这样就形成了脏页,那么脏页和磁盘中的干净页是不一致的,脏页需要flush回磁盘才能达到持久化,flush 的过程必定会导致
  • 脏页变成了干净页
  • checkpoint 向前一步推进

为了管理脏页,在 Buffer Pool 的每个instance上都维持了一个flush list,flush list 上的 page 按照修改这些 page 的LSN号进行排序。

因此定期做redo checkpoint点时,选择的 LSN 总是所有 Buffer instance 的 flush list 上最老的那个page(拥有最小的LSN)。由于采用WAL的策略,每次事务提交时需要持久化 redo log 才能保证事务不丢。而延迟刷脏页则起到了合并多次修改的效果,避免频繁写数据文件造成的性能问题。

redo log 和 bin log 的区别

  • redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。

  • redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。

  • redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

其他

fsync

小结

使用 redo log 是利用WAL(先写日志)技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能。同事也可以保证事务的持久性, 保证事务持久性并不单单只有redo log,其实还有mysql的重要机制——double write,见这一篇文章

参考资料

posted @ 2021-10-07 22:34  float123  阅读(1193)  评论(0编辑  收藏  举报