在两阶段提交的不同瞬间,MySQL 如果发生异常是怎么保证数据完整性的?
这是个 update 语句的执行流程吗,怎么会调用 commit语句?
这里其实有**两个“commit”**的概念:
- 平常说的“commit语句”,是指 MySQL 语法中,用于提交一个事务的命令。一般跟 begin/start transaction 配对使用。
- 而这个图中是“commit步骤”,指的是事务提交过程中的一个小步骤,也是最后一步。当这个步骤执行完了,这个事物就提交了。
- “commit语句”执行时也包含“commit步骤”。
因为 update 语句本身自己就是个事务,所以就会有“commit步骤”。
在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象
在图中 A 时刻,写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash)。由于 binlog 没写,redo log 也没提交,所以数据库恢复之后,这个事务会回滚。
在 B 时刻,binlog 写完,但 redo log 没 commit 之前 crash,MySQL 会怎么办呢?
我们来看一下崩溃恢复规则:
- 如果 redo log 中的事务是完整的,也就是有了 commit 标识,则直接提交。
- 如果 redo log 中的事务只有完整的 prepare,则判断对应的 benlog 是否存在并完整:
- 如果是,则提交事务;
- 如果不是,则回滚事务。
此时 B 时刻就是符合 redo log 有完整的 prepare,binlog 完整,所以直接提交事务。
追问一:MySQL 怎么知道 binlog 是完整的?
回答:一个事务是有着完整格式的。
- statement 格式的 binlog,最后会有 COMMIT;
- row 格式的 binlog,最后会有一个 XID event。
另外,在 MySQL 5.6.2 版本之后,引入了 binlog-checksum
用来校验 binlog的完整性。
追问二:redo log 和 binlog 是怎么关联起来的?
回答:它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redolog:
- 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交。
- 如果只有prepare时,就拿着 XID 去 binlog 找对应的事务。
追问三:处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢 复,MySQL 为什么要这么设计?
回答:其实,这个问题还是跟我们在反证法中说到的数据与备份的一致性有关。在时刻 B,也就是 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog恢复出来的库)使用。
所以,在主库上也要提交这个事务,保证主从一致。
追问4:先redo log写完,再写 binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑?
回答:其实,两阶段提交是经典的分布式系统问题。
如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题。
对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。 两阶段提交就是为了给所有人一个机会,当每个人都说“我ok”的时候,再一起提交。
追问5:只用 binlog 来支持崩溃恢复,又能支持归档,可以吗?
回答:不可以。
历史原因中,MySQL 原生引擎 MyISAM,在设计之初就没有支持崩溃恢复。
InnoDB 在加入 MySQL 之前就已经提供了崩溃恢复和事务支持。在接入 MySQL 之后,就直接用 InnoDB 的 redo log 来提供崩溃恢复服务了。
实际上的话,只用 binlog 还是不能支持崩溃恢复,binlog 没有能力恢复“数据页”。
在上图中,binlog2 写完了还没提交,系统发生 crash。重启后,事务 2 会回滚,但是对于事务 1 来说,系统认为提交完成了,不会在应用一次事务 1。
而且 InnoDB 使用的是 WAL(Write-ahead logging,预写日志),执行事务时,写完内存和日志,事务就算完成了。如果之后崩溃,要依赖于日志来恢复数据页。
也就是说,在图中这个位置崩溃,事务 1 也可能丢失的,而且是数据页级的丢失。此时,binlog 里面没有数据页的更新细节,是补不回来的。
追问六:那只用 redo log,不用 binlog 呢?
回答:如果只从崩溃的角度讲,可以关闭 binlog,这样就没有两阶段提交,但系统还是 crash-safe。
但 binlog 有 redo log 替代不了的功能:
- 归档。redo log 是循环写,这样历史日志没法保留,只靠 redo log 无法归档。
- MySQL 系统依赖于 binlog,被用在很多地方。其中,MySQL 高可用的基础就是 binlog 复制。
追问七:redo log 一般设置多大?
回答:如果是常见的几 TB 磁盘的话,直接设置为 4 个文件、每个文件 1GB。
追问八:正常运行中的实例,数据写入后的最终落盘,是从 redo log 更新过来的还是从 buffer pool 更新过来的呢?
回答:实际上 redolog 并没有记录数据页的完整信息,所以它没有能力自己去更新磁盘数据。
- 如果系统正常运行的话,就是把脏页直接写入磁盘。这个过程,甚至与 redo log 没有一点关系。
- 在崩溃恢复场景中,InnoDB 判断一个页可能在 crash 时丢失了数据,就会将数据页读入内存,让后让 redo log 更新内存内容。更新完后,内存中的变成了脏页,就和第一种情况一样了。
追问九:redo log buffer 是什么?是先修改内存,还是先写redo log文件?
在一个事务的更新中,日志是要写多次的。比如:
begin;
insert into t1 ...
insert into t2 ...
commit;
这两条插入语句生成的日志要保存起来,但是又不能在还没 commit 的时候就直接写到 redo log 文件里。
所以,redo log buffer就是一块内存,用来先存 redo 日志的。在执行第一个 insert 的时候,数据的内存被修改了,redo log buffer 也写入的日志。
真正把日志写入到 redo log 文件中,是上面 commit 语句执行的时候。