一致性问题:
MySQL为了兼容其它非事物引擎的复制,在server层面引入了 binlog, 它可以记录所有引擎中的修改操作,因而可以对所有的引擎使用复制功能; 但是引入了binlog,会导致一个问题——binlog和redo log的一致性问题:
- 一个事务的提交必须写redo log和binlog,那么二者如何协调一致呢?
- 事务的提交以哪一个log为标准?
- 如何判断事务提交?
- 事务崩溃恢复如何进行?
解决方案
自 5.1 之后,binlog 和 innodb 采用类似两阶段提交的方式,不过不支持 group commit;
在 5.6 中,引入binlog组提交,将 binlog 的 commit 阶段分为三个阶段:flush stage、sync stage 以及 commit stage。
两阶段提交(2PC):
第一阶段:InnoDB prepare,持有prepare_commit_mutex,并且write/sync redo log; 将回滚段设置为Prepared状态,binlog不作任何操作;
第二阶段:包含两步,1> write/sync Binlog; 2> InnoDB commit (写入COMMIT标记后释放prepare_commit_mutex);
以 binlog 的写入与否作为事务提交成功与否的标志,innodb commit标志并不是事务成功与否的标志。因为此时的事务崩溃恢复场景如下:
1> 一阶段prepare 成功 redo log落盘完成,二阶段binlog写入失败,崩溃恢复时回滚该事物。
2> 一阶段prepare 成功 redo log落盘完成,二阶段binlog写入成功,commit失败,崩溃恢复时扫描最后一个Binlog文件,提取其中的xid,InnoDB维持了状态为Prepare的事务链表,将这些事务的xid和Binlog中记录的xid做比较,在Binlog中存在,则提交该事物。
通过这种方式,可以让InnoDB和Binlog中的事务状态保持一致。如果在写入innodb commit标志时崩溃,则恢复时,会重新对commit标志进行写入;
组提交(group commit):
上面的事务的两阶段提交过程是5.6之前版本中的实现,有严重的缺陷。当sync_binlog=1时,很明显上述的第二阶段中的 write/sync binlog会成为瓶颈,而且还是持有全局大锁(prepare_commit_mutex: prepare 和 commit共用一把锁),这会导致性能急剧下降。解决办法就是MySQL5.6中的 binlog组提交。
5.6 引入了组提交,并将提交过程分成 Flush stage、Sync stage、Commit stage 三个阶段。
InnoDB, Prepare
SQL已经成功执行并生成了相应的redo和undo内存日志(对多个线程的redo合并写入磁盘);
Binlog, Flush Stage
所有已经注册线程都将写入binlog缓存;
Binlog, Sync Stage
binlog缓存将sync到磁盘,sync_binlog=1时该队列中所有事务的binlog将永久写入磁盘(对多个线程的binlog合并写入磁盘);
InnoDB, Commit stage
leader根据顺序调用存储引擎提交事务;
原理:
- 多个并发需要提交的事务共享一次fsync操作来进行数据的持久化
- 将fsync操作的开销平摊到多个并发的事务上去
- group commit 不是在任何时候都能发挥作用,要有足够多并发的需要提交的事务
实现:
- 多个并发提交的事务在写redo log或binlog前会被加入到一个队列中
- 队列头部的事务所在的线程称为leader线程,其它事务所在的线程称为follower线程
- leader线程负责为队列中所有的事务进行写binlog操作,此时,所有的follower线程处于等待状态
- 然后leader线程调用一次fsync操作,将binlog持久化
- 最后通知follower线程可以继续往下执行
参数:
binlog_group_commit_sync_delay=N
定时发车,在等待N 微秒后,进行binlog刷盘操作
binlog_group_commit_sync_no_delay_count=N
人满发车,达到最大事务等待数量,开始binlog刷盘,忽略定时发车
疑惑
看到这里,有人可能会想,既然生产环境一般建议将 innodb_flush_log_at_trx_commit 设置为 1,也就是说每次更新数据时,最终还是要将 redo log 写入到磁盘,也就是还是会发生一次磁盘 IO,而我为什么不直接停止使用 redo log,而在每次更新数据时,也不要直接更新内存了,直接将数据更新到磁盘,这样也是发生了一次磁盘 IO,何必引入 redo log 这一机制呢?
首先引入 redo log 机制是十分必要的。因为写 redo log 时,我们将 redo log 日志追加到文件末尾,虽然也是一次磁盘 IO,但是这是顺序写操作(不需要移动磁头);而对于直接将数据更新到磁盘,涉及到的操作是将 buffer pool 中缓存页写入到磁盘上的数据页上,由于涉及到寻找数据页在磁盘的哪个地方,这个操作发生的是随机写操作(需要移动磁头),相比于顺序写操作,磁盘的随机写操作性能消耗更大,花费的时间更长,因此 redo log 机制更优,能提升 MySQL 的性能。
从另一方面来讲,通常一次更新操作,我们往往只会涉及到修改几个字节的数据,而如果因为仅仅修改几个字节的数据,就将整个数据页写入到磁盘(无论是磁盘还是 buffer pool,他们管理数据的单位都是以页为单位),这个代价未免也太了(每个数据页默认是 16KB),而一条 redo log 日志的大小可能就只有几个字节,因此每次磁盘 IO 写入的数据量更小,那么耗时也会更短。 综合来看,redo log 机制的引入,在提高 MySQL 性能的同时,也保证了数据的可靠性。
参考:MySQL一条更新sql的执行流程,以及日志的两阶段提交,以及崩溃恢复流程和组提交_刘Java的博客-CSDN博客