导读
最近一直在看一些关于mysql innodb事务提交,innodb crash recovery,内部XA事务等资料,整理一下我对这块的一个初步理解。希望看完后能对大家理解mysql innodb事务提交过程,以及如何做crash recovery有个大概的了解
2PC
为了性能考虑,每次提交事务的时候,只需要将redo和undo落盘就代表事务已经持久化了,而不需要等待数据落盘。这样就已经能保证事务的crash时的前滚或者回滚。由于undo的信息也会写入redo,所以其实我们只需要根据redo是否落盘而决定crash recovrey的时候是重做还是回滚。而上面提到,开启binlog后,还需要考虑binlog是否落盘(binlog牵扯到主从数据一致性,全备恢复的位点)。根据事务是否成功写binlog决定事务的重做还是回滚。
2PC即innodb对于事务的两阶段提交机制。当mysql开启binlog的时候,会存在一个内部XA的问题:事务在存储引擎层(redo)commit的顺序和在binlog中提交的顺序不一致的问题。
2PC原理
将事务的commit分为prepare和commit两个阶段:
1、prepare阶段:redo持久化到磁盘(redo group commit),并将回滚段置为prepared状态,此时binlog不做操作。
2、commit阶段:innodb释放锁,释放回滚段,设置提交状态,binlog持久化到磁盘,然后存储引擎层提交
对2PC有了一个了解之后,我们可以发现上面图中redo和binlog都可以group commit,那么下面了解下什么是group commit
Group Commit
背景
我们知道,日志的写入基本上是顺序IO。WAL(Write-Ahead-Logging)用顺序的日志写入代替数据的随机IO实现事务持久化。但是尽管如此,每次事务提交都需要日志刷盘,仍然受限于磁盘IO。group commit的出现就是为了将日志(redo/binlog)刷盘的动作合并,从而提升IO性能
2PC机制只能解决单个事务的redo/binlog顺序一致的问题,如果并发怎么办呢(有可能存在一种情况:binlog提交顺序(T1,T2,T3),innodb commit顺序(T2,T3,T1))。
如上图,事务按照T1、T2、T3顺序开始执行,将二进制日志(按照T1、T2、T3顺序)写入日志文件系统缓冲,调用fsync()进行一次group commit将日志文件永久写入磁盘,但是存储引擎提交的顺序为T2、T3、T1。当T2、T3提交事务之后,若通过在线物理备份进行数据库恢复来建立复制时,因为在InnoDB存储引擎层会检测事务T3在上下两层都完成了事务提交,不需要在进行恢复了,此时主备数据不一致
MySQL 5.6版本之前,通过prepare_commit_mutex锁以串行的方式来保证MySQL数据库上层二进制日志和Innodb存储引擎层的事务提交顺序一致,然后会导致group commit无法生效
上图所示MySQL开启Binary log时使用prepare_commit_mutex和sync_log保证二进制日志和存储引擎顺序保持一致,prepare_commit_mutex的锁机制造成高并发提交事务的时候性能非常差而且二进制日志也无法group commit。
Redo Group Commit
2PC中的prepare阶段,会对redo进行一次刷盘操作(innodb_flush_log_at_trx_commit=1),这时候redo group commit的过程如下:
- 获取 log_mutex
- 若flushed_to_disk_lsn>=lsn,表示日志已经被刷盘,跳转5
- 若 current_flush_lsn>=lsn,表示日志正在刷盘中,跳转5后进入等待状态
- 将小于LSN的日志刷盘(flush and sync)
- 退出log_mutex
这个过程是根据LSN的顺序进行合并的,也就是说一次redo group commit的过程可能会讲别的未提交事务中的lsn也一并刷盘
Redo Group Commit优化
每个事务提交时,都会触发一次redo flush动作,由于磁盘读写比较慢,因此很影响系统的吞吐量。淘宝做了一个优化,将prepare阶段的刷redo动作移到了commit(flush-sync-commit)的flush阶段之前,保证刷binlog之前,一定会刷redo。这样就不会违背原有的故障恢复逻辑。移到commit阶段的好处是,可以不用每个事务都刷盘,而是leader线程帮助刷一批redo。如何实现,很简单,因为log_sys->lsn始终保持了当前最大的lsn,只要我们刷redo刷到当前的log_sys->lsn,就一定能保证,将要刷binlog的事务redo日志一定已经落盘。通过延迟写redo方式,实现了redo log组提交的目的,而且减少了log_sys->mutex的竞争。目前这种策略已经被官方mysql5.7.6引入。
Binlog Group Commit
Binlog Group Commit的基本思想是引入队列机制,保证innodb commit的顺序与binlog落盘的顺序一致,并将事务分组,组内的binlog刷盘动作交给一个事务进行,实现组提交目的。队列中的第一个事务称为leader,其他事务称为follower。所有事情交给leader去做。过程分为三个阶段
Flush Stag
将每个事务的binlog写入内存
1) 持有Lock_log mutex [leader持有,follower等待]。
2) 获取队列中的一组binlog(队列中的所有事务)。
3) 将binlog buffer到I/O cache。
4) 通知dump线程dump binlog。
Sync Stage
将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。
1) 释放Lock_log mutex,持有Lock_sync mutex[leader持有,follower等待]。
2) 将一组binlog 落盘(sync动作,最耗时,假设sync_binlog为1)。
Commit Stage
leader根据顺序调用存储引擎层事务的提交,Innodb本身就支持group commit,因此修复了原先由于锁prepare_commit_mutex导致group commit失效的问题。
1) 释放Lock_sync mutex,持有Lock_commit mutex[leader持有,follower等待]。
2) 遍历队列中的事务,逐一进行innodb commit。
3) 释放Lock_commit mutex。
4) 唤醒队列中等待的线程。
由于有多个队列,每个队列各自有mutex保护,队列之间是顺序的,约定进入队列的一个线程为leader,因此FLUSH阶段的leader可能是SYNC阶段的follower,但是follower永远是follower。
当有一组事务在进行commit阶段时,其他新事物可以进行Flush阶段,从而使group commit不断生效。group commit的效果由队列中事务的数量决定,若每次队列中仅有一个事务,那么可能效果和之前差不多,甚至会更差。但当提交的事务越多时,group commit的效果越明显,数据库性能的提升也就越大。
还有一点要说明:sync_binlog=1这个参数单位表示的是一组事务,而并非一个事务
Crash Recovery
Crash Recovery(no binlog)
由于未提交的事务和已回滚的事务也会记录到redo log中,因此在进行恢复的时候,这些事务要进行特殊的处理
innodb的处理策略是:进行恢复时,从checkpoint开始,重做所有事务(包括未提交的事务和已回滚的事务),然后通过undo log回滚那些未提交的事务
XA Crash Recovery
1、扫描最后一个 binlog,提取xid(标识binlog中的第几个event)
2、xid也会写到redo中,将redo中prepare状态的xid,去跟最后一个binlog中的xid比较 ,如果binlog中存在,则提交,否则回滚
为什么只扫描最后一个binlog?因为binlog rotate的时候会把前面的binlog都刷盘,而且事务是不会跨binlog的
参考资料
MySQL Group Comit - 运维那点事
MySQL 外部XA及其在分布式事务中的应用分析
MySQL undo,redo,2PC,恢复思维导图
MYSQL-GroupCommit
MySQL的Crash Safe和Binlog的关系 - 老叶茶馆