MySQL 核心模塊揭祕 | 07 期 | 二階段提交 (1) prepare 階段

二階段提交的 prepare 階段,binlog 和 InnoDB 各自會有哪些動作?

本文基於 MySQL 8.0.32 源碼,存儲引擎爲 InnoDB。

1. 二階段提交

二階段提交,顧名思義,包含兩個階段,它們是:

  • prepare 階段。
  • commit 階段。

我們只考慮 SQL 語句操作 InnoDB 表的場景,對於用戶事務,是否使用二階段提交,取決於是否開啓了 binlog。

因爲 MySQL 把 binlog 也看作一個存儲引擎,開啓 binlog,SQL 語句改變(插入、更新、刪除)InnoDB 表的數據,這個 SQL 語句執行過程中,就涉及到兩個存儲引擎。

使用二階段提交,就是爲了保證兩個存儲引擎的數據一致性。

用戶事務提交分爲兩種場景,如果開啓了 binlog,它們都會使用二階段提交。

場景 1:通過 BEGIN 或其它開始事務的語句,顯式開始一個事務,用戶手動執行 COMMIT 語句提交事務。

場景 2:沒有顯式開始的事務,一條 SQL 語句執行時,InnoDB 會隱式開始一個事務,SQL 語句執行完成之後,自動提交事務。

如果沒有開啓 binlog,SQL 語句改變表中數據,不產生 binlog,不用保證 binlog 和表中數據的一致性,用戶事務也就不需要使用二階段提交了。

InnoDB 內部事務是個特例,不管是否開啓了 binlog,改變表中數據都不會產生 binlog 日誌,所以內部事務不需要使用二階段提交。

2. prepare 階段

以下代碼中,ha_prepare_low() 會調用 binlog 和 InnoDB 處理 prepare 邏輯的方法。

int MYSQL_BIN_LOG::prepare(THD *thd, bool all) {
  ...
  thd->durability_property = HA_IGNORE_DURABILITY;
  ...
  int error = ha_prepare_low(thd, all);
  ...
}

調用 ha_prepare_low() 之前,用戶線程對象的 durability_property 屬性值會被設置爲 HA_IGNORE_DURABILITY

這個屬性和 redo 日誌刷盤有關,InnoDB prepare 會用到。

2.1 binlog prepare

binlog 被看作一種存儲引擎,它也有 prepare 階段,代碼如下:

// sql/binlog.cc
static int binlog_prepare(handlerton *, THD *thd, bool all) {
  DBUG_TRACE;
  if (!all) {
    thd->get_transaction()->store_commit_parent(
        mysql_bin_log.m_dependency_tracker.get_max_committed_timestamp());
  }
  return 0;
}

二階段提交時,all = true,不會命中分支 if (!all)。也就是說,在 prepare 階段,binlog 什麼也不會幹。

2.2 InnoDB prepare

二階段提交的 prepare 階段,InnoDB 主要做五件事。

第 1 件,把分配給事務的所有 undo 段的狀態從 TRX_UNDO_ACTIVE 修改爲 TRX_UNDO_PREPARED

進入二階段提交的事務,都至少改變過(插入、更新、刪除)一個用戶表的一條記錄,最少會分配 1 個 undo 段,最多會分配 4 個 undo 段。

具體什麼情況分配多少個 undo 段,後續關於 undo 模塊的文章會有詳細介紹。

不管 InnoDB 給事務分配了幾個 undo 段,它們的狀態都會被修改爲 TRX_UNDO_PREPARED。

第 2 件,把事務 Xid 寫入所有 undo 段中當前提交事務的 undo 日誌組頭信息。

InnoDB 給當前提交事務分配的每個 undo 段中,都會有一組 undo 日誌屬於這個事務,事務 Xid 就寫入 undo 日誌組的頭信息。

對於第 1、2 件事,如果事務改變了用戶普通表的數據,修改 undo 段狀態、把事務 Xid 寫入 undo 日誌組頭信息,都會產生 redo 日誌。

第 3 件,把內存中的事務對象狀態從 TRX_STATE_ACTIVE 修改爲 TRX_STATE_PREPARED

前面修改 undo 狀態,是爲了事務提交完成之前,MySQL 崩潰了,下次啓動時,能夠從 undo 段中恢復崩潰之前的事務狀態。

這裏修改事務對象狀態,用於 MySQL 正常運行過程中,標識事務已經進入二階段提交的 prepare 階段。

第 4 件,如果當前提交事務的隔離級別是讀未提交READ-UNCOMMITTED)或讀已提交READ-COMMITTED),InnoDB 會釋放事務給記錄加的共享、排他 GAP 鎖。

雖然讀未提交、讀已提交隔離級別一般都只加普通記錄鎖,不加 GAP 鎖,但是,外鍵約束檢查、插入記錄重複值檢查這兩個場景下,還是會給相應的記錄加 GAP 鎖。

第 5 件,調用 trx_flush_logs(),處理 redo 日誌刷盤的相關邏輯。

static void trx_flush_logs(trx_t *trx, lsn_t lsn) {
  ...
  switch (thd_requested_durability(trx->mysql_thd)) {
    case HA_IGNORE_DURABILITY:
      /* We set the HA_IGNORE_DURABILITY
      during prepare phase of binlog group commit
      to not flush redo log for every transaction here. 
      So that we can flush prepared records
      of transactions to redo log in a group
      right before writing them to binary log
      during flush stage of binlog group commit. */
      break;
    case HA_REGULAR_DURABILITY:
      ...
      trx_flush_log_if_needed(lsn, trx);
  }
}

從名字上看,trx_flush_logs() 的作用是把事務產生的 redo 日誌刷盤。

前面介紹過,MYSQL_BIN_LOG::prepare() 調用 ha_prepare_low() 之前,就已經把當前事務所屬用戶線程對象的 durability_property 屬性設置爲 HA_IGNORE_DURABILITY 了。

從上面的代碼可以看到,用戶線程對象的 durability_property 屬性值爲 HA_IGNORE_DURABILITY,prepare 階段並不會把 redo 日誌刷盤。

3. 總結

開啓 binlog 的情況下,用戶事務需要使用二階段提交來保證 binlog 和 InnoDB 表的數據一致性。

binlog prepare 什麼也不會幹。

InnoDB prepare 會把分配給事務的所有 undo 段的狀態修改爲 TRX_UNDO_PREPARED,把事務 Xid 寫入 undo 日誌組的頭信息,把內存中事務對象的狀態修改爲 TRX_STATE_PREPARED。

本期問題:二階段提交的 prepare 階段爲什麼不把 redo 日誌刷盤?歡迎大家留言交流。

下期預告:MySQL 核心模塊揭祕 | 08 期 | 二階段提交 (2) commit 階段。

更多技術文章,請訪問:https://opensource.actionsky.com/

關於 SQLE

SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章