深度解析MySQL分佈式事務原理

MySQL 分佈式事務調研

調研目標

  • 1、xa事務的完成執行過程,及每個命令mysql側對應的處理源碼。
  • 2、給出源碼級5.7與5.6 xa事務的改進點。
  • 3、給出xa事務相關的狀態查詢命令
  • 4、給出replication下如何處理xa事務的過程分析(源碼級)
  • 5、驗證各種邊界場景下xa事務的執行結果(xid 衝突,重用,故障切換)

1. 分佈式事務處理標準Open/X-The XA Specification

X/OPEN組織所定義的DTP模型,通過抽象出來的AP, TM, RM的概念可以保證事務的強一致性,

X/Open DTP中的角色

  • AP(Application Program):應用程序,主要是定義事務邊界以及那些組成事務的特定於應用程序的操作。

  • RM(Resouces Manager):資源管理器,管理一些共享資源的自治域,如提供對諸如數據庫之類的共享資源的訪問。

  • TM(Transaction Manager):事務管理器,管理全局事務,協調事務的提交或者回滾,並協調故障恢復。

在理解MySQLXA事務的工作原理時,需要將X/Open DTP模型中的各個組件映射到相關的MySQL組件中,分爲如下兩種情況:

  • MySQL爲了提高自身的高可用性和複製過程中的數據一致性,引入binlog存儲引擎用於操作日誌的存儲和傳輸,並以此來構建多數據副本,參與事務過程的存儲引擎包括了binlog,innobase,無論是普通事務操作還是XA事務操作,在MySQL內部都是一個分佈式事務的處理過程。不同點在於MySQL XA事務的處理過程中將commit(2pc)顯示的分爲XA prepare 和XA commit兩個過程來處理(細節稍有不同),以此來適配外部全局分佈式事務。DTP與MySQL內部XA事務的對應關係如下:

  • 在全局分佈式事務中,MySQL作爲RM存在,一般實現的DTP模型如下:

本文主要描述第一種情況,即MySQL XA事務內部處理過程。

2. MySQL XA事務相關源碼解析

2.1 XA相關接口實現

這部分代碼在xa.cc/h文件中。

2.1.1 對應6個命令操作(xa start/begin,end,prepare,commit,rollback,recover)的類

  • Sql_cmd_xa_start
  • Sql_cmd_xa_end
  • Sql_cmd_xa_prepare
  • Sql_cmd_xa_commit
  • Sql_cmd_xa_rollback
  • Sql_cmd_xa_recover

如上六個類是完成XA事務命令的操作入口,是對接口Sql_cmd的具體實現。

2.1.2 XA事務狀態類XID_STATE

主要包含如下數據

  • xa_states xa_state
    表示事務狀態,有如下類型
 enum xa_states {XA_NOTR=0, XA_ACTIVE, XA_IDLE, XA_PREPARED, XA_ROLLBACK_ONLY};
  • static const char *xa_state_names[]
    對應5種狀態的字符串(NON-EXISTING,ACTIVE,IDLE,PREPARED,ROLLBACK ONLY),在返回錯誤時,方便客戶端閱讀。

  • XID m_xid //XA事務的唯一標示

  • bool in_recovery //recovery的狀態標示

  • uint rm_error //資源管理器RM通過此數據向事務管理器TM彙報錯誤。

  • bool m_is_binlogged //標示XA事務的二進制日誌記錄情況。在恢復階段需要用到.

XA事務的狀態流轉受到參數innodb_rollback_on_timeout的影響,如下:
XA事務狀態輪轉圖

2.1.3 xa_option_words

表示xa命令的可選項,目前xa start/end/prepare都是不支持可選項的,只有xa commit支持ONE PHASE。

enum xa_option_words {XA_NONE, XA_JOIN, XA_RESUME, XA_ONE_PHASE,
                      XA_SUSPEND, XA_FOR_MIGRATE};

2.1.4 xid_t

標示一個唯一的XA事務。

typedef struct xid_t XID

定義了XID的數據格式,主要成員如下:

 /**
    -1 means that the XID is null
  */
  long formatID; 

  /**
    value from 1 through 64
  */
  long gtrid_length;

  /**
    value from 1 through 64
  */
  long bqual_length;

  /**
    distributed trx identifier. not \0-terminated.
  */
  char data[XIDDATASIZE]; 

2.1.5 其它相關類

class THD
class Transaction_ctx
... //太多,不一一列舉了

2.2 MySQL XA事務操作對應的源碼分析

先看下XA事務操作的時序圖(忽略了大部分RM內部處理細節),本節下面的內容會根據時序圖進行分段說明:





本章節會按照此圖分段說明XA事務操作的過程。

2.2.1 xa start操作詳解

xa start命令主要是啓動一個XA事務,並且把事務狀態從XA_NOTR設置爲XA_ACTIVE狀態。

xa start階段的函數調用關係如下:

mysql_execute_command(THD*, bool)
    Sql_cmd_xa_start::execute(THD*) //Sql_cmd::execute的接口實現
        Sql_cmd_xa_start::trans_xa_start(THD*) //xa開啓過程主要邏輯
            trans_begin(THD*, unsigned int) //開啓事務
            xid_state->start_normal_xa(m_xid); //開啓XA事務,設置xid_state中的成員變量
            transaction_cache_insert(xid_t*, Transaction_ctx*) //XA事務緩存插入
                hash_search //查重,mutex保護
                hash_insert //插入,mutex保護

--------------------下面是對這個過程進行分段說明------------------------

在開啓XA事務前,會做如下檢測,對應時序圖中的步驟3

  • 如果XA {START|BEGIN} xid [JOIN|RESUME]最後的選項不爲XA_NONE,則報錯XA選項錯誤,代碼如下:
  /* TODO: JOIN is not supported yet. */
  if (m_xa_opt != XA_NONE)
    my_error(ER_XAER_INVAL, MYF(0));

報錯內容如下:

mysql> xa start 'trx1' join;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
mysql> xa start 'trx1' resume;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
  • 如果通過XA命令開啓事務時,當前XA事務狀態不爲XA_NOTR,則報錯,代碼如下:
else if (!xid_state->has_state(XID_STATE::XA_NOTR))
    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());

報錯內容如下:

//具體報錯狀態,依賴於當前線程的所處的事務狀態。但是錯誤號是一致的。#define ER_XAER_RMFAIL 1399
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the  ACTIVE state
  • 如果通過XA start啓動XA事務時,事務處於活躍的非XA事務上下文中,則會報錯,代碼如下:
 else if (thd->locked_tables_mode || thd->in_active_multi_stmt_transaction())
    my_error(ER_XAER_OUTSIDE, MYF(0));

報錯內容下:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1(name) values('aaa');
Query OK, 1 row affected (0.00 sec)

//#define ER_XAER_OUTSIDE 1400
mysql> xa start 'trx1';
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction

通過調用XID_STATE::start_normal_xa(xid_t const*)開啓xa事務,主要是設置xid_state中的成員變量,對應時序圖中的步驟5,如下

  void start_normal_xa(const XID *xid)
  {
    DBUG_ASSERT(m_xid.is_null());
    xa_state= XA_ACTIVE; //狀態變更
    m_xid.set(xid); //設置xid
    in_recovery= false; //recovery狀態置爲false
    rm_error= 0; //資源管理器錯誤置0
  }

通過調用transaction_cache_insert,插入到事務hash緩存表中。對應時序圖中的步驟6
在transaction_cache_insert處理過程中,如果存在重複,則報錯,檢測邏輯如下:

bool transaction_cache_insert(XID *xid, Transaction_ctx *transaction)
{
  mysql_mutex_lock(&LOCK_transaction_cache);//加鎖
  if (my_hash_search(&transaction_cache, xid->key(),
                     xid->key_length())) //搜索
  {
    mysql_mutex_unlock(&LOCK_transaction_cache);//衝突,解鎖
    my_error(ER_XAER_DUPID, MYF(0));//衝突,報錯
    return true;
  }
  bool res= my_hash_insert(&transaction_cache,//無衝突,插入 (uchar*)transaction);  
  mysql_mutex_unlock(&LOCK_transaction_cache);//解鎖
  return res;
}

報錯內容如下:

mysql> xa start 'trx1';
ERROR 1440 (XAE08): XAER_DUPID: The XID already exists

trans_xa_start完成後,需要處理作爲slave實例的相關邏輯

bool Sql_cmd_xa_start::execute(THD *thd)
{
  bool st= trans_xa_start(thd);

  if (!st)
  {
    thd->rpl_detach_engine_ha_data();
    my_ok(thd);
  }

  return st;
}

2.2.2 XA事務數據更新

在AP操作RMS進行數據寫入前,RMS需要向TM註冊,見時序圖中的步驟12,19,並且後註冊的RM在prepare階段會先被調用。

這裏只說明一點:
XA事務中,AP直接訪問RM去進行數據更新,在數據更新失敗後(RM-innobase),如果innodb參數innodb_rollback_on_timeout爲on,則會更新xa_state中的rm_error,用來通知TM發生錯誤。

2.2.3 xa end操作詳解

xa end命令主要是結束一個xa事務操作,並將xa事務狀態從XA_ACTIVE置爲XA_IDLE,或者是XA_ROLLBACK_ONLY,這取決於Innodb參數設置innodb_rollback_on_timeout,在開啓innodb_rollback_on_timeout時,如果事務操作(dml)出現鎖超時,死鎖等情況,xa end操作將會將XA事務狀態置爲XA_ROLLBACK_ONLY。

XA end操作的函數調用關係

..........
mysql_execute_command(THD*, bool)
    Sql_cmd_xa_end::execute(THD*)
        Sql_cmd_xa_end::trans_xa_end(THD*)
        check...//各種異常檢測
            xid_state->set_state(XID_STATE::XA_IDLE);

正常情況下,xa end是將XA事務狀態從從XA_ACTIVE置爲XA_IDLE。下面討論對於異常的處理情況,對應時序圖中的步驟26

  • xa end命令不可以附加任何選項,否則報錯,代碼如下:
 if (m_xa_opt != XA_NONE)
    my_error(ER_XAER_INVAL, MYF(0));

客戶端收到報錯如下:

mysql> xa end 'trx1' SUSPEND;
ERROR 1398 (XAE05): XAER_INVAL: Invalid arguments (or unsupported command)
  • 執行xa end命令時,xa事務狀態必須爲XA_ACTIVE,否則報錯,代碼如下
else if (!xid_state->has_state(XID_STATE::XA_ACTIVE))
    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());

客戶端收到報錯如下:


mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the  IDLE state
mysql> xa prepare 'trx1';
Query OK, 0 rows affected (0.02 sec)
mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the  PREPARED state
mysql> xa commit 'trx1';
Query OK, 0 rows affected (0.02 sec)

mysql> xa end 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the  NON-EXISTING state
  • 檢測用戶輸入的xid是否爲當前事務上下文中的xid,如果不匹配,報錯。代碼如下:
else if (!xid_state->has_same_xid(m_xid))
    my_error(ER_XAER_NOTA, MYF(0));

客戶端收到報錯如下:

mysql> xa start 'trx1';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1(name) values('aa');
Query OK, 1 row affected (0.00 sec)

mysql> xa end 'trx2';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID
  • 檢測事務是否被標誌爲回滾,如果是的話,需要更新XA事務狀態爲XA_ROLLBACK_ONLY。代碼如下:
bool XID_STATE::xa_trans_rolled_back()
{
  DBUG_EXECUTE_IF("simulate_xa_rm_error", rm_error= true;);
  if (rm_error)
  {
    switch (rm_error) //根據rm_error判斷RM錯誤類型
    {
    case ER_LOCK_WAIT_TIMEOUT: //RM發生鎖超時
      my_error(ER_XA_RBTIMEOUT, MYF(0));
      break;
    case ER_LOCK_DEADLOCK:  //RM發生死鎖
      my_error(ER_XA_RBDEADLOCK, MYF(0));
      break;
    default:
      my_error(ER_XA_RBROLLBACK, MYF(0));
    }
    xa_state= XID_STATE::XA_ROLLBACK_ONLY;
  }

  return (xa_state == XID_STATE::XA_ROLLBACK_ONLY);
}

此時客戶端會收到報錯,並將XA事務狀態設置爲XA_ROLLBACK_ONLY,事務無法提交,只能回滾,符合參數innodb_rollback_on_timeout設計初衷。客戶端報錯內容如下:

mysql> xa start 'trx1';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1(name) values('hhh');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t1(name) values('hhh');
Query OK, 1 row affected (0.01 sec)

mysql> insert into t1(name) values('hhh');
Query OK, 1 row affected (0.02 sec)

mysql> xa end 'trx1';
ERROR 1613 (XA106): XA_RBTIMEOUT: Transaction branch was rolled back: took too long

2.2.4 xa prepare操作詳解

在說明XA事務的prepare操作之前,先來看下,普通事務兩階段提交過程,如下圖所示:


xa prepare命令的主要作用是將事務設置爲XA_PREPARE狀態,包含xa prepare信息的binlog日誌的落盤,innodb日誌刷盤(不包含事務prepare標誌),innodb中事務prepare,所以這裏就存在一個問題,prepare完成後,innodb中事務prepare信息沒有落盤。

XA prepare主要的函數調用關係如下:

mysql_execute_command(THD*, bool)
    Sql_cmd_xa_prepare::execute(THD*)
        Sql_cmd_xa_prepare::trans_xa_prepare(THD*)
            check //異常情況檢測
            ha_prepare(THD*) //進入XA事務的prepare階段
            xid_state->set_state(XID_STATE::XA_PREPARED); //設置XA事務狀態

異常情況檢測,對應時序圖中的步驟30

  • 判斷XA事務狀態,如果不是XA_IDLE,則報錯
if (!xid_state->has_state(XID_STATE::XA_IDLE))
    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());

客戶端收到報錯如下:

mysql> xa start  'trx1';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'trx1';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the  ACTIVE state
  • 檢測用戶輸入Xid和當前事務上下文中的Xid是否匹配,如果不匹配,報錯,代碼如下
else if (!xid_state->has_same_xid(m_xid))
    my_error(ER_XAER_NOTA, MYF(0));

客戶端收到報錯如下:

mysql> xa end 'trx1';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'trx2';
ERROR 1397 (XAE04): XAER_NOTA: Unknown XID

XA事務prepare階段詳解,此處重點突出與非XA事務提交過程中的區別和關係。

XA事務中prepare操作,通過ha_prepare直接調用RM(binlog,innobase)的prepare接口,僞代碼如下:

 while (ha_info)
    {
      handlerton *ht= ha_info->ht();
      thd->status_var.ha_prepare_count++;
      if (ht->prepare)
      {
        if (ht->prepare(ht, thd, true)) //調用RM的prepare函數
        {
          ha_rollback_trans(thd, true);
          error=1;
          break;
        }
      }
      else
      {
        push_warning_printf(thd, Sql_condition::SL_WARNING,
                            ER_ILLEGAL_HA, ER(ER_ILLEGAL_HA),
                            ha_resolve_storage_engine_name(ht));
      }
      ha_info= ha_info->next();
    }

此時會先調用binlog_prepare(和註冊先後順序有關,後註冊的先調用),對應時序圖中的步驟32,binlog_prepare的具體實現如下:

static int binlog_prepare(handlerton *hton, THD *thd, bool all)
{
  DBUG_ENTER("binlog_prepare");
  if (!all) //此處是XA事務的prepare階段,無需更新此事務的last_committed,此值和MySQL多線程回放相關,不做過多說明。
  {
    Logical_clock& clock= mysql_bin_log.max_committed_transaction;
    thd->get_transaction()->
      store_commit_parent(clock.get_timestamp());
  }

  DBUG_RETURN(all && is_loggable_xa_prepare(thd) ?
              mysql_bin_log.commit(thd, true) : 0); //在XA事務中會調用mysql_bin_log.commit(thd, true),普通事務直接退出。
}

調用mysql_bin_log.commit(thd, true)相當於直接進入了2pc的第二個階段,也就是commit階段,對應時序圖中的步驟33,這是和非XA事務進入此階段的處理是不同的,僞代碼&&分析如下:

TC_LOG::enum_result MYSQL_BIN_LOG::commit(THD *thd, bool all)
{
    binlog_cache_mngr *cache_mngr= thd_get_cache_mngr(thd);
    Transaction_ctx *trn_ctx= thd->get_transaction();
    my_xid xid= trn_ctx->xid_state()->get_xid()->get_my_xid();
    bool skip_commit= is_loggable_xa_prepare(thd);//此處skip_commit爲true,ordered_commit函數中會利用這個變量跳過關於innodb層的提交處理,因爲xa prepare過程是不能在innobase中進行事務提交的。
    XID_STATE *xs= thd->get_transaction()->xid_state();
    err= cache_mngr->trx_cache.finalize(thd, &end_evt, xs);//寫入xa end/prepare log event 
    ordered_commit(thd, all, skip_commit)//進入order_commit,2pc的commit階段,非XA事務不會進入到這個邏輯。
}

隨後進入MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)的處理,對應時序圖中的步驟35-48之間。這部分邏輯主要分爲三個部分,flush,sync,commit,僞代碼如下:

MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)
{
    flush()
    {
        innobase_flush_logs(); //innobase事務日誌刷盤,步驟36
        flush_binlog_cache(); //binlog 緩存刷新到文件,步驟38
        semisync_after_flush_hook();//
    }
    sync()
    {
        sync_binlog();        //binlog 落盤,步驟41
        update_binlog_end_pos(); //通知dump線程發送binlog,步驟42,此時binlog日誌將開始發送給slave實例。
       
    }
    commit()
    {
        semisync_after_sync_hook(); //調用半同步插件after_sync
       // binlog->commit(); //什麼都不做
        //innobase_commit();//跳過.
        semisync_after_commit_hook(); //調用半同步插件after_commit
    }
}

再調用innobase_xa_prepare(handlerton*, THD*, bool),更新事務狀態。這裏需要注意的是,innobase沒有把事務日誌持久化(歷史原因,爲了innobase 事務日誌的組提交),對應時序圖步驟49-52。

在RMS prepare結束後,設置XA事務狀態爲XA_PREPARED,對應步驟54,然後XA prepare階段結束。

2.2.5 xa commit操作詳解

xa commit命令主要完成XA事務最後的提交動作,分爲commit當前事務和commit其它事務。
主要函數調用關係如下:

mysql_execute_command(THD*, bool)
    Sql_cmd_xa_commit::execute(THD*)
        Sql_cmd_xa_commit::trans_xa_commit(THD*)
        {
            if (!xid_state->has_same_xid(m_xid))//commit非此事務上下文的XA事務
            {
                //檢測當前事務狀態,如果不是初始化狀態,則報錯
                if (!xid_state->has_state(XID_STATE::XA_NOTR))
                {
                    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());

                    DBUG_RETURN(true);
                }
                Transaction_ctx *transaction= transaction_cache_search(m_xid);//事務緩存hash表中搜索
                XID_STATE *xs= (transaction ? transaction->xid_state() : NULL); //獲取目標事務的狀態信息
                res= !xs || !xs->is_in_recovery(); //獲取recovery狀態
                if (res)  //不存在或者in_recovery爲false,報錯.
                {
                     my_error(ER_XAER_NOTA, MYF(0));
                    DBUG_RETURN(true);
                }
                ha_commit_or_rollback_by_xid(thd, m_xid, !res);
                {
                    innobase_commit_by_xid(handlerton*, xid_t*)//innodb層事務提交。
                    do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //寫Query_log_event(XA COMMIT X'74727831',X'',1)信息
                    MYSQL_BIN_LOG::commit(THD*, bool)
                        MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)//flush,sync,commit。  其中commit階段會省略innodb層的事務提交.
                }
                
            }
            //以下邏輯爲提交當前事務.
            tc_log->commit();//通過事務協調器進行事務提交操作。
                MYSQL_BIN_LOG::commit(THD *thd, bool all)//協調器提交
                do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //寫Query_log_event(XA COMMIT X'74727831',X'',1)信息
                MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool)//flush,sync,commit (包括半同步相關的插件調用)
        }
        

ordered_commit的過程和prepare階段類似,主要不同點如下:

  • 此時強制innodb 事務日誌(prepared標誌)落盤
  • 會調用innodb的事務提交邏輯,釋放鎖等資源。

提交完成後,會從事務緩存hash表中,將此事務刪除,對應時序圖中的步驟82。以及XA 事務狀態設置爲初始化XA_NOTR。

2.2.6 xa recover操作詳解

xa recover的主要命令是查看處於XA_PREPARED狀態的所有XA事務。這個命令的處理過程比較簡單,主要函數調用關係:

mysql_execute_command(THD*, bool)
   Sql_cmd_xa_recover::execute(THD*)
        Sql_cmd_xa_recover::trans_xa_recover(THD*)
            xs->store_xid_info(protocol, m_print_xid_as_hex);//遍歷transaction_cache哈希表,進行保存。

無論事務是否處於in_recovery狀態,都會被列出來。

2.2.7 xa rollback操作詳解

xa rollback的主要作用是對XA事務進行回滾,包括處於XA_IDLE/XA_PREPARED/XA_ROLLBACK_ONLY三個狀態的XA事務。時序圖中沒有畫出回滾的邏輯。
主要函數調用關係

mysql_execute_command(THD*, bool)
    Sql_cmd_xa_rollback::execute(THD*)
        Sql_cmd_xa_rollback::trans_xa_rollback(THD*)
             if (!xid_state->has_same_xid(m_xid))
             {
                 //處理非當前事務上下文的XA事務
             }
             //回滾當前事務上下文的事務
             check//狀態檢測
             xa_trans_force_rollback(THD*)
                ha_rollback_trans(THD*, bool)

XA rollback大體上分爲回滾當前事務,和回滾其它事務。如下圖:

2.2.7.1 回滾當前事務邏輯

函數調用關係以及解析如下:

Sql_cmd_xa_rollback::trans_xa_rollback(THD*)
    xa_trans_force_rollback(THD*)
        error= tc_log->rollback(thd, all);
            MYSQL_BIN_LOG::rollback(THD*, bool)
                do_binlog_xa_commit_rollback(THD*, xid_t*, bool)
                {
                    if (!xid_state->is_binlogged())
                        return 0; //如果無binlog寫入,所以回滾的時候也不需要對binlog進行處理。一般是XA_IDLE狀態的事務。
                    //如下爲對處於XA_PREPARED狀態的事務回滾操作,處於XA_PREPARED狀態的事務binlog日誌可能已經發送給了slave,所以需要寫入rollback 信息通知slave進行事務回滾。
                    char buf[XID::ser_buf_size];
                    char query[(sizeof("XA ROLLBACK")) + 1 + sizeof(buf)];
                    int qlen= sprintf(query, "XA %s %s", commit ? "COMMIT" : "ROLLBACK",
                    xid->serialize(buf));
                    Query_log_event qinfo(thd, query, qlen, false, true, true, 0, false);
                    return mysql_bin_log.write_event(&qinfo);
                }
                if(新寫入了binlog日誌) //對應XA_PREPARED狀態的事務回滾
                {
                     error= ordered_commit(thd, all, /* skip_commit */ true); //通過ordered_commit進行binlog的提交,通知dump線程發送等。
                }
                ha_rollback_low(THD*, bool) //通過handlerton functions去調用回滾,binlog端應該不會做什麼了
                {
                    binlog_rollback(handlerton*, THD*, bool) //進入binlog rollback的實現
                    {
                        int error= 0;
                        if (thd->lex->sql_command == SQLCOM_ROLLBACK_TO_SAVEPOINT)
                            error= mysql_bin_log.rollback(thd, all);
                        DBUG_RETURN(error);
                    }
                    innobase_rollback(handlerton*, THD*, bool)//進入innobase的rollback實現
                    {
                        trx_rollback_for_mysql(trx_t*)
                            trx_rollback_low(trx_t*) //innobase內部事務回滾
                    }
                }
cleanup_trans_state(THD*)
    transaction_cache_delete(Transaction_ctx*) //從事務緩存hash表中刪除
2.2.7.2 回滾其它事務邏輯

回滾非當前XA事務的邏輯如下:

mysql_execute_command(THD*, bool)
    Sql_cmd_xa_rollback::execute(THD*)
        Sql_cmd_xa_rollback::trans_xa_rollback(THD*)
        {
             XID_STATE *xid_state= thd->get_transaction()->xid_state();
             if (!xid_state->has_same_xid(m_xid)) //回滾的是非當前事務
             {
                if (!xid_state->has_state(XID_STATE::XA_NOTR)) //當前事務上下文不爲初始化狀態,則報錯
                {
                    my_error(ER_XAER_RMFAIL, MYF(0), xid_state->state_name());
                    DBUG_RETURN(true);
                }
                if (!xs || !xs->is_in_recin_recoveryoin_recoveryvery())
                { //事務處於非recovery狀態,也就是in_recovery=false,則報錯.
                    my_error(ER_XAER_NOTA, MYF(0));
                    DBUG_RETURN(true);
                }
                ha_commit_or_rollback_by_xid(thd, m_xid, false)
                {
                    xarollback_handlerton(THD*, st_plugin_int**, void*)
                    {
                        innobase_rollback_by_xid(handlerton*, xid_t*)
                            innobase_rollback_trx(trx_t*)//innodb中事務回滾
                        binlog_xa_rollback(handlerton*, xid_t*)
                            binlog_xa_commit_or_rollback(THD*, xid_t*, bool) //binlog 回滾
                            {
                                do_binlog_xa_commit_rollback(THD*, xid_t*, bool) //寫回滾日誌,"XA ROLLBACK X'74727831',X'',1"
                                MYSQL_BIN_LOG::rollback(THD*, bool)
                                {
                                    MYSQL_BIN_LOG::ordered_commit(THD*, bool, bool) //flush/sync/commit
                                }
                            }
                    }
                   
                }

            }
        }
2.2.7.3 XA事務是如何處於in_recovery狀態的:

當處於XA_PREPARED狀態的事務線程退出時,mysqld內部會進行如下操作:

THD::release_resources()
    THD::cleanup()
        //對於處於XA_PREPARED狀態的事務,會進行事務狀態信息的更改,重新插入
        if (trn_ctx->xid_state()->has_state(XID_STATE::XA_PREPARED))
        {
            transaction_cache_detach(trn_ctx);
            my_hash_delete(&transaction_cache, (uchar *)transaction);
            create_and_insert_new_transaction(xid_t*, bool)
            {
                XID_STATE::start_recovery_xa(xid_t const*, bool)
                {
                    xa_state= XA_PREPARED;
                    m_xid.set(xid);
                    in_recovery= true;
                    rm_error= 0;
                    m_is_binlogged= binlogged_arg;
                }
                return my_hash_insert(&transaction_cache, (uchar*)transaction); //重新插入
            }
        }
        else //否則,直接刪除掉
        {
            xs->set_state(XID_STATE::XA_NOTR);
            trans_rollback(this);
            transaction_cache_delete(trn_ctx);
        }
            
                

3. MySQL-5.7對比5.6 XA事務的改進點

關於MySQL-5.6和MySQL-5.7 XA事務對比的信息來源於用戶手冊,release notes,以及MySQL官方的Work log,其它第三方的博客等。

3.1 MySQL-5.7.7版本及之後解決了xa_prepared狀態的事務丟失的情況

這個提升點的明確說法,來源於MySQL官方手冊:

但是,事實上MySQL5.7到目前爲止都還存在xa prepred無法將事務處久化的問題。
可以見上文對XA prepare階段的過程描述。

3.2 引入XA_prepare_log_event ,事務prepared之後binlog落盤,配合半同步複製,可以確保prepared狀態的事務不丟失。

還是看時序圖吧

帶有XA_prepare_log_event的binlog在此階段強制落盤,並且可以實現半同步。

3.3 MySQL-5.7關於XA的一些bug修復

4. XA事務狀態查詢

處於prepared的XA事務可以通過xa recover命令查詢,實現原理參照上文中的描述。

其它狀態的事務可以通過performance_schema提供的監控項來查看,操作步驟如下:

//開啓performance_scheam對於事務相關的監控項以及存儲
mysql> update setup_instruments set ENABLED='YES',TIMED='YES' where NAME='transaction';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update setup_instruments set ENABLED='YES',TIMED='YES' where NAME like '%xa*%';

mysql> update setup_consumers set ENABLED = 'YES' where name like 'events_transactions%';
Query OK, 3 rows affected (0.03 sec)
Rows matched: 3  Changed: 3  Warnings: 0
//查詢

確保需要監控的用戶對象和數據對象被覆蓋到

mysql> select * from setup_actors;
+------+------+------+---------+---------+
| HOST | USER | ROLE | ENABLED | HISTORY |
+------+------+------+---------+---------+
| %    | %    | %    | YES     | YES     |
+------+------+------+---------+---------+
1 row in set (0.00 sec)

mysql> select * from setup_objects;
+-------------+--------------------+-------------+---------+-------+
| OBJECT_TYPE | OBJECT_SCHEMA      | OBJECT_NAME | ENABLED | TIMED |
+-------------+--------------------+-------------+---------+-------+
| EVENT       | mysql              | %           | NO      | NO    |
| EVENT       | performance_schema | %           | NO      | NO    |
| EVENT       | information_schema | %           | NO      | NO    |
| EVENT       | %                  | %           | YES     | YES   |
| FUNCTION    | mysql              | %           | NO      | NO    |
| FUNCTION    | performance_schema | %           | NO      | NO    |
| FUNCTION    | information_schema | %           | NO      | NO    |
| FUNCTION    | %                  | %           | YES     | YES   |
| PROCEDURE   | mysql              | %           | NO      | NO    |
| PROCEDURE   | performance_schema | %           | NO      | NO    |
| PROCEDURE   | information_schema | %           | NO      | NO    |
| PROCEDURE   | %                  | %           | YES     | YES   |
| TABLE       | mysql              | %           | NO      | NO    |
| TABLE       | performance_schema | %           | NO      | NO    |
| TABLE       | information_schema | %           | NO      | NO    |
| TABLE       | %                  | %           | YES     | YES   |
| TRIGGER     | mysql              | %           | NO      | NO    |
| TRIGGER     | performance_schema | %           | NO      | NO    |
| TRIGGER     | information_schema | %           | NO      | NO    |
| TRIGGER     | %                  | %           | YES     | YES   |
+-------------+--------------------+-------------+---------+-------+
20 rows in set (0.01 sec)

隨後即可通過performance_schema中的events_transactions_current,events_transactions_history,events_transactions_history_long來查看相關XA事務的狀態。
如下:


當事務prepare之後,表中數據就沒了,需要使用xa recover命令查看

5. 複製場景下XA事務的處理

本節主要描述在複製場景下,如何處理XA事務。在XA begin/end階段不涉及到和slave實例進行網絡交互,所以此處對這兩個過程暫時省略。
如下討論的參數前提爲:

innodb_flush_log_at_trx_commit=1
log_bin=on
sync_binlog=1
rpl_semi_sync_master_enabled=on
rpl_semi_sync_master_wait_point=after_sync

5.1 preapre階段

XA事務與半同步複製(異步複製不再單獨說明)。
在半同步複製下,主庫進行XA prepare操作時的處理邏輯(僞代碼)如下:

write_preapre_log_event()//寫preapre log event
ordered_commit()
{

    flush_stage()
    {
        flush_innobase_log();//innodb trx處於active狀態
        flush_binlog_cache();
        call_after_flush_hook();
    }
    sync_stage()
    {
        sync_binlog();
        update_binlog_end_pos();//觸發binlog dump線程發送binlog.
    }
    commit_stage()
    {
        call_after_sync_hook(); //master 收到ack後進行下一步操作.
        //skip innobase commit
        call_after_commit_hook();
    }
}

innobase_prepare();//innodb 進行trx的prepare操作.

從如上邏輯可以看出,在after_sync半同步複製模式下,主庫在執行XA prepare階段,會將包含有XA_prepare_log_event的binlog日誌發送到slave機器,並且等到ack返回後,繼續下一步操作。但此時事務在innodb中還處於active狀態,並且後續的innobase_prepare過程是不會強制將prepared信息落盤的。在這個過程中如果master因故宕機可能會帶來一些問題,如下:

5.2 XA commit階段

在半同步複製下,主庫進行XA commit操作時的處理邏輯(僞代碼)如下:

write_query_log_event()//寫xa commit
ordered_commit()
{

    flush_stage()
    {
        flush_innobase_log();//innodb trx處於prepared狀態
        flush_binlog_cache();
        call_after_flush_hook();
    }
    sync_stage()
    {
        sync_binlog();
        update_binlog_end_pos();//觸發binlog dump線程發送binlog.
    }
    commit_stage()
    {
        call_after_sync_hook(); //master收到ack後繼續下一步操作。
        innobase_commit();//innodb層進行事務提交,鎖資源釋放
        call_after_commit_hook();
    }
}

5.3 SQL線程回放XA事務時的特殊處理

SQL線程回放XA事務時,在執行完XA preapre操作後,會進行applier_reset_xa_trans操作,這個操作會解綁(或者叫分離)當前XA事務和當前SQL回放線程,解綁的原因是因爲XA事務提交分爲單獨的兩部分來處理,同一個XA事務的prepare,commit操作中間可能會後很多其它的事務操作,XA prepare操作後,SQL線程需要去回放其它事務,所以需要解綁,重置當前線程的事務狀態。詳細的過程如下所示:

XA_prepare_log_event::do_commit(THD*)
    Sql_cmd_xa_prepare::execute(THD*)
        .........//prepare過程省略,見上文.
        applier_reset_xa_trans(THD*)//事務重置邏輯
        {
            Transaction_ctx *trn_ctx= thd->get_transaction();//獲取當前線程事務上下文
            XID_STATE *xid_state= trn_ctx->xid_state();//獲取XA事務狀態
            
            thd->variables.option_bits&= ~OPTION_BEGIN;
            trn_ctx->reset_unsafe_rollback_flags(Transaction_ctx::STMT);
            thd->server_status&= ~SERVER_STATUS_IN_TRANS;
            transaction_cache_detach(trn_ctx) //分離解綁,重新插入事務緩存hash表,這個過程類似於在master上一個處於prepared狀態的XA事務,它的線程exit,或者被kill之後的邏輯。
            {
                 mysql_mutex_lock(&LOCK_transaction_cache);
                 my_hash_delete(&transaction_cache, (uchar *)transaction);
                 res= create_and_insert_new_transaction(&xid, was_logged);mysql_mutex_unlock(&LOCK_transaction_cache);
            }
            xid_state->reset();//狀態重置
            attach_native_trx(thd)
            {
                reattach_engine_ha_data_to_thd(THD*, handlerton const*)
                    innodb_replace_trx_in_thd(THD*, void*, void**)
                        trx_disconnect_prepared(trx_t*)
                            trx_disconnect_from_mysql(trx_t*, bool) //斷開innodb trx與mysql的關聯,並且將事務狀態is_recovered設置爲true.
            }
            trn_ctx->set_ha_trx_info(Transaction_ctx::SESSION, NULL);
            trn_ctx->set_no_2pc(Transaction_ctx::SESSION, false);
            trn_ctx->cleanup();
            thd->mdl_context.release_transactional_locks();
            
        }   

SQL線程在執行XA commit操作時的處理邏輯類似於XA commit一個不屬於當前事務上下文的XA 事務,其實就是去提交一個處於in_recovery=true的XA事務,這部分的處理在上文中已經進行了描述。

6. XA事務的邊界場景描述和應對手段

6.1 XID衝突

對MySQL內部XA事務而言,在XID衝突的情況下,會報錯。XA事務在啓動時會在mutex鎖保護下進行查重,如果衝突,事務無法繼續進行。
對於全局分佈式事務而言,XID的產生應該有一套自己的機制,避免重複。

6.2 XID重用

對MySQL內部XA事務而言,XID可以重用,但是前提是被重用的XID必須處於已提交/回滾的狀態,也就是說,在事務緩存hash表中,不存在此XID。否則會報錯。

6.3 故障切換

本節討論基於MySQL5.7.18,並且數據庫參數設置如下:

innodb_flush_log_at_trx_commit=1
log_bin=on
sync_binlog=1
rpl_semi_sync_master_enabled=on
rpl_semi_sync_master_wait_point=after_sync

6.3.1 XA preapare階段master 發生宕機

XA prepare階段的處理邏輯如下圖:

以下列舉部分可能出現在XA prepare階段的故障場景,只是部分場景。

  • 場景一:master 在進入ordered_commit前宕機

此時客戶端收到XA prepare失敗,master內部的情況爲:binlog尚未寫入磁盤,innodb事務狀態爲active,binlog未發送給slave。在這種情況下,master重啓後active事務回滾,slave中無此事務,主從數據是一致的。在全局分佈式事務下,可以回滾其它事務分支。

  • 場景二:master 在flush_stage()後,sync_stage()前宕機器

此時客戶端收到XA prepare失敗,master內部的情況爲:binlog已經寫入磁盤,並且有可能已經落盤,innodb事務狀態爲active,binlog未發送給slave。在這種情況下,master重啓後active事務回滾,slave中無此事務,主從數據是一致的。和場景一結果相同。

  • 場景三:master在binlog發送完畢後宕機

此時客戶端收到XA prepare失敗,master內部的情況爲:binlog落盤,innodb事務狀態爲acvive,binlog已經發送。在這種情況下,master重啓後active事務回滾,slave中存在此prepared狀態的事務,主從數據不一致,但是可以通過XA rollback命令進行回滾。

  • 場景四:master在innobase_prepare後,但是prepare日誌落盤前宕機。

此時AP收到XA prerpare失敗,master內部的情況爲:和場景三相同。slave進行回滾即可。

  • 場景五:master在innobase_prepare後,並且prepare日誌已經落盤

此時AP收到XA prepare失敗,master內部的情況爲:binlog落盤,innodb事務狀態爲prepared,slave中存在此prepare狀態的事務,master重啓後處於XA prepared狀態的事務依然存在,主從是一致的。

  • 場景六:master在返回給客戶端XA preapre 成功的消息後宕機,但是innobase preapre信息還沒有落盤。

此時AP認爲事務分支已經prepare成功,下發commit操作,但是連接報錯,需要重新連接,假設master啓動很快,主從沒有發生切換,則XA commit會報錯事務不存在。如果發生了切換,XA commit下發給新主,則沒有問題,但是需要修復老主和新主之間的複製關係。

  • 場景7:master在返回給客戶端XA preapre成功,並且innodb preapre日誌落盤後宕機。

此時客戶端認爲事務已經preapre成功,下發XA commit操作,但是連接報錯,需要重新連接,假設master啓動很快,此時無論主從是否發生切換,重連後,XA commit都可以下發成功。

綜合上述,在全局分佈式事務中,需要考慮XA prepare成功或者失敗後的各種情況。而真正在使用XA事務時,可以簡化處理邏輯,指定處理規範。一般來講,可以進行如下處理。

某個事務分支XA preapre失敗,可能有多種情況,見下圖:

  • 由於XA狀態部位XA_IDLE引起的prepare失敗,可能是TM的控制問題,測試階段如果發現,需要人工介入進行排查。

  • 由於鏈接丟失導致的preapre失敗,可能是被kill,或者master宕機,應該回滾其它事務分支。並且故障事務分支所在的分片鏈接恢復後,需要進行XID狀態自檢查,如果存在prepared狀態的XA事務,應該進行回滾操作。

處理策略爲回滾全局分佈式事務。可能面臨回滾報錯,如果Xid不存在,則忽略,認爲回滾成功,slave如果存在此XID,則需要回滾(人工介入或者其它機制去回滾)

6.3.2 commit階段master宕機

某個事務分支在XA commit階段失敗,也可能有多種情況,見下圖:

處理策略如下:

  • XA狀態錯誤引起的XA commit失敗,可能是TM的控制問題,測試階段如果發現,需要人工介入排查

  • 由於鏈接丟失導致的commit失敗,可能是被kill,可能是master宕機,此時不可以回滾其它事務分支,並且故障事務分支所在的分片鏈接恢復後,需要進行XID狀態自檢查,如果存在prepared狀態的XA事務,應該進行提交操作,如果不存在,則說明上次提交是成功的(無論是否發生了切換)。

6.4 XA commit in_recovery狀態的XA事務

如下爲XA commit一個處於xa_prepared狀態的事務,在半同步複製模式爲after_sync時,等待ack時,innodb層沒有提交,所以事務對其它客戶端不可見。

但是,如果XA commit操作是去提交一個in_recovery狀態的XA事務,將會出現如下的情況(這部分內容在時序圖中沒有體現):

此時,在after_sync hook調用前,其它線程可以看到對應的數據,這是和非分佈式事務不同的地方,原因是因爲通過XA commit 處於in_recovery狀態的XA事務時,innodb提交的過程被提前了。但是從帶來的影響上來看,只要半同步slave存活,這部分數據是在slave中的,即便發生了切換,slave上也可以繼續提交此事務。後續對bug修復的研究過程中,會查看此類問題是否被修復。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章