关键字
日志、索引
这一章是专栏老师的答疑课,在这一节中,主要解决了一些关于日志和索引的疑惑。
日志相关问题
在第二篇文章中,讲到了 binlog 和 redo log,这两个日志使用了两阶段提交。这样的方法可以解决崩溃恢复的问题。在这里,就详细分析一下在 MySQL 发生异常重启的时候,是怎么保证数据完整性的。
首先看一下在第二篇文章中的图:在这里,我们就分析一下,在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象。
如果在 时刻A 发生重启,也就是 redo log 处于 prepare 阶段之后、写 binlog 之前发生了崩溃。此时,由于 binlog 还没写,redo log 没提交,所以在恢复的时候,这个事务会回滚。
如果 时刻B 发生重启,也就是在 binlog 写完,redo log 还没有 commit 时发生了崩溃。此时有两个判断规则:
1.如果 redo log 里的事务完整,即已经有了 commit,则直接提交。
2.如果 redo log 的事务只有 prepare,则需要判断 binlog 是否存在且完整。如果完整,提交事务;如果不完整,回滚。
在 时刻B,已经有了完整的 binlog,崩溃恢复的事务会被提交。
追问:MySQL 怎么知道 binlog 是否完整?
答:一个事务的 binlog 是有完整格式的,所以,MySQL 有办法验证事务 binlog 的完整性。
追问:redo log 和 binlog 是如何关联起来的?
答:它们有一个共同的字段,XID。崩溃恢复的时候,会顺序扫描 redo log:
- 如果 redo log 中既有 prepare 又有 commit ,就直接提交。
- 如果只有 prepare,而没有 commit,则需要拿着 redo log 中的 XID 去 binlog 中寻找对应事务。
追问:prepare 的 redo log + 完整的 binlog --> 重启后提交这个事务。为什么不设置为回滚?
答:在 时刻B,binlog 已经写入了,之后就会被从库(或用这个 binlog 恢复出来的库)使用。所以,在主库也要提交这个事务,以保持主库和备库的数据一致性。
追问:为什么要这样设计两阶段提交呢?为什么不先写完 redo log,再写 binlog。崩溃恢复的时候,要求两个日志都完整才可以回复。是不是也是一样的逻辑?
答:不是的,对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。
所以,只有当每个人都说“我 OK ”的时候,再一起提交。
追问:为什么不能用 binlog 既支持崩溃回复,又支持归档呢?
注:言下之意是:只保留 binlog,然后可以把提交流程改成这样:… -> “数据更新到内存” -> “写 binlog” -> “提交事务”,是不是也可以提供崩溃恢复的能力?
答:不可以。这有历史原因和实现上的原因两个方面。
历史原因:InnoDB 并不是 MySQL 的原生引擎。MySQL 的原生 MyISAM 引擎并不支持崩溃恢复。
InnoDB 作为插件加入 InnoDB 之后,才通过 redo log 提供了崩溃回复的功能。
实现上的原因:在下面的图中,没有 redo log,如果在指定的地方发生崩溃,会出现一些问题:
在发生 crash 时,binlog2 写完了,但是 事务2 还没有 commit。
重启后,事务2 会进行回滚,然后使用 binlog2 补回来;但是对于 事务1 来说,因为已经 commit,所以并不会使用 binlog1。
InnoDB 使用的是 WAL,执行事务的时候,写完内存和日志,事务就算完成了,注意,此时内存中的数据并不一定已经刷到了磁盘中。
所以,如果在图中的位置发生崩溃,事务1 可能会丢失,而且是数据页级别的丢失。所以,目前来说,binlog 还不能支持崩溃恢复。
追问:能不能只用 redo log,不用 binlog?
答:如果只从崩溃恢复的角度来讲是可以的。关掉 binlog 之后系统依然是 crash-safe 的。
但是,binlog 拥有很多其它功能,比如归档、比如在异构系统中消费 binlog 来更新自己的数据。如果关掉 binlog,这些功能都无法使用。
总之,很多系统机制都依赖 binlog,所以从生态的角度来看,binlog 必不可少。
追问:redo log 设置多大?
答:如果 redo log 设置太小,会很快被写满,然后触发强行刷 redo log,这会降低性能。
所以,如果你的磁盘足够,就不要太小气了,直接将 redo log 设置为 4 个文件、每个文件 1GB 吧。
追问:数据的最终落盘,是从 redo log 更新还是从 buffer pool 更新呢?
答:实际上,redo log 并没有记录数据页的完整数据,所以最终落盘,不可能是由 redo log 更新过去的。
- 脏页落盘和 redo log 毫无关系。
- 在崩溃恢复中,InnoDB 如果判断一个数据页可能丢失更新,就会将它读到内存中,然后用 redo log 更新内存内容。更新后,就又变成了脏页落盘的问题,这就和 redo log 无关了。
小结
这是一篇答疑文章,主要回答了日志的相关问题。实际上,专栏文章还讲到了一个比较复杂的问题,但是我并没有理解那个问题,所以在这里就没有写出。如果对之后的问题感兴趣,可以到专栏中找一找。
上期问题
上期的问题是,用一个计数表记录一个业务表的总行数,在往业务表插入数据的时候,需要给计数值加 1。
逻辑实现上是启动一个事务,执行两个语句:
- insert into 数据表;
- update 计数表,计数值加 1。
从系统并发能力的角度考虑,怎么安排这两个语句的顺序。
解答:
并发系统性能的角度考虑,应该先插入操作记录,再更新计数表。
知识点在《行锁功过:怎么减少行锁对性能的影响?》
因为更新计数表涉及到行锁的竞争,先插入再更新能最大程度地减少事务之间的锁等待,提升并发度。
本期思考
我们创建了一个简单的表 t,并插入一行,然后对这一行做修改。
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL primary key auto_increment,
`a` int(11) DEFAULT NULL
) ENGINE=InnoDB;
insert into t values(1,2);
这时候,表 t 里有唯一的一行数据 (1,2)。假设,我现在要执行:
mysql> update t set a=2 where id=1;
你会看到这样的结果:
结果显示,匹配 (rows matched) 了一行,修改 (Changed) 了 0 行。
仅从现象上看,MySQL 内部在处理这个命令的时候,可以有以下三种选择:
- 更新都是先读后写的,MySQL 读出数据,发现 a 的值本来就是 2,不更新,直接返回,执行结束;
- MySQL 调用了 InnoDB 引擎提供的“修改为 (1,2)”这个接口,但是引擎发现值与原来相同,不更新,直接返回;
- InnoDB 认真执行了“把这个值修改成 (1,2)"这个操作,该加锁的加锁,该更新的更新。
你觉得实际情况会是以上哪种呢?你可否用构造实验的方式,来证明你的结论?进一步地,可以思考一下,MySQL 为什么要选择这种策略呢?
以上就是本节内容,愿你能解决疑问。
注:本文章的主要内容来自我对极客时间app的《MySQL实战45讲》专栏的总结,我使用了大量的原文、代码和截图,如果想要了解具体内容,可以前往极客时间