MySQL日志篇

日志的类型

undo log:回滚日志,是 Innodb 储存引擎层生成的日志,实现了事务的原子性,主要用于事务的回滚和MVCC
redo log:重做日志,是 Innoddb 储存引擎层生成的日志,实现了事务的持久性,主要用于掉电等故障恢复
binlog:归档日志,是 Server 层生成的日志,主要用于数据备份和主从复制

为什么需要 undo log

作用

事务回滚

这里就要提及到 事务 的原子性,无论是单条 sql 语句还是多条 sql 语句,都会需要开启事务,当开启事务后,就有可能会发生某条 sql 语句报错

当发生报错后,事务中之前修改的数据就需要去进行修复回滚,除了这种报错的情况,还有就是在执行事务的过程中 MySQL 发生了崩溃,当再次重启的时候,也需要去修复回滚数据

当回滚的时候,就能够使用到 undo log 了,它保证了事务的原子性

在这里插入图片描述
由图可以看出大概的流程,实际上的步骤就是记录 回滚所需要的信息 到 undo log 当中
比如:

  • 插入记录,记录这条记录的主键,当回滚的时候,根据主键值进行删除
    • 所以说当事务回滚后,如果主键是递增的,那么当前递增值就丢失了
    • 插入 158 ,恢复删除158,再插入的时候就是从159 开始了
  • 更新记录,记录更新前的值,当需要回滚的时候,更新到旧值
  • 删除记录,记录删除前的整条记录,当需要回滚的时候,重新插入当前记录

这样就很清晰明了了,也就是要在操作之前记录上一步要变化的值,以防止事务失败而导致的不一致性
并且由于不同的操作,所需要的东西实际上是不相同的,所以记录的 undo log 格式自然也就不相同了

MVCC

MVCC 为多版本并发控制,初衷是因为读事务和写事务之间的冲突,避免写操作等待读操作,几乎所有的主流数据库都采用了MVCC的方式。而undo log在这其中的作用就是记录历史的版本数据,来满足MVCC读取旧版本数据的需求
每条记录都会产生属于自己的版本链,在经历多次更新 / 删除操作时候,就会通过 roll_pointer 去传承一个链表,并且记录是哪个事务修改的

  • 因此会有两个隐藏列 回滚指针 roll_pointer 和 事务idtrx_id

版本链的结构如下:
在这里插入图片描述
所以当事务要去进行快照读(Read View)的时候,就会去比对其中的 trx_id判断当前事务是否是可以被读取的数据,从而读取到历史数据

  • 每个事务都有一个唯一的id,并且事务id是递增的

为什么需要 redo log

首先需要提及到 MySQL 的持久化机制,是需要经历一个缓冲池 BufferPoll 的,也就是在命令执行完成后,并不是直接写到文件当中,而是先写到了缓冲池,再由缓冲池统一写到文件当中。那为什么需要缓冲池这一个机制呢?

为什么需要 BufferPoll

作用

简单来说,就是提升查询和插入的效率。
思考一个问题,IO一般来说是很慢的,如果每次都从文件中直接读取,那么读取的这个时间耗时就会很长,会很影响体验。除了读取,还有写入,我每写一条记录进行一次IO,那么 100 条记录,就进行了100次IO,是很耗时的,优化的方案应该是 100 条记录统一的写入到文件当中,把IO过程归到一起。
所以说 MySQL 设计了缓冲池这一机制
在这里插入图片描述
所以说 MySQL 的 BufferPoll 机制储存了一部分的记录

  • 当要查询的时候,先走缓冲池,是否有相应数据,如果没有,才需要进行IO加载,并缓冲到缓冲池中
  • 修改的时候,同样也是先走缓冲池,数据库的读取实际上是按页的方式读取的,并不是一行行的进行读取,所以会去寻找当前行所在的页,修改该页的数据,然后将其标记为脏页(被修改的页面),这里的脏页的目的就是为了减少磁盘IO的次数,尽量统一的将修改的数据进行写入。

结构

缓冲池需要缓冲什么?
由于 InnoDB 会把存储的数据划分为若干个[页],以页作为磁盘和内存交互的基本单位,一个页的大小默认为 16KB
所以缓冲池也应当缓冲数据[页]
当 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的 16KB 的大小划分出一个个的页,Buffer Pool 的页就叫做缓存页,此时这些缓存也都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中

在这里插入图片描述
图中的数据页 + 索引页就是缓存的页面
其他内容不属于日志的内容,不详细深入,但是了解一下 Undo 页

Undo 页是指事务更新记录前要记录相应的 undo log

这里的Undo 页并不是说 undo log 也要像数据页一样走缓存写入,undo log 是直接进行的磁盘写入,而缓存中的 Undo 页则是为了更快的查询要进行回滚的数据,当事务失败需要进行回滚的时候,就可以直接走缓存,回滚当前事务相关的页面。

redo log

什么是 redo log

redo log 是物理日志,记录了某个数据页做了什么修改,对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。

了解了 BufferPool 后,就可以知道他是作为缓存作用,加快查询和插入的效率,但是 BufferPool 是基于内存的,是不可靠的,因为 MySQL 也是一个程序,当 MySQL 崩溃后,内存中的脏页数据还没来得及写入磁盘就丢失了。
所以需要防止这种情况的发生,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后把记录写到 redo log 里面,这个时候更新就算完成

WLA(Write-Ahead Logging) 技术:指的是 MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间在写到磁盘上

在这里插入图片描述

所以说在将数据写到缓冲池的时候,需要有一个日志redo log来保证数据不会丢失,当数据完整写入缓冲池后,日志redo log也应当是完整的,如果不是就需要回滚。
如果日志完整了就保证了缓冲池里的数据不会丢失,但此时的事务还没有提交,数据也还没有真正写到磁盘当中,只有当数据写到磁盘后,两者保持一致性,才是真正的完成了事务

上面有说到一个 Undo 页,是写入 undo log 的缓存,需要去记录对应的 redo log

因为开启事务后,InnoDB 层更新记录前,是需要记录 undo log 的,因为需要预防回滚的情况会发生。
所以需要保证 undo log 的写入,因此需要记录到 redo log

redo log 和 undo log 有什么区别?

两个都是属于 InnoDB 存储引擎的日志,它们的区别在于:

  • redo log 记录了此次事务 完成后 的数据状态,记录的是更新之后的值
  • undo log 记录了此次事务 开始前 的数据状态,记录的是更新之前的值

事务提交之前发生了崩溃,会通过 undo log 进行事务回滚,同时 redo log 也需要进行回滚

  • redo log 需要和 binlog 保持一致性,下面会说到 bin log

当事务提交后,但是缓存中的脏页还没有刷盘到磁盘中,系统崩溃了,重启后,就会根据 redo log 的 commit 记录进行数据恢复

PS:要注意一个事情, redo log 是针对 完整页 的恢复,对于残缺页是没有办法恢复的

残缺页和完整页是怎么发生的?

残缺页导致的原因:当在刷盘的过程中的时候,遇到了宕机,可能会导致一个数据页 16KB,但只写入了15KB,这也就导致了残缺页的发生,因此会引入一个新的机制double write二次写

double write

会创建一个新的结构,double write buffer ,这个区占用 2M 内存空间,同时有一个相对应的持久化文件 double write文件,当 BufferPool 中的脏页要进行刷盘的时候,就需要进行二次写

  • 第一步是写入 double write buffer 中
  • 第二部进行 IO 写入到 double write文件中,
  • 当文件写入完毕,再进行数据页的刷盘

当有了这个结构后,再遇到残缺页的情况

在系统崩溃后,重启遇到残缺页,通过 double write 文件进行数据页恢复,这样就可以适配 redo log 的完整数据恢复了,且不会遇到残缺页的情况

redo log 和 数据都要写到磁盘,为何多此一举?

首先看两种的写法

  • 数据写到磁盘:随机写,需要找到当前页所在的位置,然后在写入磁盘,页的位置不固定
  • redo log 写入磁盘:顺序写,只需要在尾部添加记录

所以说 redo log 写入的效率会更高,因此,磁盘改为在缓存中保存后再写,减少了IO的次数
到这里,可以看到 redo log 有两大优势

  • 保证了 ACID 中的 D 持久性,让数据库有 崩溃恢复 的功能,crash-safe,重启后之前提交的记录都不会丢失
  • 将随机读变为了顺序读,减缓了IO次数,提升 MySQL 写入磁盘的能力

redo log 是直接写入磁盘么?

并不是,和数据页的情况一样,即使它是顺序写,但也承受不住IO次数过多,耗时也是会变长,所以MySQL也选择引入了一个缓冲池 redo log buffer ,每当产生一条 redo log 的时候,都写入缓冲池的时候,根据策略持久化到磁盘的 redo log 当中

redo log buffer 默认为 16MB,可以通过innodb_log_Buffer_size 参数动态的调整大小,根据需求的不同调节

redo log 刷盘策略

由于有一个缓冲池 redo log buffer,所以需要有个刷盘的时机

  • MySQL 关闭时
  • redo log buffer 的写入量达到总容量的一半
  • 后台线程每隔 1s 刷盘一次
  • 事务提交后,写入刷盘(这个可以通过参数配置选择不同的方案)
    • 除了本项,上三项都是固定的策略

参数详情

InnoDB 一共提供了三种策略,由参数 innodb_flush_log_at_trx_commit 参数控制(默认值为 1)

  • 参数为 0 :事务提交后,不操作
  • 参数为 1 :事务提交后,将 redo log buffer 中的 redo log 写入 redo log 文件,并刷盘(fsync)
  • 参数为 2 :事务提交后,将 redo log buffer 中的 redo log 写入 redo log 文件,但不进行刷盘
    • 这里并不意味着成功写入了磁盘,没有进行一个刷盘(fsync)操作,有可能会在文件系统的缓冲中(Page Cache),还没有完全写入文件

参数 1 对于事务是安全的,但 0 和 1 对于事务可能是不安全的

参数 0 : 由于没有操作,等待每一秒的刷盘操作,如果MySQL崩溃了,会导致上一秒的所有事务数据丢失,安全性无法保证
参数 2 : 相比较于 0 安全些,如果 MySQL 崩溃,并不会影响到数据的写入,只有当操作系统崩溃的时候,那会丢失上一秒的所有事务的数据

这三个参数有什么意义?它的应用场景在哪里?

  • 数据安全性: 参数 1 > 参数 2 > 参数 0
  • 写入效率:参数 0 > 参数 2 > 参数1

看到这两个性质,就能够明白了它的场景,数据安全性和写入效率是不可得兼的,根据两者的取舍决定使用哪个参数

  • 比如需要高安全性,数据不能够丢失,那么只能够采取参数1了,但同时效率也会下降
  • 再如可以忍受一秒数据的丢失,那可以采取参数0,使用写入效率最高的参数
  • 或者取中,采用风险性和效率都是中间的参数2,只需要保证服务器不会重启,断电,即使数据库崩溃,也不会丢失数据,性能也比 1 更好

redo log 文件组

redo log 大小并不是无限的,而是固定的,目的是为了节省空间,毕竟有些旧的事务日志也就不必要储存了,可以清理掉。
默认情况下,InnoDB 有一个重做文件组,其中有两个 redo log 文件,,这两个 redo 日志的文件名叫 :ib_logfile0ib_logfile1
两个文件各自的大小分别是 1GB,总共就可以记录 2GB 的操作
为了复用,所以采取一种循环写的方式,循环队列
在这里插入图片描述

  • write pos 是写入的指针,向左移动
  • check point 是对应数据脏页还未刷盘的开始指针,可以说是队头,也是向左移动
  • write pos ~ check point 之间的数据,就是已经完成了脏页1的刷盘操作,不需要redolog保证脏页会丢失,也就已经是无用的数据
  • check point ~ write pos 之间的数据,是对应的脏页还没有完成刷盘的操作,需要去保证脏页不会丢失的一个情况

那有没有可能整个redo log 都是还未刷盘的脏页对应日志

是有可能出现这个情况的,因此,在 redo log 日志满了之后,需要主动触发脏页刷新机制,也就是主动让缓存池中对应的脏页写到磁盘,做持久化的操作

为什么需要 binlog

redo log 和 undo log 都是 innodb 引擎独有的,但 binlog 是大家都有的

binlog 文件时记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT…

这么看来的话,binlog 和 redo log 的记录十分相像,但并不相同,最明细的就是目的,binlog 是保留了全量的日志,用于主从同步的目的,redo log 是保存一段时间的日志,主要是实现 crash-safe 能力,有一定的容灾能力

redo log 和 binlog 有什么不同

主要分为 4 个方向的不同

  1. 适用对象不同
    • binlog 是 MySQL 的 Server 层实现的日志,所有的存储引擎都可以使用
    • redo log 是 InnoDB 存储引擎实现的日志
  2. 文件格式不同
    • binlog 有 3 中格式类型
      • STATEMENT :每条修改的SQL记录都记录到 binlog,记录了逻辑操作,相当于逻辑记录
        • 缺点:对于动态函数的问题,无法保证主从复制的从库执行结果是否一致
      • ROW:记录行数据最终被修改成了什么结果,可以说是物理日志,详细记录了每条记录的值被修改成了什么值
        • 缺点:会造成文件过大,因为每次修改都要写入,当一条 update 更新了几十条记录的时候,STATEMENT 格式可能只记录一条SQL,而 ROW 则需要记录几十上百条数据变动
      • MIXEDSTATEMENTROW 混合使用,会根据不同的情况使用 ROWSTATEMENT
  3. 写入方式不同
    • binlog 是追加写,记录的是全量的数据,当一个文件写满后,就创建新的文件继续写,不会去覆盖旧日志
    • redo log 是 循环写,空间大小是固定的,主要是对脏页保证的作用
  4. 用途不同
    • binglog 用于 备份恢复、主从复制
    • redo log 用于 服务器断电、宕机等故障恢复

bin log 刷盘策略

事务执行过程中,先把日志写到 binlog cache (Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中
MySQL 给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个内存限制,就要被写到磁盘当中

在这里插入图片描述
每个线程写入的都是同一个 binlog

  • 图中的 write 就是把日志写道 binlog 文件,但是由于还没有进行刷盘操作,仍会在 操作系统的 page cache 当中,所以 write 的速度是比较快的,因为不涉及磁盘IO
  • 图中的 fsync,才是将数据持久化到磁盘的操作,会涉及到磁盘IO,比较缓慢

MySQL 提供了 sync_binlog 控制同步的时间,和 redo log 的有些许相似

  • 参数为 0 :提交事务后只 write ,不 fsync,由操作系统决定刷盘
  • 参数为 1 :提交事务后 write 并立刻 fsync
  • 参数为 N:提交事务后 write,但累计 N 个事务后才 fsync

完整的事务执行过程

优化器分析出最小的执行计划后,执行器按照计划执行
UPDATE student SET name = 'hello world' where id = 1;
以一条更新SQL为例书写流程:

  1. 执行器调用存储引擎的接口,通过主键索引树获取 id = 1 这一行记录
    • 如果该行记录对应的页缓存 BufferPool 中,可以直接获取
    • 如果该行记录对应的页不在 BufferPool 中,需要将数据页从磁盘中读取到 BufferPool,然后返回
  2. 执行器得到记录后,会查看当前记录和更新后的记录是否相同
    • 相同:跳过后续流程,不需要变更
    • 不相同:把更新前 和 更新后的数据作为参数传递给 InnoDB 层,让 InnoDB 执行更新记录操作
  3. 开启事务,更新记录前,先记录 undo log,在写入磁盘undo log文件的同时,也要写到 Buffer Pool 中的 Undo 页(方便后续读取),同时undo log 文件的修改需要记录对应的 redo log(这个过程是防止undo log中的历史记录丢失)到 redo log buffer 当中
  4. 开始更新记录,将修改的行对应的页,更新到 Buffer Pool 当中(标记为脏页),将记录写入 redo log中。由于采用 WAL 技术,不会立刻将脏页刷盘到磁盘当中,而是先写完日志。
  5. 记录更新完毕,需要记录对应的 binlog,此时开始写到 binlog cache
  6. 当 binlog cache 写入完毕,就进入到提交阶段
  7. 提交阶段又分为两阶段提交

两阶段提交

原理

上面说到 redo log 和 binlog 要保持一致性,为什么?

redo log是为主库的持久化提供一个 crash-safe 功能的,主要是保证数据不会丢失
binlog 则是为从库提供数据同步功能的
由于这两个特性,如果他们不一致,会造成主从数据不一致的问题,下面举例说明:

  • 当 redo log 写入完成,发生断电重启,binlog 没有写入,就会导致主库成功恢复记录,而从库复制过去的数据记录缺少了记录,发生主从不一致
  • 当 binlog 写入完成,发生断电重启,redo log 没有写入,就会导致主库没有成功恢复记录,而从库因为 binlog 多出来了记录,发生主从不一致

所以,要抛弃这种半成功的状态,需要保证 redo log 和 binlog 的一致性
就提出了两阶段提交这种方案

在 MySQL 的 InnoDB 存储引擎中,为了维持两个日志的一致性,使用了 内部XA事务,由 binlog 作为协调者,存储引擎是参与者

两阶段提交将单个事务拆分了2个阶段

  • 准备 prepare:将 XID(内部XA事务的ID)写入 redo log,同时将 redo log 对应的事务状态设置为 prepare,将 redo log 刷盘
  • 提交 commit: 将 XID 写到 binlog,然后将 binlog 刷新到磁盘,然后调用提交事务接口,将 redo log 状态设置为 commit(也需要刷盘到 redo log)
    在这里插入图片描述

故障时刻分析

在这里插入图片描述
当发生故障(断电重启,宕机恢复)等情况,重启MySQL后,恢复的阶段首先是去读取 redo log,寻找相应的XID(内部XA事务)

  • A时刻:此时 redo log 和 binlog 都还没有进行刷盘,也就是 redo log 中找不到对应的 XID 事务,同样 binlog 中也不存在,所以相当于这个事务是丢失了的,可以抛弃,一致性也相同
    • 由于WAL,日志肯定是要比数据先写入磁盘当中,可以看缓冲池脏页的刷新策略,理论上日志肯定在脏页之前刷盘
      • redo log 日志满了的时候,主动触发脏页刷新
      • 缓冲池空间不足,主动触发脏页刷新(日志肯定早早写入了,因为淘汰策略,已经准备淘汰的脏页,相应的日志肯定早已经生成了)
      • MySQL 认为空闲的时候,后台线程定期将适量脏页刷新到磁盘(既然都空闲了,redo log 也应该写完了)
      • MySQL 正常关闭时,刷盘脏页,redo log 同样也刷盘,并且每隔 1 s 刷盘一次
  • B时刻:此时 redo log 写入到了一半的内容,就会出现 redo log 中能查询到 XID,而 binlog 中没有。这个时候也是需要回滚事务,因为这相当于说这个事务失败了,需要删除这段 redo log 日志
    • 理论上 redo log 只会丢失 1s 的数据,redo log 并不是完全依赖提交事务后才进行持久化的,前面有说到后台线程会每隔 1s 对 redo log buffer 进行一次刷盘
  • C时刻:此时 redo log 写入完毕,但是没有 binlog对应,查询不到 binlog 对应的 XID,所以需要进行事务回滚
  • D时刻:此时 redo log 写入完毕,而 bin log 只写入了一半,在这里我查不到相应的资料,我倾向于两个方案
    • 第一个 XID 是在完全刷盘后才会进行写入的,这样比对XID就可以找出这种情况
    • 第二种 XID 在开头写入了,两者都有 XID 的情况下,遍历进行比较数据是否一致,但由于结构不相同,应该不是这种。
  • E时刻:此时 redo log 和 binlog 都写入完成,XID 都能够找到,所以可以修复数据,将 redo log 置为 commit,完成事务

如果事务没有提交,redo log 有可能会被刷新到磁盘么?

这个看前面的 redo log 刷盘策略,有一个固定的刷盘策略,每隔 1s 后台线程就会将 redo log buffer 中的 redo log 持久化到磁盘当中,所以是有可能的

弊端

两阶段提交虽然保证了两个日志的数据一致性,但是有两个方面的影响:

  • 磁盘 I/O次数高:每个事务都需要进行两次 fsync (刷盘)
  • 锁竞争激烈:两阶段提交虽然保证了单事务一致,但是在多事务的情况下需要保证原子性才能够一致,因此需要加锁
    • 加锁的时机:prepare 阶段获取锁,commit阶段结束,释放锁,性能不佳

组提交

MySQL 引入 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数

引入组提交后, prepare 阶段无变化,只针对于 commit 阶段,将 commit 阶段分为三个过程:

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

每一个阶段都有一个队列,每个阶段都有进行保护,因此保证了事务的写入顺序,第一个进入队列的事务会成为 leader,领导所在的队列的所有事务

binlog 有组提交,那么 redo log 有组提交么?

在 MySQL 5.6 之前没有 redo log 组提交,在 5.7 版本加入 redo log 组提交

  • 5.6 的时候,每个事务各自在 prepare 阶段对 redo log 进行刷盘
  • 5.7 的时候,prepare 阶段不再让事务各自执行,将 prepare 阶段移动到 flush 阶段之中,在 sync 阶段之前完成写入,通过延迟刷盘时机,进行组写入

资料来源

MySQL 崩溃恢复过程分析
double write(二次写)
庖丁解InnoDB之UNDO LOG
MySQL 日志 | 小林 coding

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值