两阶段提交的过程是怎样的?

image-20241014160316484

从图中可看出,事务的提交过程有两个阶段,就是将redoLog的写入拆成了两个步骤:prepare和commit,中

间再穿插写入binlog,具体如下:

1:prepare阶段:将XID(内部XA事务的ID)写入到redo log buffer,同时将redo log对应的事务状态设置为prepare,然后将redo log持久化到磁盘(innodb_flush_log_at_trx_commit=1的作用);

2:commit阶段:把XID写入到binlog cache,然后将binlog持久化到磁盘(sync_binlog=1的作用),接着调用引擎的提交事务接口,将redo log状态设置为commit,此时该状态并不需要持久化到磁盘,只需要write到文件系统的page cache中就够了,因为只要binlog写磁盘成功,就算redo log的状态还是prepare也没有关系,一样会被认为事务已经执行成功;

为什么需要两阶段提交?

如果只写一次的话,那到底先写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就被设计成了两阶段提交模式,设置成两阶段提交后,整个执行过程有三处崩溃点:

1:redo-log(prepare):在写入准备状态的redo记录时宕机,连prepare都没写成功,所以不会影响一致性。

2:bin-log:在写binlog记录时崩溃,重启后会根据redo记录中的事务ID,回滚前面已写入的数据。

3:redo-log(commit):在bin-log写入成功后,写redo(commit)记录时崩溃,因为bin-log中已经写入成功了,所以从机也可以同步数据,因此重启时直接再次提交事务,写入一条redo(commit)记录即可。

结论:通过这种两阶段提交的方案,就能够确保redo-log、bin-log两者的日志数据是相同的,bin-log中有的主机再恢复,如果bin-log没有则直接回滚主机上写入的数据,确保整个数据库系统的数据一致性。

两阶段提交有什么问题?以及Mysql是如何优化的?

两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响:

1:磁盘I/O次数高:对于”双1”配置,每个事务提交都会进行两次fsync(刷盘),一次是redo log刷盘,另一次是binlog刷盘。

2:锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。在早期的MySQL版本中,通过使用prepare_commit_mutex锁来保证事务提交的顺序,在一个事务获取到锁时才能进入prepare阶段,一直到commit阶段结束才能释放锁,下个事务才可以继续进行prepare操作。通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳

通过->事务提交的方式一一组提交的方式来解决

binlog组提交产生之前

MySQL5.6以前,为了保证数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致,MySQL数据库内

部使用了prepare_commit_mutex锁。锁的持有与释放在二阶段中如下:

  • InnoDB prepare (持有prepare_commit_mutex);

  • write/sync Binlog;

  • InnoDB commit(写入COMMIT标记后释放prepare_commit_mutex)。

这样事务提交就是一个一个执行,效率低,只有当上一个事务commit后释放锁,下一个事务才可以进行prepare操作,并且在每个事务过程中Binary Log都有fsync()的调用。由于内存数据写入磁盘的开销很大,如果频繁fsync()把日志数据永久写入磁盘数据库的性能将会急剧下降。如图

image-20241024235546457

为什么需要保证二进制日志的写入顺序和InnoDB层事务提交顺序一致性呢?

单个事务的二阶段提交过程,能够保证存储引擎和binaryLog日志保持一致,但是在并发的情况下怎么保lnnoDB层事务日志和MySQL数据库二进制日志的提交的顺序一致?当多个事务并发提交的情况,如果BinaryLog和存储引

擎顺序不一致会造成什么影响?

这是因为备份及恢复需要,例如通过xtrabackup或ibbackup这种物理备份工具进行备份时,并使用备份来建立复

制,如图:

image-20241024235602459

如上图,事务按照T1、T2、T3顺序开始执行,将二进制日志(按照T1、T2、T3顺序)写入日志文件系统缓冲,调用fsync()进行将日志文件永久写入磁盘,但是存储引擎提交的顺序为T2、T3、T1。当T2、T3提交事务之后,若通过在线物理备份进行数据库恢复来建立复制时,因为在InnoDB存储引擎层会检测事务T3在上下两层(prepare、commit两层)都完成了事务提交,不需要在进行恢复了,此时主备数据不一致(搭建slave时,change master to的日志偏移量记录T3在事务位置之后)。

为了解决以上问题,在早期的MySQL5.6版本之前,通过prepare_commit_mutex锁以串行的方式来保证MySQL数据库上层二进制日志和innodb存储引擎层的事务提交顺序一致,然后会导致组提交(group commit)特性无法生效。

MySQL数据库内部在prepare redo阶段获取prepare_commit_mutex锁,一次只能有一个事务可获取该mutex。通过这个臭名昭著prepare_commit_mutex锁,将redo log和binlog刷盘串行化,串行化的目的也仅仅是为了保证redo log和BinLog一致,继而无法实现group commit,牺性了性能。

bin log 组提交

MySQL引入了binlog组提交(groupcommit)机制,当有多个事务提交的时候,会将多个binlog刷盘操作并成一个,从而减少磁盘VO的次数。

如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。

引入了组提交机制后,prepare 阶段不变,只针对二阶段中的commit阶段,将commit 阶段拆分为三个过程:

  1. flush阶段:多个事务按进入的顺序将binlog从cache写入文件(不刷盘);

  2. sync阶段:对binlog文件做fsync操作(多个事务的binlog合并一次刷盘);

  3. commit阶段:各个事务按顺序做InnoDB commit操作;

上面的每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。

具体过程如下:

binlog提交将提交分为了3个阶段,FLUSH阶段,SYNC阶段和COMMIT阶段。每个阶段都有一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。BLGC的步骤分为以下三个阶段:

1:FLUSH阶段:

持有lock_log mutex [leader持有,follower等待]

获取队列中的一组binlog(队列中的所有事务)

将binlog buffer到 I/O cache

通知dump线程dump binlog(主从同步的binlog dump线程)

2:SYNC阶段:

释放lock_log mutex,持有lock_sync mutex [leader持有,follower等待]

将一组binlog落盘(fsync动作,最耗时,也是group commit实现了的优化的重点所在)

3:COMMIT阶段:

释放lock_sync mutex,持有lock_commit mutex [Leader持有,follower等待]

遍历队列中的事务,逐一进行innodb commit(这里不用写redo log进行刷盘,在prepare阶段已刷盘)

释放lock_commit mutex

唤醒队列中等待的线程

优点:

每个stage分配一个线程进行操作。

这种实现的优势在于三个阶段可以并发执行,从而提升效率。(ps:innodb prepare阶段没有变,还是write/sync redo log,打上prepare标记)

每个stage都有自己的队列。每个队列各自有mutex保护,队列之间是顺序的。只有flush完成后,才能进入到sync阶段的队列中;sync完成后,才能进入到commit阶段的队列中。但是,这三个阶段的作业是可以同时并发执行的,即当一组事务在进行commit阶段时,其他新事务可以进行flush阶段,实现了group commit。

当一个事务来到一个stage是一个空队列,那么他就是leader,后面来的事务就是follower,leader控制队列中follower的行为。如果leader带着自己的follower去下一个stage,是非空队列,那么leader变成follower。但是follower不会变成leader。

Tips:当引入Group Commit后,sync_binlog的含义就变了,假定设为1000,表示的不是1000个事务后做一次fsync,而是1000个事务组。也就是说,当设置sync_binlog=1,binlog还未落盘,此时系统crash,会丢失对应的最后一个事务组;如果这个事务组内有10个事务,那么这10个事务都会丢失

image-20241024185605879

如图所示:

对每个阶段引入了队列后,锁就只针对每个队列进行保护,不再锁住提交事务的整个过程,可以看的出来,锁粒度减小了,这样就使得多个阶段可以并发执行,从而提升效率。

redo log组提交

这个要看MySQL版本,MySQL5.6没有redo log组提交,MysQL5.7有redo log组提交。

在MySQL5.6的组提交逻辑中,每个事务各自执行prepare阶段,也就是各自将redo log刷盘,这样就没办法对redo log进行组提交。

所以在MySQL5.7版本中,做了个改进,在prepare阶段不再让事务各自执行redo log刷盘操作,而是推迟到组提交的flush阶段,也就是说prepare阶段融合在了flush阶段。

这个优化是将redo log的刷盘延迟到了flush阶段之中,sync阶段之前。通过延迟写redo log的方式,为redo log做了一次组写入,这样binlog和redo log都进行了优化。

redo log+binlog组提交的过程

注意:下面的过程针对的是”双1”配置(sync_binlog和innodb_flush_log_at_trx_commit都配置为1)。

1.flush阶段

image-20241024191640514

第一个事务会成为flush阶段的leader,此时后面到来的事务都是follower:

image-20241024191728935

接着,获取队列中的事务组,由绿色事务组的leader 对redo log 做一次write+fsync,即一次将同组事务的redo log刷盘:

MySQL为了优化磁盘持久化的开销,会有一个组提交(group commit)的机制。首先我们介绍一下日志逻辑序列号(log sequence number,LSN),它是用来对应每个redo log写入点的递增序列号。每次写入长度为length的 redo log,LSN 就会加上 length。

下面是3个并发事务(trxl、trx2、trx3)在prepare 阶段都写完了redo log buffer,然后组提交持久化的过程。其中3个事务对应的LSN分别是:50、120、160。

  1. trx1是最先到达的,会被选为组leader;

  2. 当trx1准备提交的时候,组里面已经有3个事务了,此时LSN变成了160;

  3. trx1写盘结束后,LSN小于160的区块都已经被持久化;

  4. 此时trx2和trx3就可以直接返回了。

image-20241024192134602

完成了prepare阶段后,将绿色这一组事务执行过程中产生的binlog写入binlog文件(调用write,不会调用fsync,所以不会刷盘,binlog缓存在操作系统的文件系统中)。

从上面这个过程,可以知道fLush阶段队列的作用是用于支撑redoLog的组提交。

如果在这一步完成后数据库崩溃,由于binlog中没有该组事务的记录,所以MySQL会在重启后回滚该组事务。

2.sync 阶段

image-20241024192305462

绿色这一组事务的binlog写入到binlog 文件后,并不会马上执行刷盘的操作,而是会等待一段时间,这个等待的时长由 binlog_group_commit_sync_delay参数控制,目的是为了组合更多事务的binlog,然后再一起刷盘,如下过程:

image-20241024192428286

不过,在等待的过程中,如果事务的数量提前达到了binlog_group_commit_sync_no_delay_count参数设置值,就不用继续等待了,就马上将binlog刷盘,如上图:

从上面的过程,可以知道sync阶段队列的作用是用于支持binlog的组提交。

如果想提升binlog组提交的效果,可以通过设置下面这两个参数来实现:

  • 1:binlog_group_commit_sync_delay=N,表示在等待N微妙后,直接调用fsync,将处于文件系统 page cache中的binlog 刷盘,也就是将「binlog文件」持久化到磁盘。

  • 2:binlog_group_commit_sync_no_delay_count=N,表示如果队列中的事务数达到N个,就忽视binlog_group_commit_sync_delay的设置,直接调用fsync,将处于文件系统中pagecache中的binlog刷盘。

如果在这一步完成后数据库崩溃,由于binlog中已经有了事务记录,MySQL会在重启后通过redo log刷盘的数据继续进行事务的提交

3. commit 阶段

image-20241024192817611

  • 1:首先获取队列中的事务组(当前队列中的所有事务)

  • 2:依次将redo log中已经prepare的事务在引擎层提交(图中InnoDB Commit)

  • 3:commit阶段不用刷盘,如上所述,flush阶段中的redo log刷盘已经足够保证数据库崩溃时的数据安全了

  • 4:commit阶段队列的作用是承接sync阶段的事务,完成最后的引擎提交,使得sync可以尽早的处理下一组事务,最大化组提交的效率