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修復的研究過程中,會查看此類問題是否被修復。