我们在MySQL 的日志中详细的介绍了undo log、redo log、binlog这三个日志和所用到的一些缓存知识,那么下面我们分析一下更新语句执行过程,它们是怎么变化的呢?下面我们直接给答案吧。假如我们修改一行主键索引(id)为1的数据,更新执行流程如下:
1. 首先执行语句过程可以查考MYSQL语句执行过程文章,一直到调用存储引擎接口,通过主键索引值找到id=1的数据记录:
a. 如果id=1这一行记录数据所在的数据页在 buffer pool中,那么直接返回给执行器更新
b. 如果记录不在buffer pool中,那么就从磁盘中读取数据页到buffer pool中,然后返回给 执行器更新
2. 执行器拿到数据记录之后,会比较更新前和更新后的数据是否一样?如果一样,那么就不进行后续的更新过程;如果不一样,那么就会把更新前和更新后的记录传给InnoDB去执行真正的更新过程
3. 到了InnoDB中,会先开启事务,并在InnoDB 层更新记录前,首先会在undo log中做相应的记录,即通过生产一条undo log把更新的列的旧值记下来,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在修改该 Undo 页面前需要先记录对应的 redo log,所以先记录修改 Undo 页面的 redo log ,然后再真正的修改 Undo 页面
4. InnoDB开始更新记录,根据WAL技术,修改数据页面的过程是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘
5. 至此,一条记录更新完了
6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘
事务提交,剩下的就是「两阶段提交」的事情了,也是下面重点讲解的问题。
什么是两阶段提交?
两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。
它把单个事务的提交拆分成了 2 个阶段,分别是分别是「准备(Prepare)阶段」和「提交(Commit)阶段」,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。注意,不要把提交(Commit)阶段和 commit 语句混淆了,commit 语句执行的时候,会包含提交(Commit)阶段。更通俗点讲,就比如两个人打拳,裁判就是协调者,拳击手就是参与者,在所有拳击手都准备好之后,是不会开始比赛的,这个阶段就是准备阶段;当都准备好之后,那就开始打拳,但是如果有一个人还没有准备好,那么暂停比赛(这就是事务回滚操作),这个阶段就是提交阶段。
为什么需要两阶段提交?
在事务提交之后,我们有redo log 和binlog都要持久化到磁盘中,但是这两个逻辑是相互独立的,那么就会出现半成功的状态,这就导致了两份日志逻辑不一致问题。假如修改id = 1 这行数据,半成功状态具体如下:
1. 如果redo log 刷入磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。MySQL 重启后,通过redo log 可以把Buffer Pool 中的id = 1这行数据修改的值恢复到最新的值。但是,由于binlog里面没有记录这条更新语句,而在主从架构中,binlog会被复制到从库中,所以由于binlog丢失了这条更新语句,而从库对这行的数据还是旧值,从而出现主从值不一致问题
2. 如果binlog刷入磁盘之后,MySQL突然宕机了,而redo log 还没有来得及写入。MySQL 重启后,由于redo log 还没有写入这个数据更新信息,所以崩溃恢复之后这个更新事务是无效的,所以id = 1 这行数据更改的值还是旧值。但是,binlog已经记录了这条更新语句,那么在主从架构中,binlog会被复制到从库中,从而从库会执行这条更新语句,即从库是这行数据是新值,从而导致主从值不一致问题
从上面可以知道,在redo log 和 binlog 这两份日志持久化的时候,可能会出现半成功状态,而出现主从环境中数据不一致问题。这是因为redo log影响主库,binlog影响从库,所以要解决上面的问题,必须要保证这两个日志的一致性。所以MYSQL为了解决这个问题,使用了两阶段提交解决。
两阶段提交的提交过程
在MYSQL的InnoDB存储引擎中,如果开启了binlog情况下,MYSQL会同时维护binlog和InnoDB中的redo log,为了保证这两个日志的一致性问题,它使用了内部XA事务(当然也有外部XA事务)解决。内部XA事务是由binlog作为协调者,redo log 作为参与者。MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交,如下图:
从两个阶段提交定义和上图可以知道,我们是把redo log 拆分为两个步骤:prepare 和 commit,而在这中间加入binlog的写入,具体如下:
1. prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘
2. commit 阶段:把 XID 写入到 binlog,然后将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit
如果在两个阶段出现异常怎么处理?
两个阶段出现异常情况如下图:
上图解释说明:
1. 时刻A: redo log 写入,binlog没有写入;时刻A: redo log 和binlog都写入,但没有提交commit标识;
2. 不管在时刻A,还是时刻B, redo log 都是处于 prepare 阶段
如果在MYSQL重启之后会按照顺序读取redo log 文件,当碰到处于 prepare阶段的redo log 文件,就会拿着redo log 的XID去binlog中寻找,看是否有该XID:
1. 如果binlog中没有该XID,那么就说明redo log 刷盘完成,但binlog还没有,则回滚事务,即时刻A阶段异常
2. 如果binlog中有该XID,那么就说明redo log 和binlog都刷盘完成,但commit标识还没有提交,则提交事务,即时刻B阶段异常
所以从上面可以看出,对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就可以保证 redo log 和 binlog 这两份日志的一致性了。
总结:两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。
问题1: 处于 prepare 阶段的 redo log 加上完整的binlog, 就提交事务,MYSQL为什么这么设计?
答案: 因为binlog写入了,那么根据主从原理得知最后从库也会写入。所以主库需要提交这个事务,使主从库数据一致。
问题2:如果事务还没有提交,redo log会持久化吗?
答案:会持久化。因为事务执行过程中redo log 会直接写到 redo log buffer中,这些在 redo log buffer里的redo log 会每隔一秒就被持久化到磁盘中(根据持久化策略决定)。所以事务没有提交,redo log可能会持久化到磁盘的。
问题3: 如果在事务还没有提交,而redo log 已经被持久化磁盘了。这时MYSQL崩溃了,怎么办?
答案:在MYSQL重启时,事务会进行回滚操作。因为事务没提交的时候,binlog 是还没持久化到磁盘的。
两阶段提交解决两个日志一致性,那它会不会带来新的问题呢?
虽然两阶段提交是解决了两个日志数据一致性问题,但是它也带来了一定性能问题:
1. 磁盘 I/O 次数高:对于“双1”配置(sync_binlog 和 innodb_flush_log_at_trx_commit 都配置为 1),每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘
2. 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。
磁盘 I/O 次数高
因为redo log 和binlog 都是存在对应的缓存里,即redo log缓存在redo log buffer,binlog缓存在 binlog cache中,而持久化它们是各自通过参数来控制的。一般为了数据不会丢失,都会设置这两个参数为1:
当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁
当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘
这两个参数都设置为1,就是“双1”配置。那么当有事务提交的时候,至少就是刷两个磁盘(分别是 redo log 刷盘和binlog 刷盘),所以就导致了性能问题。
锁竞争激烈
在比较老的MYSQL版本中,为了解决了顺序一致性的问题是通过加锁来处理的。即通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。但是在并发量很大的时候,会导致锁竞争激烈,而性能低下。
在比较新的MYSQL版本里,为了解决这两个问题,使用了组提交。
组提交
为了解决两阶段提交性能问题,MYSQL引入了binlog 组提交机制,就是当有多个事务提交时,会将多个binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数。
其实binlog组提交机制就把之前的 commit 阶段,拆分为三个过程:
flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);
sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);
commit 阶段:各个事务按顺序做 InnoDB commit 操作;
其实上面这三个阶段中每个阶段都有一个队列来完成,而每个队列有锁来保护,从而保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。
对每个阶段引入了队列后,锁就只针对每个队列进行保护,不再锁住提交事务的整个过程,可以看的出来,锁粒度减小了,这样就使得多个阶段可以并发执行,从而提升效率。
有 binlog 组提交,那有 redo log 组提交吗?
在MYSQL5.7版本之前是没有的,从该版本就开始有了。即在 prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了flush 阶段。
这个改进是将 redo log 的刷盘延迟到了 flush 阶段之中,sync 阶段之前。通过延迟写 redo log 的方式,为 redolog 做了一次组写入,这样 binlog 和 redo log 都进行了优化。
下面,在“双 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 可以尽早的处理下一组事务,最大化组提交的效率。
组提交总结
如果出现 MySQL 磁盘 I/O 很高的现象,我们可以通过控制以下参数,来 “延迟” binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:
设置组提交的两个参数:binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。
将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入「 redo log文件」意味着写入到了操作系统的文件缓存,然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。