【MySQL】一文彻底搞懂 Redo-log 为什么要两阶段提交?
文章目录
- 【MySQL】一文彻底搞懂 Redo-log 为什么要两阶段提交?
- 两阶段提交的过程是怎样的?
- 为什么需要两阶段提交?
- 两阶段提交有什么问题?
- 事务提交的方式——组提交
两阶段提交的过程是怎样的?
详细大家应该听说过MySQL
事务两阶段提交方案,啥叫做事务两阶段提交呢?实则是指Redo-log
分两次写入,如下:
从图中可看出,事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:
- prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
- commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;
为什么需要两阶段提交?
其实想要弄明白这个问题,要结合
bin-log
日志一起来聊。
如果只写一次的话,那到底先写bin-log
还是redo-log
呢?
- 先写
bin-log
,再写redo-log
:当事务提交后,先写bin-log
成功,结果在写redo-log
时断电宕机了,再重启后由于redo-log
中没有该事务的日志记录,因此不会恢复该事务提交的数据。但要注意,主从架构中同步数据是使用bin-log
来实现的,而宕机前bin-log
写入成功了,就代表这个事务提交的数据会被同步到从机,也就意味着从机会比主机多出一条数据。 - 先写
redo-log
,再写bin-log
:当事务提交后,先写redo-log
成功,但在写bin-log
时宕机了,主节点重启后,会根据redo-log
恢复数据,但从机依旧是依赖bin-log
来同步数据的,因此从机无法将这个事务提交的数据同步过去,毕竟bin-log
中没有撒,最终从机会比主机少一条数据。
经过上述分析后可得知:如果redo-log
只写一次,那不管谁先写,都有可能造成主从同步数据时的不一致问题出现,为了解决该问题,redo-log
就被设计成了两阶段提交模式,设置成两阶段提交后,整个执行过程有三处崩溃点:
redo-log(prepare)
:在写入准备状态的redo
记录时宕机,事务还未提交,不会影响一致性。bin-log
:在写bin
记录时崩溃,重启后会根据redo
记录中的事务ID
,回滚前面已写入的数据。redo-log(commit)
:在bin-log
写入成功后,写redo(commit)
记录时崩溃,因为bin-log
中已经写入成功了,所以从机也可以同步数据,因此重启时直接再次提交事务,写入一条redo(commit)
记录即可。
通过这种两阶段提交的方案,就能够确保redo-log、bin-log
两者的日志数据是相同的,bin-log
中有的主机再恢复,如果bin-log
没有则直接回滚主机上写入的数据,确保整个数据库系统的数据一致性。
OK~,最后再简单补充一点:为什么
bin-log
又被叫做二进制日志呢?因为记录日志时,MySQL
写入的是二进制数据,而并非字符数据,也就意味着直接用cat/vim
这类工具是无法打开的,必须要通过MySQL
提供的mysqlbinlog
工具解析查看。
两阶段提交有什么问题?
两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响:
- 磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
- 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。
为什么两阶段提交的磁盘 I/O 次数会很高?
binlog 和 redo log 在内存中都对应的缓存空间,binlog 会缓存在 binlog cache,redo log 会缓存在 redo log buffer,它们持久化到磁盘的时机分别由下面这两个参数控制。一般我们为了避免日志丢失的风险,会将这两个参数设置为 1:
- 当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁盘;
- 当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘;
可以看到,如果 sync_binlog 和 当 innodb_flush_log_at_trx_commit 都设置为 1,那么在每个事务提交过程中, 都会至少调用 2 次刷盘操作,一次是 redo log 刷盘,一次是 binlog 落盘,所以这会成为性能瓶颈。
为什么锁竞争激烈?
在早期的 MySQL 版本中,通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。
通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳。
事务提交的方式——组提交
MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数,如果说 10 个事务依次排队刷盘的时间成本是 10,那么将这 10 个事务一次性一起刷盘的时间成本则近似于 1。
引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);
- sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
- commit 阶段:各个事务按顺序做 InnoDB commit 操作;
上面的每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
对每个阶段引入了队列后,锁就只针对每个队列进行保护,不再锁住提交事务的整个过程,可以看的出来,锁粒度减小了,这样就使得多个阶段可以并发执行,从而提升效率。
有 binlog 组提交,那有 redo log 组提交吗?
这个要看 MySQL 版本,MySQL 5.6 没有 redo log 组提交,MySQL 5.7 有 redo log 组提交。
在 MySQL 5.6 的组提交逻辑中,每个事务各自执行 prepare 阶段,也就是各自将 redo log 刷盘,这样就没办法对 redo log 进行组提交。
所以在 MySQL 5.7 版本中,做了个改进,在 prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了 flush 阶段。
这个优化是将 redo log 的刷盘延迟到了 flush 阶段之中,sync 阶段之前。通过延迟写 redo log 的方式,为 redolog 做了一次组写入,这样 binlog 和 redo log 都进行了优化。
接下来介绍每个阶段的过程,注意下面的过程针对的是“双 1” 配置(sync_binlog 和 innodb_flush_log_at_trx_commit 都配置为 1)。
flush 阶段
第一个事务会成为 flush 阶段的 Leader,此时后面到来的事务都是 Follower :
接着,获取队列中的事务组,由绿色事务组的 Leader 对 rodo log 做一次 write + fsync,即一次将同组事务的 redolog 刷盘:
完成了 prepare 阶段后,将绿色这一组事务执行过程中产生的 binlog 写入 binlog 文件(调用 write,不会调用 fsync,所以不会刷盘,binlog 缓存在操作系统的文件系统中)。
从上面这个过程,可以知道 flush 阶段队列的作用是用于支撑 redo log 的组提交。
如果在这一步完成后数据库崩溃,由于 binlog 中没有该组事务的记录,所以 MySQL 会在重启后回滚该组事务。
sync 阶段
绿色这一组事务的 binlog 写入到 binlog 文件后,并不会马上执行刷盘的操作,而是会等待一段时间,这个等待的时长由 Binlog_group_commit_sync_delay
参数控制,目的是为了组合更多事务的 binlog,然后再一起刷盘,如下过程:
不过,在等待的过程中,如果事务的数量提前达到了 Binlog_group_commit_sync_no_delay_count
参数设置的值,就不用继续等待了,就马上将 binlog 刷盘,如下图:
从上面的过程,可以知道 sync 阶段队列的作用是用于支持 binlog 的组提交。
如果想提升 binlog 组提交的效果,可以通过设置下面这两个参数来实现:
binlog_group_commit_sync_delay= N
,表示在等待 N 微妙后,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘,也就是将「 binlog 文件」持久化到磁盘。binlog_group_commit_sync_no_delay_count = N
,表示如果队列中的事务数达到 N 个,就忽视binlog_group_commit_sync_delay 的设置,直接调用 fsync,将处于文件系统中 page cache 中的 binlog 刷盘。
如果在这一步完成后数据库崩溃,由于 binlog 中已经有了事务记录,MySQL会在重启后通过 redo log 刷盘的数据继续进行事务的提交。
commit 阶段
最后进入 commit 阶段,调用引擎的提交事务接口,将 redo log 状态设置为 commit。
commit 阶段队列的作用是承接 sync 阶段的事务,完成最后的引擎提交,使得 sync 可以尽早的处理下一组事务,最大化组提交的效率。