二、通关redo log和binlog

1、redo log(重做日志)

redolog是物理日志,记录的是“某个数据页上发生了什么修改”,采用的是循环读写,所以当MySQL实例挂了或者宕机了,可以用于恢复数据,从而保证数据的完整性和持久性。redo log属于引擎层,InnoDB独有的。

MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。

更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。

注意:redo并没有记录数据页的完整数据,所以它是没有能力自己去更新磁盘页。

InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
在这里插入图片描述
write pos** 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。

redo log的写入机制

事务在执行过程中,生成的redo log是要先写到redo log buffer的。

然后就有同学问了,redo log buffer里面的内容,是不是每次生成后都要直接持久化到磁盘呢?

答案是,不需要。

如果事务执行期间MySQL发生异常重启,那这部分日志就丢了。由于事务并没有提交,所以这时日志丢了也不会有损失。

那么,另外一个问题是,事务还没提交的时候,redo log buffer中的部分日志有没有可能被持久化到磁盘呢?

答案是,确实会有。

这个问题,要从redo log可能存在的三种状态说起。这三种状态,对应的就是图2 中的三个颜色块。
在这里插入图片描述
这三种状态分别是:

  1. 存在redo log buffer中,物理上是在MySQL进程内存中,就是图中的红色部分;
  2. 写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面,也就是图中的黄色部分;
  3. 持久化到磁盘,对应的是hard disk,也就是图中的绿色部分。

日志写到redo log buffer是很快的,wirte到page cache也差不多,但是持久化到磁盘的速度就慢多了。

为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:

  1. 设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;
  2. 设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;
  3. 设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。

InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。

注意,事务执行中间过程的redo log也是直接写在redo log buffer中的,这些redo log也会被后台线程一起持久化到磁盘。也就是说,一个没有提交的事务的redo log,也是可能已经持久化到磁盘的。

实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的redo log写入到磁盘中。

  1. **一种是,redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动写盘。**注意,由于这个事务并没有提交,所以这个写盘动作只是write,而没有调用fsync,也就是只留在了文件系统的page cache。
  2. **另一种是,并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。**假设一个事务A执行到一半,已经写了一些redo log到buffer中,这时候有另外一个线程的事务B提交,如果innodb_flush_log_at_trx_commit设置的是1,那么按照这个参数的逻辑,事务B要把redo log buffer里的日志全部持久化到磁盘。这时候,就会带上事务A在redo log buffer里的日志一起持久化到磁盘。

如果把innodb_flush_log_at_trx_commit设置成1,那么redo log在prepare阶段就要持久化一次,因为有一个崩溃恢复逻辑是要依赖于prepare 的redo log,再加上binlog来恢复的。

通常我们说MySQL的“双1”配置,指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog。

2、binlog(归档日志)

binlog是逻辑日志,记录的内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。

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

binlog的写入机制

binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

系统给binlog cache分配了一片内存,每个线程一个,参数 binlog_cache_size用于控制单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache。**每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
在这里插入图片描述
可以看到,每个线程有自己binlog cache,但是共用同一份binlog文件。

  • 图中的write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。
  • 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS。

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

  1. sync_binlog=0的时候,表示每次提交事务都只write,不fsync;
  2. sync_binlog=1的时候,表示每次提交事务都会执行fsync;
  3. **sync_binlog=N(N>1)**的时候,表示每次提交事务都write,但累积N个事务后才fsync。

因此,在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。在实际的业务场景中,考虑到丢失日志量的可控性,一般不建议将这个参数设成0,比较常见的是将其设置为100~1000中的某个数值。

但是,将sync_binlog设置为N,对应的风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志。

3、两阶段提交

首先要明确两阶段提交的过程发生在事务的提交阶段(commit阶段),两阶段提交只是事务commit阶段中的一个步骤,也是最后一个步骤,完成则意味着事务提交结束。

两阶段提交的目的是为了让redolog和binlog这两份日志之间的逻辑一致
在这里插入图片描述
我们来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

为什么需要两阶段提交呢?**

  1. 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。
    但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
    然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
  2. 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

进一步分析在两阶段提交的不同时刻,MySQL异常重启会出现什么现象?

  • 如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。

  • 在时刻 B,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?

    我们先来看一下崩溃恢复时的判断规则。

    1. 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;

    2. 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:

      a. 如果是,则提交事务;

      b. 否则,回滚事务

追问1:MySQL怎么知道binlog是完整的?

回答:一个事务的 binlog 是有完整格式的:

  • statement 格式的 binlog,最后会有 COMMIT;
  • row 格式的 binlog,最后会有一个 XID event。

另外,在 MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。

追问 2:redo log 和 binlog 是怎么关联起来的?

回答:它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

这里稍微补充说明下undolog

4、undo log

每一个事务对数据的修改会被记录到undo log,当执行过程中出现错误或者需要执行回滚操作的话,MySQL可以利用undo log将数据恢复到事务开始之前的状态。

undo log属于逻辑日志,记录的是SQL语句,比如说事务执行一DELETE语句,那undo log就会记录一条相对应的INSERT语句。

《MySQL实战45讲》

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值