所谓两阶段提交,其实就是把 redo log 的写入拆分成了两个步骤:prepare 和 commit。
首先,存储引擎将执行更新好的新数据存到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare
状态。
然后告知执行器执行完成了,随时可以提交事务
然后执行器生成这个操作的 bin log,并把 bin log 写入磁盘
最后执行器调用存储引擎的提交事务接口,存储引擎把刚刚写入的 redo log 状态改成提交(commit
)状态,更新完成
如果数据库在写入 redo log(prepare) 阶段之后、写入 binlog 之前,发生了崩溃:
此时 redo log 里面的事务处于 prepare 状态,binlog 还没写,之后从库进行同步的时候,无法执行这个操作,但是实际上主库已经完成了这个操作,所以为了主备一致,MySQL 崩溃时会在主库上回滚这个事务
而如果数据库在写入 binlog 之后,redo log 状态修改为 commit 前发生崩溃,此时 redo log 里面的事务仍然是 prepare 状态,binlog 存在并完整,这样之后就会被从库同步过去,但是实际上主库并没有完成这个操作,所以为了主备一致,即使在这个时刻数据库崩溃了,主库上事务仍然会被正常提交。
如何解决 bin log 与 redo log 的一致性问题
redo log和bin log 的区别
1)适用对象不同:
- bin log 是 MySQL 的 Server 层实现的,所有引擎都可以使用
- 而 redo log 是 InnoDB 引擎特有的
2)写入内容不同:
- bin log 是逻辑日志,记录的是这个语句的原始逻辑,比如 “给 id = 1 这一行的 age 字段加 1”
- redo log 是物理日志,记录的是 “在某个数据页上做了什么修改”
3)写入方式不同:
- bin log 是可以追加写入的。“追加写” 是指 bin log 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志
- redo log 是循环写的,空间固定会被用完
可以看到,redo log 和 bin log 的一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷入磁盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。
而 bin log 是追加日志,保存的是全量的日志。这就会导致一个问题,那就是没有标志能让 InnoDB 从 bin log 中判断哪些数据已经刷入磁盘了,哪些数据还没有。
举个例子,bin log 记录了两条日志:
记录 1:给 id = 1 这一行的 age 字段加 1 记录 2:给 id = 1 这一行的 age 字段加 1
假设在记录 1 刷盘后,记录 2 未刷盘时,数据库崩溃。重启后,只通过 bin log 数据库是无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 id = 1 这行数据来说,都是不对的。
但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中被抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。
这就是为什么说 redo log 具有崩溃恢复的能力,而 bin log 不具备。
可以看到,所谓两阶段提交,其实就是把 redo log 的写入拆分成了两个步骤:prepare 和 commit。
根据两阶段提交,崩溃恢复时的判断规则是这样的:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交
- 如果 redo log 里面的事务处于 prepare 状态,则判断对应的事务 binlog 是否存在并完整
- a. 如果 binlog 存在并完整,则提交事务;
- b. 否则,回滚事务。
而如果数据库在写入 binlog 之后,redo log 状态修改为 commit 前发生崩溃,此时 redo log 里面的事务仍然是 prepare 状态,binlog 存在并完整(对应 2a),所以即使在这个时刻数据库崩溃了,事务仍然会被正常提交。
Why?
因为 binlog 已经写入成功了,这样之后就会被从库同步过去,但是实际上主库并没有完成这个操作,所以为了主备一致,在主库上需要提交这个事务。
所以,其实可以看出来,处于 prepare 阶段的 redo log 加上完整的 bin log,就能保证数据库的崩溃恢复了。
可能有同学就会问了,MySQL 咋知道 bin log 是不是完整的?
简单来说,一个事务的 binlog 是有完整格式的(这个我们在后面的文章中会详细解释):
- statement 格式的 bin log,最后会有 COMMIT
- row 格式的 bin log,最后会有 XID event
binlog 持久化
这里引入了一个新的概念:binlog cache
从名字就能看出来,binlog cache 其实就是一片内存区域,充当缓存的作用。
每个线程都有自己 binlog cache 区域,在事务运行的过程中,MySQL 会先把日志写到 binlog cache 中,等到事务真正提交的时候,再统一把 binlog cache 中的数据写到 binlog 文件中。(binlog cache 有很多个,binlog 文件只有一个!)
事实上,这个从 binlog cache 写到 binlog 文件中的操作,并不就是落盘操作了,这里仅仅是把 binlog 写到了文件系统的 page cache 上(这一步对应下图中的 write
操作)。
所以,最后需要把 page cache 中的数据同步到磁盘上,才算真正完成了 binlog 的持久化(这一步对应下图中的 fsync
操作)。一般情况下,我们认为 fsync
才占磁盘的 IOPS (Input/Output Operations Per Second)
不同于 binlog cache 每个线程都有一个,redolog buffer 只有那么一个。
分析下 redolog 可能存在的三种状态(binlog 也差不多):
- 事务执行过程中,存在 MySQL 的进程内存中的 redolog buffer 中
- 事务提交,执行 write 操作存在文件系统的 page cache 中,但是没有执行 fsync 操作持久化到磁盘
- 事务提交,执行 fsync 操作持久化到磁盘
至于为什么说事务还没提交的时候,redolog 也有可能被持久化到磁盘呢?
InnoDB 有一个后台线程,每隔 1 秒轮询一次,具体的操作是这样的:调用 write 将 redolog buffer 中的日志写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。而在事务执行中间过程的 redolog 都是直接写在 redolog buffer 中的,也就是说,一个没有提交的事务的 redolog,也是有可能会被后台线程一起持久化到磁盘的。
另外,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redolog 写盘:
-
innodb_flush_log_at_trx_commit 设置是 1,这样并行的某个事务提交的时候,就会顺带将这个事务的 redolog buffer 持久化到磁盘
举个例子,假设事务 A 执行到一半,已经写了一些 redolog 到 redolog buffer 中,这时候有另外一个事务 B 提交,按照 innodb_flush_log_at_trx_commit = 1 的逻辑,事务 B 要把 redolog buffer 里的日志全部持久化到磁盘,这时候,就会带上事务 A 在 redolog buffer 里的日志一起持久化到磁盘redo log buffer 占用的空间达到 redolo buffer 大小(由参数
innodb_log_buffer_size
控制,默认是 8MB)一半的时候,后台线程会主动写盘。不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘redo log buffer 占用的空间达到 redolo buffer 大小(由参数innodb_log_buffer_size
控制,默认是 8MB)一半的时候,后台线程会主动写盘。不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘 -
redo log buffer 占用的空间达到 redolo buffer 大小(由参数
innodb_log_buffer_size
控制,默认是 8MB)一半的时候,后台线程会主动写盘。不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘
-