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

从图中可看出,事务的提交过程有两个阶段,就是将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()把日志数据永久写入磁盘数据库的性能将会急剧下降。如图

为什么需要保证二进制日志的写入顺序和InnoDB层事务提交顺序一致性呢?
单个事务的二阶段提交过程,能够保证存储引擎和binaryLog日志保持一致,但是在并发的情况下怎么保lnnoDB层事务日志和MySQL数据库二进制日志的提交的顺序一致?当多个事务并发提交的情况,如果BinaryLog和存储引
擎顺序不一致会造成什么影响?
这是因为备份及恢复需要,例如通过xtrabackup或ibbackup这种物理备份工具进行备份时,并使用备份来建立复
制,如图:

如上图,事务按照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 阶段拆分为三个过程:
flush阶段:多个事务按进入的顺序将binlog从cache写入文件(不刷盘);
sync阶段:对binlog文件做fsync操作(多个事务的binlog合并一次刷盘);
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个事务都会丢失

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

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

接着,获取队列中的事务组,由绿色事务组的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。
trx1是最先到达的,会被选为组leader;
当trx1准备提交的时候,组里面已经有3个事务了,此时LSN变成了160;
trx1写盘结束后,LSN小于160的区块都已经被持久化;
此时trx2和trx3就可以直接返回了。

完成了prepare阶段后,将绿色这一组事务执行过程中产生的binlog写入binlog文件(调用write,不会调用fsync,所以不会刷盘,binlog缓存在操作系统的文件系统中)。
从上面这个过程,可以知道fLush阶段队列的作用是用于支撑redoLog的组提交。
如果在这一步完成后数据库崩溃,由于binlog中没有该组事务的记录,所以MySQL会在重启后回滚该组事务。
2.sync 阶段

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

不过,在等待的过程中,如果事务的数量提前达到了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 阶段

1:首先获取队列中的事务组(当前队列中的所有事务)
2:依次将redo log中已经prepare的事务在引擎层提交(图中InnoDB Commit)
3:commit阶段不用刷盘,如上所述,flush阶段中的redo log刷盘已经足够保证数据库崩溃时的数据安全了
4:commit阶段队列的作用是承接sync阶段的事务,完成最后的引擎提交,使得sync可以尽早的处理下一组事务,最大化组提交的效率



