数据库内核月报

数据库内核月报 - 2026 / 07

浅析 mysql binlog 提交流程

Author: 晔之

背景介绍

binlog介绍

binlog作用

binlog又叫归档日志,可以用于复制,订阅,数据库的增量备份和恢复等。

crash safe

数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe的能力,binlog 日志只能用于归档。InnoDB存储引擎提供了crash-safe能力,具体做法是引入了另一套日志系统redo log。

redo log介绍

redo log是一种物理日志,所谓物理日志就是日志记录的是数据页变更 。redo记录了数据库的各种操作,对数据页的更改。

WAL

事务提交时,先写redo log再修改数据页;当由于发生宕机而导致数据丢失时,就可以通过重做日志来完成数据页的恢复。

优势

如果没有redo log,每次更新操作,磁盘要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。有了redo log,每次更新的时候,只要写redo log,更新内存即可,在系统空闲的时候再写磁盘。随机写变成了顺序写,磁盘顺序写要快于随机写。

两阶段提交

两阶段提交是用来保证redo log和binlog的一致性,如果redo log和binlog 不一致,会造成主备不一致。

redo log和binlog的写入机制

这两个日志文件大概有三种状态,在MySQL进程内存中,在文件系统的 page cache中,和在磁盘上。commit时,需要对这两个日志文件进行持久化(这个持久化,不一定是指写到磁盘上,binlog受参数sync_binlog控制,redo log受参数innodb_flush_log_at_trx_commit控制)。

binlog的写入机制

image

事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

由上图可见每个线程THD拥有一个binlog cache(即一片内存空间),但是共用一份binlog文件。由于每个事务的 binlog 不能被拆开,因此不论这个事务多大,也要确保一次性写入。所以都往内存中写是不现实的,因此binlog cache分为两块,一块儿是memory(内存),一块是tmp file(是一个临时磁盘文件)

binlog_cache_manager->Binlog_cache_storage->IO_CACHE_binlog_cache_storage->IO_CACHE(binlog cache由 IO_CACHE实现)

commit时

write(写到文件系统的page cache中,也被称为flush,redo log中相应操作,叫法不同) 和 fsync (写到磁盘上)的时机,是由参数 sync_binlog 控制的:

redo log写入机制

image

redo log的三种状态:

commit时

为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:

双1配置,即innodb_flush_log_at_trx_commit=1&&sync_binlog=1,也就是每次提交时,binlog和redo log都写盘,这样可以达到数据和日志完全一致的高可靠性,以下讨论基于双1配置。

两阶段提交介绍

image

Prepare

Prepare 阶段最主要的动作是将事务从 active 状态置为 prepared 状态,并将 undo 回滚段也置为 prepared。这个动作的意义是,标记事务所有操作都已经结束,在 crash recovery 过程中,处在 prepared 状态的事务是可以回滚也可以提交的(active 状态事务只能回滚不能提交)。

Commit

Commit 阶段除了事务提交的操作外,还包括 redo log 的刷盘,binlog 的写入和刷盘。这里对这两个日志文件的操作顺序很重要。

Binlog 刷盘,就意味着事务已经可以被传给从库,此时就算实例没有完成 commit 就 crash 了,crash recovery 过程中也需要能够提交此事务,这样才不会造成主从不一致。crash-recovery是通过redo log保证的,因此必须先将 redo log 刷盘,才可以写 binlog 文件并刷盘。因此,在提交阶段,以 binlog 刷盘完成为标志,刷盘完成前发生 crash,事务回滚,刷盘完成后发生 crash,事务提交。

在完成 binlog 刷盘后,事务就可以提交。提交时会把事务从 prepared 状态改为 commit 状态,这个操作是需要写 redo log 的,但是事务完成提交并不需要等待这个 redo log 刷盘。刚才说过,只要 binlog 刷盘完成,就可以认为这个事务已经持久化的提交了,因此不必再等待一次 redo log 刷盘,直接返回事务提交成功即可。

各时间点 Crash 分析

image

组提交

引入两阶段提交以后,在prepare阶段,redo log要进行一次写盘,在sync binlog阶段,binlog也要进行一次写盘,为了降低写磁盘的次数,引入了组提交机制组提交就是将多个刷盘操作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。

核心思路

提交过程分成三个阶段:Flush 阶段做redo log的持久化(redo log的组提交)和binlog的write操作,Sync 阶段调用 fsync 操作持久化binlog,Commit 阶段调用存储引擎接口提交事务。每个阶段都有一个队列,每个队列都有一把锁保护,第一个进入队列的事务会成为leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。各个阶段的leader,在等下一个阶段的leader释放锁的过程中,本队列中事务不断增加,leader全权负责整队的操作,这样就可以把多个事务的多次fsync合并成1次进行。

image

image

下文将对prepare,commit过程中FLUSH阶段,SYN阶段,COMMIT阶段以及commit完成后的收尾工作做一些介绍,着重介绍这五个阶段中对binlog的操作。

Binlog组提交

代码路径

-mysql_execute_command
--trans_commit //normal事务提交
--trans_commit_stmt //单语句事务提交
---ha_commit_trans 
----MYSQL_BIN_LOG::prepare
-----ha_prepare_low
------binlog_prepare
------innobase_xa_prepare
----MYSQL_BIN_LOG::commit
-----MYSQL_BIN_LOG::ordered_commit //组提交
-----ha_commit_low //引擎层提交

prepare

 /*
        Acquire a metadata lock which will ensure that COMMIT is blocked
        by an active FLUSH TABLES WITH READ LOCK (and vice versa:
        COMMIT in progress blocks FTWRL).

        We allow the owner of FTWRL to COMMIT; we assume that it knows
        what it does.
      */
      MDL_REQUEST_INIT(&mdl_request, MDL_key::COMMIT, "", "",
                       MDL_INTENTION_EXCLUSIVE, MDL_EXPLICIT);

      DBUG_PRINT("debug", ("Acquire MDL commit lock"));
      if (thd->mdl_context.acquire_lock(&mdl_request,
                                        thd->variables.lock_wait_timeout)) {
        ha_rollback_trans(thd, all);
        return 1;
      }
 if (!all) {
    thd->get_transaction()->store_commit_parent(
        mysql_bin_log.m_dependency_tracker.get_max_committed_timestamp());
  }

-innobase_xa_prepare
--trx_prepare_for_mysql
---trx_undo_gtid_add_update_undo //为存储gtid分配undo segment
----trx_prepare
-----trx_prepare_low //更新undo状态,TRX_UNDO_ACTIVE->TRX_UNDO_PREPARED
 else if (real_trans && xid && trn_ctx->rw_ha_count(trx_scope) > 1 &&
             !trn_ctx->no_2pc(trx_scope)) {
      Xid_log_event end_evt(thd, xid);
      if (cache_mngr->trx_cache.finalize(thd, &end_evt)) return RESULT_ABORTED;
    }

下面三个阶段的主要逻辑就在MYSQL_BIN_LOG::ordered_commit函数中,在MySQL中每个阶段都有一个队列,每个队列都有一把锁保护,第一个进入队列的事务会成为leader,leader领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。

-MYSQL_BIN_LOG::ordered_commit
--change_stage //进入一个基本阶段
---Commit_stage_manager::enroll_for //具体的入队操作在此函数中进行,leader线程会释放上一阶段的锁,获得本阶段相应的锁,直接返回;follower线程会等待leader线程唤醒,然后做一些收尾工作finish_commit

下文FLUSH,SYNC和COMMIT中,形成队列,获取该阶段lock,释放上一阶段lock都是在change_stage中进行。change_stage 的流程是:线程先入队,再释放上一阶段的 lock,最后申请下一阶段的 lock。这样保证了每个时刻,每个 stage 都只有一个线程在执行,从而保证了线程的顺序性。反之,如果先释放上一个 stage lock,再申请入队,后面的线程就可能赶上来,同时申请入队,从而无法保证顺序性。

FLUSH

这块逻辑主要在MYSQL_BIN_LOG::process_flush_stage_queue中

int MYSQL_BIN_LOG::process_flush_stage_queue(my_off_t *total_bytes_var,
                                             bool *rotate_var,
                                             THD **out_queue_var) {
 ...
  /*
    Fetch the entire flush queue and empty it, so that the next batch
    has a leader. We must do this before invoking ha_flush_logs(...)
    for guaranteeing to flush prepared records of transactions before
    flushing them to binary log, which is required by crash recovery.
  */
  // 清空flush queue,redo log的组提交,这里把flush阶段的线程队列串成了一个链表,链表头是leader线程
  THD *first_seen = fetch_and_process_flush_stage_queue();

  ...
    //分配gtid
  assign_automatic_gtids_to_flush_group(first_seen);
  /* Flush thread caches to binary log. */
  for (THD *head = first_seen; head; head = head->next_to_commit) {
    //把current_thd切换成head
    Thd_backup_and_restore switch_thd(current_thd, head);
    /*-MYSQL_BIN_LOG::flush_thread_caches 增加全局transaction_counter->xid
      --binlog_cache_mngr::flush
	  ---binlog_cache_data::flush 获得sequence number
	  ----MYSQL_BIN_LOG::write_transaction 
      -----Gtid_log_event::write 先把GTID Event写入binlog file
      -----MYSQL_BIN_LOG::write_cache 再写其他Event
    */
    std::pair<int, my_off_t> result = flush_thread_caches(head);
    total_bytes += result.second;
    if (flush_error == 1) flush_error = result.first;
#ifndef NDEBUG
    no_flushes++;
#endif
  }

  *out_queue_var = first_seen;
  *total_bytes_var = total_bytes;
    //确定binlog是否需要rotate
  if (total_bytes > 0 &&
      (m_binlog_file->get_real_file_size() >= (my_off_t)max_size ||
       DBUG_EVALUATE_IF("simulate_max_binlog_size", true, false)))
    *rotate_var = true;
#ifndef NDEBUG
  DBUG_PRINT("info", ("no_flushes:= %d", no_flushes));
  no_flushes = 0;
#endif
  return flush_error;
}

SYNC

//get_sync_period()=sync_binlog 表示几个group(一个thd的链表)提交一次,而不是几个thd提交一次
//对syn_binlog=1和syn_binlog=0立刻等待,对于syn_binlog>1是将要fsync时才等待
if (!flush_error && (sync_counter + 1 >= get_sync_period()))
    //通过等待来增加队列中的事务
    Commit_stage_manager::get_instance().wait_count_or_timeout(
        opt_binlog_group_commit_sync_no_delay_count,
        opt_binlog_group_commit_sync_delay, Commit_stage_manager::SYNC_STAGE);

  final_queue = Commit_stage_manager::get_instance().fetch_queue_acquire_lock(
      Commit_stage_manager::SYNC_STAGE);
//对于syn_binlog>1 积累够一定数目的才fsync,对于syn_binlog=0不fsync
  thd_wait_begin(thd, THD_WAIT_GROUP_COMMIT);
  DEBUG_SYNC(thd, "before_sync_binlog_file");

  if (flush_error == 0 && total_bytes > 0) {
    std::pair<bool, bool> result = sync_binlog_file(false);
    sync_error = result.first;
  }

COMMIT

收尾

// opt_binlog_order_commits决定是否由 leader 一起做 commit
  if ((opt_binlog_order_commits || Clone_handler::need_commit_order()) &&
      (sync_error == 0 || binlog_error_action != ABORT_SERVER)) {
    // 由commit leader对队列中的所有thd进行commit
    if (change_stage(thd, Stage_manager::COMMIT_STAGE, final_queue,
                     leave_mutex_before_commit_stage, &LOCK_commit)) {
      DBUG_PRINT("return", ("Thread ID: %u, commit_error: %d", thd->thread_id(),
                            thd->commit_error));
      return finish_commit(thd);
    }
    THD *commit_queue =
        Commit_stage_manager::get_instance().fetch_queue_acquire_lock(
            Commit_stage_manager::COMMIT_STAGE);
    DBUG_EXECUTE_IF("semi_sync_3-way_deadlock",
                    DEBUG_SYNC(thd, "before_process_commit_stage_queue"););
    
    if (flush_error == 0 && sync_error == 0)
      	sync_error = call_after_sync_hook(commit_queue);

    // 对 queue 中每个线程执行 ha_commit_low,完成事务提交
    process_commit_stage_queue(thd, commit_queue);
    mysql_mutex_unlock(&LOCK_commit); 
    process_after_commit_stage_queue(thd, commit_queue);
    final_queue = commit_queue;    
  } else {
    // 如果不进行 order commit,那么sync leader还没有change stage
    // 需要我们手动释放 sync lock
    if (leave_mutex_before_commit_stage)
      mysql_mutex_unlock(leave_mutex_before_commit_stage);
    if (flush_error == 0 && sync_error == 0)
      sync_error = call_after_sync_hook(final_queue);
  }
  /*
    Handle sync error after we release all locks in order to avoid deadlocks
  */
  if (sync_error)
    handle_binlog_flush_or_sync_error(thd, true /* need_lock_log */, nullptr);

  DEBUG_SYNC(thd, "before_signal_done");
  	// 通知队列中所有等待的线程
	// 通过 thd->tx_commit_pending 标志来通知 thd
    // follower 线程被唤醒后调用 finish_commit
 	// 如果发现事务没有提交,会调用 ha_commit_low, 此时就不能保证 commit 的顺序了。
  Commit_stage_manager::get_instance().signal_done(final_queue);
  //清空binlog cache内存和临时文件,但不释放,保留文件描述符
  (void)finish_commit(thd);
  DEBUG_SYNC(thd, "bgc_after_commit_stage_before_rotation");
  // do_rotate 标志位在 flush 阶段被设置
  if (DBUG_EVALUATE_IF("force_rotate", 1, 0) ||
      (do_rotate && thd->commit_error == THD::CE_NONE &&
       !is_rotating_caused_by_incident)) {
    bool check_purge = false;
    mysql_mutex_lock(&LOCK_log);
    // 进行 binlog rotate 操作
    int error = rotate(false, &check_purge);
    mysql_mutex_unlock(&LOCK_log);
       int error = rotate(false, &check_purge);
    mysql_mutex_unlock(&LOCK_log);

    if (error)
      thd->commit_error = THD::CE_COMMIT_ERROR;
    else if (check_purge)
    //是否清理binlog
      purge();
  }