通过上篇文章,我们知道MySQL是采用两段提交策略来保证事务的原子性的,redo log刷盘的时机是在事务提交的commit阶段采取刷盘的,在此之前,redo log都存在于redo log buffer这块指定的内存区域中。
这里我们首先要明确两个概念和两个参数:
write:刷盘 fsync:持久化到磁盘 write(刷盘)指的是MySQL从buffer pool中将内容写到系统的page cache中,并没有持久化到系统磁盘上。这个速度其实是很快的。 fsync指的是从系统的cache中将数据持久化到系统磁盘上。这个速度可以认为比较慢,而且也是IOPS升高的真正原因。
innodb_flush_logs_at_trx_commit(redo log) 取值0:每次提交事务都只把redo log留在redo log buffer中 取值1:每次提交事务都将redo log 持久化到磁盘上,也就是write+fsync 取值2:每次都把redo log写到系统的page cache中,也就是只write,不fsync sync_binlog(binlog) 取值0:每次提交都将binlog 从binlog cache中 write到磁盘上,而不fsync到磁盘 取值1:每次提交事务都将binlog fsync到磁盘上 取值N:每次提交事务都将binlog write到磁盘上,累计N个事务之后,执行fsync
在某些特定场景下,redo log会在commit这个动作到来之前进行刷盘操作,例如下面的两种情况会让没有提交的事务的redo log写入磁盘:
1、redo log buffer占用的空间即将达到buffer pool的一般的时候,后台线程会主动刷盘,这个时候,由于事务没有提交,所以仅仅是将redo log buffer中的内容通过write的方法写入到系统的cache中,没有进行fsync的持久化动作。
2、并行提交事务的时候,会顺带将上一个事务的部分redo log从redo log buffer中fsync到磁盘上,例如下面的例子:
假设redo log buffer中的内容如下(假设每个事务的redo log有4部分):
redo log B1
redo log A1
redo log B2
此时,事务B发生了commit操作,而设置的innodb_flush_logs_at_trx_commit的值是1,那么会触发事务B的redo log持久化到磁盘。此时事务A的一部分redo log,也就是redo log A1会被顺带着持久化fsync到磁盘中。
这里还需要说明一点,因为MySQL的innodb存储引擎时需要支持崩溃恢复的,依赖prepare阶段的redo log ,所以,如果innodb_flush_logs_at_trx_commit的值是1,MySQL会在redo log的prepare阶段就进行一次持久化redo log的fsync操作。这个fsync的存在,再加上每秒一次的后台刷盘操作,innodb会认为redo log在commit的时候,就不需要fsync了,只write到文件系统的page cache就够了。
所以,真正的两阶段提交,应该是下图所示:
之所以redo log的write和fsync没有连接在一起,其实是考虑到了组提交的功能,分开来进行这两个步骤,在并发的场景下,可以让这一组一次性提交的redo log更多一点,从而一次性fsync更多的组员。
那么两段提交过程中失败会发生什么结果呢?MySQL又是怎样处理的呢?
首先我们先放一下两段提交的图
接下来,我们就一起分析一下在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象。
如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。
如果在图中在时刻 B,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?
我们先来看一下崩溃恢复时的判断规则。
如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交; 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整: a. 如果是,则提交事务; b. 否则,回滚事务。
这里,时刻 B 发生 crash 对应的就是 2(a) 的情况,崩溃恢复过程中事务会被提交。
那么问题又来了,MySQL是如何判断binlog是不是完整的呢?
我们都知道binlog有三种格式statement、row、mix。其中mix是前两种方式的组合,一个事务的binlog是有完整的格式的,
statement 格式的 binlog,最后会有 COMMIT; row 格式的 binlog,最后会有一个 XID event。
另外,在 MySQL 5.6 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。所以,MySQL 还是有办法验证事务 binlog 的完整性的。
而且redolog和binlog有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:如果碰到既有 prepare、又有 commit 的 redo log,就直接提交; 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。这样在两段提交的前提下就能完全保证事务的特性了。