[TOC]
0. 参考文献
本文主要介绍了 MySQL binlog 组提交的原理和源码实现. 感谢上述参考文献在本文形成的过程中提供的帮助. 本文所介绍的内容如下:
MySQL 两阶段提交实现的历史以及存在的问题
MySQL binlog 组提交实现的原理
1. innodb 和 binlog 的两阶段提交
众所周知, 事务在 innodb 上提交的时候需要日志先行 WAL(Write-Ahead-Log). 在 binlog 开启的情况下, 为了保证 binlog 和存储引擎的一致性, 会在事物提交的时候自动开启两阶段提交. 对于单个事务, MySQL 实现的两阶段提交流程如图所示(参考文献 1 和 文献 2 ):
当事务进入 PrePare 阶段的时候, 会在存储引擎层进行提交. 生成 undo log 和 redo log 内存日志.
之后生成 binlog 并调用 sync 落盘.
在存储引擎层提交, 通过 innodb_flush_log_at_trx_commit 参数的设置, 使 undo 和 redo 永久写入磁盘.
在 MySQL 启动恢复的阶段, 会执行如下的操作:
如果事务在 prepare 阶段 MySQL 异常退出, 且 binlog 和 innodb 都没有提交. 则在恢复阶段直接忽略这个事务不进行提交.
如果事务在 innodb commit 的阶段异常, 但是 binlog 已经写入了磁盘. 则在恢复的时候, MySQL 会从 binlog 中提取信息, 并把这个事务重做.
以上是 MySQL 在开启 binlog 的情况下使用两阶段提交保证 binlog 和 innodb 层面都提交的流程. 不过在并发的情况下, 会存在一定的问题. 如图所示, 有 3 个事务 T1,T2,T3 进入 Prepare 阶段:
下面来说明下图中 T1,T2,T3 提交的过程中都发生了什么:
T1 ,T2,T3 依次写入 binlog 文件, 并调用 fsync 一次性写入磁盘.
T2,T3 先行进入提交阶段执行 commit.
在 T1 提交之前, 做了一次热备份(例如使用 mysqlbackup,xtrabackup 等工具). 此时因为 T1 没有提交, 备份工具记录的当前 binlog 位置是指向的 T3 提交的时刻.
T1 提交.
如果此时 DBA 使用上面第三点的备份数据, 在其他机器上恢复备份并搭建主从复制, 则 T1 事务会完美的被错过造成主从数据不一致. 原因是因为备份开始同步 binlog 的位置是指向了 T3 提交的时刻(不会拉取 T3 提交时刻以前的 binlog, 因此 T1 提交的 binlog 不会被读取), 而且因为 T1 在备份时刻没有提交, 则在恢复备份的时候会被 MySQL 回滚.
对于这个问题, 在 mysql5.6 之前使用 prepare_commit_mutex 保证顺序. 并且只有当上一个事务 commit 后释放锁, 下个事务才可以进行 prepara 操作, 并且在每个事务过程中 binlog 没有 fsync() 的调用. 接下来介绍下, 在使用 prepare_commit_mutex 保证事务顺序提交的时候, 为什么能够解决这个问题.
同样如上图所示, 展示了 3 个事务 T1,T2,T3 顺序提交的时候的过程. 如果 DBA 在 T3 写入 binlog 之后 commit 之前建立了一次备份, 则如上所述 T3 因为没有提交, 在恢复备份的时候会被回滚. 之后 DBA 在搭建同步的时候, 根据备份时候备份工具 (例如使用 mysqlbackup,xtrabackup 等工具) 记录的参数从 T2commit 的时刻开始拉取 binlog, 则此时可以拉取到 T3 提交的事务并重放, 因此保证了主从的一致性. 在这里, 可以看出如果使用了 prepare_commit_mutex 保证顺序提交, 则会极大的影响 MySQL 的并发性能. 因此在 mysql5.6 开始提出了 binlog 组提交的改进.
2. 组提交原理
上文提到 mysql5.6 之后对于 binlog 的提交做了改进. 首先去掉了 prepare_commit_mutex 锁, 并且把整个 commit 阶段分为 3 个部分:
FLUSH: 在这个阶段 leader 事务把 thd 的缓存写到 binlog 文件的缓存中.
SYNC: 在这个阶段 leader 事务调用 fsync 把缓存一次性落盘.
COMMIT : 在这个阶段, 根据参数 binlog_order_commits 的设定, 让事务依次提交或者各种提交(binlog 中提交的顺序可能会和 innodb 中提交的顺序不同)
组提交的流程如图所示:
从上图中可以看出, 每个阶段都会产生一个 leader 进程. 当一个事务进程进入队列的时候, 会有如下的 2 种情况:
队列为空.
队列中已有其他的事务.
在第一种情况下, 当前事务称为 leader 进程, 后续进来事务成为 follower 并使用条件变量进入休眠. 后续的工作会由 leader 进程代替 follower 进程完成. 在第二种情况下, 当前事务会成为 followr 进而休眠等到 leader 完成剩余的工作.
3. 组提交实现
前文介绍了组提交的原理, 本小节将介绍下组提交在 MySQL 源码层面上的实现过程. 本文去掉了代码中关于错误处理, 同步和其他输出代码, 保留了组提交主流程的相关代码.
3.1 order_commit
如上图所示, 组提交的入口是 order_commit 函数:
- 9498 int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit)
- 9499 {
- ... ...
- 9570 if (change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log))
- 9571 {
- 9572 DBUG_PRINT("return", ("Thread ID: %u, commit_error: %d",
- 9573 thd->thread_id(), thd->commit_error));
- 9574 DBUG_RETURN(finish_commit(thd));
- 9575 }
- ... ...
- 9594 flush_error= process_flush_stage_queue(&total_bytes, &do_rotate,
- 9595 &wait_queue);
- ... ...
- 9646 /*
- 9647 Shall introduce a delay only if it is going to do sync
- 9648 in this ongoing SYNC stage. The "+1" used below in the
- 9649 if condition is to count the ongoing sync stage.
- 9650 When sync_binlog=0 (where we never do sync in BGC group),
- 9651 it is considered as a special case and delay will be executed
- 9652 for every group just like how it is done when sync_binlog= 1.
- 9653 */
- 9654 if (!flush_error && (sync_counter + 1>= get_sync_period()))
- 9655 stage_manager.wait_count_or_timeout(opt_binlog_group_commit_sync_no_delay_count,
- 9656 opt_binlog_group_commit_sync_delay,
- 9657 Stage_manager::SYNC_STAGE);
- ... ...
- 9639 if (change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue, &LOCK_log, &LOCK_sync))
- 9640 {
- 9641 DBUG_PRINT("return", ("Thread ID: %u, commit_error: %d",
- 9642 thd->thread_id(), thd->commit_error));
- 9643 DBUG_RETURN(finish_commit(thd));
- 9644 }
- ... ...
- 9661 if (flush_error == 0 && total_bytes> 0)
- 9662 {
- 9663 DEBUG_SYNC(thd, "before_sync_binlog_file");
- 9664 std::pair<bool, bool> result= sync_binlog_file(false);
- 9665 sync_error= result.first;
- 9666 }
- 9667
- ... ...
- 9702 commit_stage:
- 9703 if (opt_binlog_order_commits &&
- 9704 (sync_error == 0 || binlog_error_action != ABORT_SERVER))
- 9705 {
- 9706 if (change_stage(thd, Stage_manager::COMMIT_STAGE,
- 9707 final_queue, leave_mutex_before_commit_stage,
- 9708 &LOCK_commit))
- ... ...
- 9736 process_commit_stage_queue(thd, commit_queue);
- 9737 mysql_mutex_unlock(&LOCK_commit);
- ... ...
- 9759 /* Commit done so signal all waiting threads */
- 9760 stage_manager.signal_done(final_queue);
- ... ...
- }
如源码所示, 在 commit 阶段会调用 change_stage 函数 3 次, 分别传入不同的参数 FLUSH_STAGE,SYNC_STAGE 和 COMMIT_STAGE.change_stage 主要用于事务加入队列. 在代码中有一个值得注意的地方是在 9655 行中, sync 落盘缓存之前会等到 binlog_group_commit_sync_delay 毫秒或收集到 binlog_group_commit_sync_no_delay_count 个事务之后再 sync.
3.2 change_stage 和 enroll_for
change_stage 函数主要的作用是将当期事务加入对应的队列中, 并返回这个事务是否成为 leader. 函数关键代码如下所示:
- 9140 bool
- 9141 MYSQL_BIN_LOG::change_stage(THD *thd,
- 9142 Stage_manager::StageID stage, THD *queue,
- 9143 mysql_mutex_t *leave_mutex,
- 9144 mysql_mutex_t *enter_mutex)
- 9145 {
- ... ...
- 9156 if (!stage_manager.enroll_for(stage, queue, leave_mutex))
- 9157 {
- 9158 DBUG_ASSERT(!thd_get_cache_mngr(thd)->dbug_any_finalized());
- 9159 DBUG_RETURN(true);
- 9160 }
- ... ...
- }
在 change_stage 函数中主要调用了 enroll_for 函数进行注册, enroll_for 函数关键代码如下:
- bool
- Stage_manager::enroll_for(StageID stage, THD *thd, mysql_mutex_t *stage_mutex)
- {
- // If the queue was empty: we're the leader for this batch
- DBUG_PRINT("debug", ("Enqueue 0x%llx to queue for stage %d",
- (ulonglong) thd, stage));
- bool leader= m_queue[stage].append(thd);
- ... ...
- if (!leader)
- {
- mysql_mutex_lock(&m_lock_done);
- #ifndef DBUG_OFF
- /*
- Leader can be awaiting all-clear to preempt follower's execution.
- With setting the status the follower ensures it won't execute anything
- including thread-specific code.
- */
- thd->get_transaction()->m_flags.ready_preempt= 1;
- if (leader_await_preempt_status)
- mysql_cond_signal(&m_cond_preempt);
- #endif
- while (thd->get_transaction()->m_flags.pending)
- mysql_cond_wait(&m_cond_done, &m_lock_done);
- mysql_mutex_unlock(&m_lock_done);
- }
- return leader;
- }
在代码中可以看出在接入对应的队列后, 如果发现当前事务不能成为 leader 则会在后续调用条件变量进行休眠. 当 order_commit 函数中, leader 完成了所有的任务, 则在 9760 行使用条件变量唤醒其他 Follower 进程. follower 进程会调用 DBUG_RETURN(finish_commit(thd))完成 commit 并退出函数.
4. 小结
本文主要介绍了关于 binlog 组提交的逻辑. 限于本文的作者水平有限, 文中的错误在所难免, 恳请大家批评指正.
来源: https://www.cnblogs.com/bush2582/p/11338455.html