0%

MySQL基础之BLGC

1. 组提交介绍

1.1 什么是组提交

  Binary Log Group Commit 即二进制日志组提交。这是 MySQL5.6 版本中引进的一个新的特性。为什么需要引进这个特性呢?我们知道当我们把 MySQL 的 binlog 开启的时候,MySQL 会将每个事务的操作都记录到 binlog 中,方便我们使用 binlog 来完成复制或者恢复操作。可是需要调用 fsync() 才能将缓存中被更改的 binlog 真正的写到磁盘上,保证数据的持久化。但是这是一个从内存写到磁盘的过程,I/O 比较慢。如果每次事务提交都执行一遍 fsync() 将 binlog 持久化落盘到磁盘的话,效率很低。于是就想,能不能等几个事务的 binlog 一起调用一次 fsync(),一次性落盘。减少 fsync() 的次数,从而提高效率。这就是二进制日志组提交的概念。

2.两阶段提交

2.1 为什么需要二阶段提交

  我们知道在 MySQL 中不仅仅有 binlog,还有 redo log 和 undo log 。binlog 用来记录每个事务的操作信息,redo 是在数据库宕机恢复时使用,用来恢复数据库数据,undo 用来回滚还未被提交的数据。binlog 是在数据库 Server 层产生的,即它会记录所有存储引擎中事务的操作,而 redo 是 InnoDB 存储引擎特有的日志。

  在事务提交的时候,我们需要先写入二进制日志,再写 InnoDB 存储引擎的 redo。并且要求二进制日志和 redo 要么都写,要么都不写。不然可能会出现这样的情况:在主从复制的环境下,master 提交了一个事务,先写了二进制日志,但是在要写 InnoDB 存储引擎的时候,数据库发生了宕机,此时 binlog 又已经被 slave 接收到了,slave 会执行这个事务,但是实际 master 上并没有这个事务。这就会导致主从数据的不一致。所以我们引入了二阶段提交来解决这个问题,即将写 binlog 操作个 InnoDB 提交操作通过事务变成原子的。

2.2 什么是二阶段提交

  所谓的二阶段提交就是,我在事务提交的时候,确保先将 binlog 写入,然后再到数据引擎层提交,并且这两个操作是原子的。在 MySQL 中用内部的 XA 事务来完成,即将这两个操作包装成一个事务的概念。

  上图表示了二阶段提交的过程。当一个会话中的某一事务 COMMIT 的时候,进去二阶段提交的过程。首先数据库先去协调 Server 层和 Engine,询问是否都可以开始写日志,这个过程就是图中的的 prepare 阶段。协调好两层之间的关系,Server 层和 Engine 层都表示可以写日志,这时候进入下一个过程。

  第二个过程就是写 binlog 的过程,先把 binlog 写到内存中,然后调用 fsync() 将 binlog 从内存写到磁盘上。

  第三个过程就是在存储引擎层提交的过程,将真实修改的数据提交到数据库中。当这一步完成才最终返回给会话一个 COMMIT 成功的信号。

  这整个过程就是二阶段提交的过程,如果在 fsync() 之前数据库 crash 了,重启之后数据将会被回滚,若在 fsync() 之后 crash,则会进行重做操作。通过二阶段提交的方式就保证了存储引擎与二进制日志保持一致

  

3.三阶段提交

3.1 为什么需要三阶段提交

  上面的二阶段提交是针对单一事务提交时候的操作顺序,下面我们来看看当多个事务并发的时候会是什么样的一个情况。

  现在有T1、T2、T3 三个事务需要执行,从图中可以看到数据在 fsync() 之前,三个事务已经写入到了 binlog 中,通过 fsync() 操作将 binlog 刷到磁盘。之后先是 T2 COMMIT,将数据更改更新到存储引擎层,接着是 T3 COMMIT,将数据更新到存储引擎层。这时候我们做了一个热备份的操作,有多种方式进行数据库的热备份,比如:XtraBackup等。这时候就会发生错误。会发生什么错误,我们需要先了解一下 XtraBackup 等热备工具的备份原理。

  XtraBackup备份原理:直接拷贝数据库文件,并且记录下当前二进制日志中已经提交的最后一个事务标记。在新的数据库实例上完成 recovery 操作。

  了解完备份原理之后,我们就可以想到上述情况下做热备会出现什么情况。因为 T2、T3 已经提交,所以备份的时候会记录下 T3 是最后一个提交的事务,会认为 T3 之前的事务都是已经提交的,由于是直接拷贝数据库文件,可以看到 T1 事务的数据还没有提交到存储引擎层,所以备份数据中还并没有 T1 的数据。如果新的数据库是用来做主从复制的话,change master to 会指向二进制日志中 T3 的位置,从 T3 事务开始往后进行复制,这样一来 T1 事务的数据就这样没了。产生这个问题的主要原因就是:事务写入二进制日志的顺序与事务在存储引擎层提交的顺序不一致

  为了解决这个问题,MySQL 引入了 prepare_commit_mutext 的机制,当事务提交的时候,需要先获得 prepare_commit_mutext 这个锁。有了这个锁就可以保证事务写入二进制日志的顺序与事务在存储引擎层提交的顺序一致。

  但是这样一来,从图中我们也可以看到,原先是并发的事务,又变成了串行的,效率又变低了。只要是问题,必然存在解决方法。于是三阶段提交就出现了。

3.2 什么是三阶段提交

  三阶段提交,顾名思义有三个阶段: Flush 阶段、sync 阶段、commit 阶段。分别对应的就是二进制日志写内存的阶段、二进制日志刷盘的阶段、事务提交到存储引擎层的阶段。

  每个阶段都有 leader、follower 两种角色。当一个事务进入三个阶段中的某一个阶段,如果发现这个阶段中的队列为空,那么这个事务就会成为 leader 的角色,之后进入同一阶段的事务,发现这个阶段的队列中已经有事务存在了,那就变成 follower 角色。leader 角色的任务是安排当前阶段队列中的事务按顺序执行,并且带领队列中所有的事务进入下一个阶段。当 leader 带领队列中的事务进入下一阶段的时候,如果发现下一阶段中已经有事务存在(即下一阶段已有 leader 存在),新来的 leader 自动变成 follower 角色。

  三阶段提交在每个阶段都控制了事务的顺序,从而也就控制了事务执行的整体顺序。解决了 prepare_commit_mutex 锁导致的问题,事务可以并发的执行。

参考资料

  1. MySQL并发复制系列一:binlog组提交
  2. MySQL并发复制系列二:多线程复制
  3. Binary Log Group Commit in MySQL 5.6
  4. 《MySQL技术内幕》

博客地址:https://win-man.github.io/
公众号:欢迎关注