mysql平台workb_MySQL分布式事务
一、分布式事務
在說分布式事務(XA)之前,可以先看一下“以交易系統為例,看分布式事務架構的五大演進”,闡述了分布式事務解決了什么問題?
InnoDB存儲引擎提供了對XA事務的支持,并通過XA事務來支持分布式事務的實現。分布式事務指的是允許多個獨立的事務資源參與到一個全局的事務中。事務資源通常是關系型數據庫系統,但也可以是其他類型的資源。全局事務要求在其中的所有參與的事務要么都提交,要么都回滾,這對于事務原有的ACID要求又有了提高。另外,在使用分布式事務時,InnoDB存儲引擎的事務隔離級別必須設置為SERIALIZABLE。
XA事務語允許不同數據庫之間的分布式事務,如一臺服務器是MySQL數據庫的,另一臺是Oracle數據庫的,又可能還有一臺服務器是SQL Server數據庫的,只要參與在全局事務中的每個節點都支持XA事務。
考慮下面一種場景:當你發了工資之后,把你的當月工資¥10000從支付寶轉到了余額寶。如果在支付寶賬戶扣除¥10000之后,余額寶系統掛掉了,余額寶的賬戶并沒有增加¥10000,這時候就出現了數據不一致的情況。
# 支付寶
update account set money = money - 10000 where user='dkey';
# 余額寶
update account set money = money + 10000 where user='dkey';
1
2
3
4
5
# 支付寶
updateaccountsetmoney=money-10000whereuser='dkey';
# 余額寶
updateaccountsetmoney=money+10000whereuser='dkey';
在很多系統中都能找到上述情況的影子:
在下單的時候,需要在訂單表中插入一條數據,然后把庫存減去一。
在搜索的時候如果點擊了廣告,需要先記錄該點擊事件,然后通知商家系統扣除廣告費。
在這種情況下,一定需要使用分布式事務來保證數據的安全。如果發生的操作不能全部提交或回滾,那么任何一個節點出現問題都會導致嚴重的結果。
在一個分布式事務結束的時候,事務的原子特性要求所有參與該事務的服務器必須全部提交或全部放棄該事務。為了實現這一點,其中一個服務器承擔了協調者(coordinater)的角色,由它來保證所有的服務器獲得相同的結果。
協調者(coordinater)的工作方式取決于它選用的協議,“兩階段提交”是分布式事務最常用的協議。
兩階段提交協議(Two-phase Commit,2PC)經常被用來實現分布式事務。一般由一個或多個資源管理器(resource managers)、一個事務協調器(transaction coordinater)以及一個應用程序(application program)組成。事務協調器可以和資源管理器在一臺機器上。
資源管理器:提供訪問事務資源的方法,通常一個數據庫就是一個資源管理器。
事務協調器:協調參與全局事務中的各個事務,需要和參與全局事務的所有資源管理器進行通信。
應用程序:定義事務的邊界,指定全局事務中的操作。
在MySQL數據庫的分布式事務中,資源管理器就是MySQL數據庫,事務協調器為連接MySQL服務器的客戶端(支持分布式事務的客戶端)。下圖顯示了一個分布式事務的模型。
分布式事務通常采用2PC協議,全稱Two Phase Commitment Protocol。該協議主要為了解決在分布式數據庫場景下,所有節點間數據一致性的問題。在分布式事務環境下,事務的提交會變得相對比較復雜,因為多個節點的存在,可能存在部分節點提交失敗的情況,即事務的ACID特性需要在各個數據庫實例中保證。總而言之,在分布式提交時,只要發生一個節點提交失敗,則所有的節點都不能提交,只有當所有節點都能提交時,整個分布式事務才允許被提交。
在該協議的第一個階段,每個參與者投票表決該事務是放棄還是提交,一旦參與者要求提交事務,那么就不允許放棄該事務。因此,在一個參與者要求提交事務之前,它必須保證最終能夠執行分布式事務中自己的那部分,即使該參與者出現故障而被中途替換掉。
一個事務的參與者如果最終能提交事務,那么可以說參與者處于事務的準備好(prepared)狀態。為了保證能夠提交,每個參與者必須將事務中所有發生改變的對象以及自身的狀態(prepared)保存到持久性存儲中。
在該協議的第二個階段,事務的每個參與者執行最終統一的決定。如果任何一個參與者投票放棄事務,那么最終的決定是放棄事務,則所有的節點都被告知需要回滾。如果所有的參與者都投票提交事務,那么最終的決定是提交事務。
1. 我們的應用程序(client)發起一個開始請求到TC;
2. TC先將消息寫到本地日志,之后向所有的RM發起消息。以支付寶轉賬到余額寶為例,TC給A的prepare消息是通知支付寶數據庫相應賬目扣款1萬,TC給B的prepare消息是通知余額寶數據庫相應賬目增加1w。為什么在執行任務前需要先寫本地日志,主要是為了故障后恢復用,本地日志起到現實生活中憑證的效果,如果沒有本地日志(憑證),出問題容易死無對證;
3. RM收到消息后,執行具體本機事務,但不會進行commit,如果成功返回,不成功返回。同理,返回前都應把要返回的消息寫到日志里,當作憑證。
4. TC收集所有執行器返回的消息,如果所有執行器都返回yes,那么給所有執行器發生送commit消息,執行器收到commit后執行本地事務的commit操作;如果有任一個執行器返回no,那么給所有執行器發送rollback消息,執行器收到rollback消息后執行事務rollback操作。
注:TC或RM把發送或接收到的消息先寫到日志里,主要是為了故障后恢復用。如某一RM從故障中恢復后,先檢查本機的日志,如果已收到,則提交,如果則回滾。如果是,則再向TC詢問一下,確定下一步。如果什么都沒有,則很可能在階段RM就崩潰了,因此需要回滾。
可見與本地事務不同的是,分布式事務需要多一次的PREPARE操作,待收到所有節點的同意信息后,再進行COMMIT或是ROLLBACK操作。
現如今實現基于兩階段提交的分布式事務也沒那么困難了,如果使用java,那么可以使用開源軟件atomikos(http://www.atomikos.com/)來快速實現。
不過但凡使用過的上述兩階段提交的同學都可以發現性能實在是太差,根本不適合高并發的系統。為什么?
兩階段提交涉及多次節點間的網絡通信,通信時間太長!
事務時間相對于變長了,鎖定的資源的時間也變長了,造成資源等待時間也增加好多!
正是由于分布式事務存在很嚴重的性能問題,大部分高并發服務都在避免使用,往往通過其他途徑來解決數據一致性問題。比如使用消息隊列來避免分布式事務。
二、MySQL分布式事務操作
XA事務語法
# 在mysql實例中開啟一個XA事務,指定一個全局唯一標識;
mysql> XA START 'any_unique_id';
# XA事務的操作結束;
mysql> XA END 'any_unique_id';
# 告知mysql準備提交這個xa事務;
mysql> XA PREPARE 'any_unique_id';
# 告知mysql提交這個xa事務;
mysql> XA COMMIT 'any_unique_id';
# 告知mysql回滾這個xa事務;
mysql> XA ROLLBACK 'any_unique_id';
# 查看本機mysql目前有哪些xa事務處于prepare狀態;
mysql> XA RECOVER;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在mysql實例中開啟一個XA事務,指定一個全局唯一標識;
mysql>XASTART'any_unique_id';
# XA事務的操作結束;
mysql>XAEND'any_unique_id';
# 告知mysql準備提交這個xa事務;
mysql>XAPREPARE'any_unique_id';
# 告知mysql提交這個xa事務;
mysql>XACOMMIT'any_unique_id';
# 告知mysql回滾這個xa事務;
mysql>XAROLLBACK'any_unique_id';
# 查看本機mysql目前有哪些xa事務處于prepare狀態;
mysql>XARECOVER;
XA事務演示
在單個節點上運行分布式事務是沒有意義的,起碼兩個節點才有意義。但是要在MySQL數據庫的命令行下演示多個節點參與的分布式事務也是行不通的。通常來說,都是通過編程語言來完成分布式事務操作的。當前Java的JTA可以很好地支持MySQL的分布式事務。下面用一個簡單的例子來演示:
public class XaDemo {
public static MysqlXADataSource getDataSource(String connStr, String user, String pwd) {
try {
MysqlXADataSource ds = new MysqlXADataSource();
ds.setUrl(connStr);
ds.setUser(user);
ds.setPassword(pwd);
return ds;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] arg) {
String connStr1 = "jdbc:mysql://192.168.0.1:3306/test";
String connStr2 = "jdbc:mysql://192.168.0.2:3306/test";
try {
//從不同數據庫獲取數據庫數據源
MysqlXADataSource ds1 = getDataSource(connStr1, "root", "123456");
MysqlXADataSource ds2 = getDataSource(connStr2, "root", "123456");
//數據庫1獲取連接
XAConnection xaConnection1 = ds1.getXAConnection();
XAResource xaResource1 = xaConnection1.getXAResource();
Connection connection1 = xaConnection1.getConnection();
Statement statement1 = connection1.createStatement();
//數據庫2獲取連接
XAConnection xaConnection2 = ds2.getXAConnection();
XAResource xaResource2 = xaConnection2.getXAResource();
Connection connection2 = xaConnection2.getConnection();
Statement statement2 = connection2.createStatement();
//創建事務分支的xid
Xid xid1 = new MysqlXid(new byte[] { 0x01 }, new byte[] { 0x02 }, 100);
Xid xid2 = new MysqlXid(new byte[] { 0x011 }, new byte[] { 0x012 }, 100);
try {
//事務分支1關聯分支事務sql語句
xaResource1.start(xid1, XAResource.TMNOFLAGS);
int update1Result = statement1.executeUpdate("update account_from set money=money - 50 where id=1");
xaResource1.end(xid1, XAResource.TMSUCCESS);
//事務分支2關聯分支事務sql語句
xaResource2.start(xid2, XAResource.TMNOFLAGS);
int update2Result = statement2.executeUpdate("update account_to set money= money + 50 where id=1");
xaResource2.end(xid2, XAResource.TMSUCCESS);
// 兩階段提交協議第一階段
int ret1 = xaResource1.prepare(xid1);
int ret2 = xaResource2.prepare(xid2);
// 兩階段提交協議第二階段
if (XAResource.XA_OK == ret1 && XAResource.XA_OK == ret2) {
xaResource1.commit(xid1, false);
xaResource2.commit(xid2, false);
System.out.println("reslut1:" + update1Result + ", result2:" + update2Result);
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
publicclassXaDemo{
publicstaticMysqlXADataSourcegetDataSource(StringconnStr,Stringuser,Stringpwd){
try{
MysqlXADataSourceds=newMysqlXADataSource();
ds.setUrl(connStr);
ds.setUser(user);
ds.setPassword(pwd);
returnds;
}catch(Exceptione){
e.printStackTrace();
}
returnnull;
}
publicstaticvoidmain(String[]arg){
StringconnStr1="jdbc:mysql://192.168.0.1:3306/test";
StringconnStr2="jdbc:mysql://192.168.0.2:3306/test";
try{
//從不同數據庫獲取數據庫數據源
MysqlXADataSourceds1=getDataSource(connStr1,"root","123456");
MysqlXADataSourceds2=getDataSource(connStr2,"root","123456");
//數據庫1獲取連接
XAConnectionxaConnection1=ds1.getXAConnection();
XAResourcexaResource1=xaConnection1.getXAResource();
Connectionconnection1=xaConnection1.getConnection();
Statementstatement1=connection1.createStatement();
//數據庫2獲取連接
XAConnectionxaConnection2=ds2.getXAConnection();
XAResourcexaResource2=xaConnection2.getXAResource();
Connectionconnection2=xaConnection2.getConnection();
Statementstatement2=connection2.createStatement();
//創建事務分支的xid
Xidxid1=newMysqlXid(newbyte[]{0x01},newbyte[]{0x02},100);
Xidxid2=newMysqlXid(newbyte[]{0x011},newbyte[]{0x012},100);
try{
//事務分支1關聯分支事務sql語句
xaResource1.start(xid1,XAResource.TMNOFLAGS);
intupdate1Result=statement1.executeUpdate("update account_from set money=money - 50 where id=1");
xaResource1.end(xid1,XAResource.TMSUCCESS);
//事務分支2關聯分支事務sql語句
xaResource2.start(xid2,XAResource.TMNOFLAGS);
intupdate2Result=statement2.executeUpdate("update account_to set money= money + 50 where id=1");
xaResource2.end(xid2,XAResource.TMSUCCESS);
//兩階段提交協議第一階段
intret1=xaResource1.prepare(xid1);
intret2=xaResource2.prepare(xid2);
//兩階段提交協議第二階段
if(XAResource.XA_OK==ret1&&XAResource.XA_OK==ret2){
xaResource1.commit(xid1,false);
xaResource2.commit(xid2,false);
System.out.println("reslut1:"+update1Result+", result2:"+update2Result);
}
}catch(Exceptione){
e.printStackTrace();
}
}catch(Exceptione){
e.printStackTrace();
}
}
}
XA事務恢復
如果執行分布式事務的某個mysql crash了,MySQL按照如下邏輯進行恢復:
a. 如果這個xa事務commit了,那么什么也不用做。
b. 如果這個xa事務還沒有prepare,那么直接回滾它。
c. 如果這個xa事務prepare了,還沒commit,那么把它恢復到prepare的狀態,由用戶去決定commit或rollback。
當mysql crash后重新啟動之后,執行“XA RECOVER;”查看當前處于prepare狀態的xa事務,然后commit或rollback它們即可。如果不去處理,那么它們占用的資源就一直不會釋放,比如鎖。
三、MySQL分布式事務限制
a. XA事務和本地事務以及鎖表操作是互斥的
開啟了xa事務就無法使用本地事務和鎖表操作
mysql> xa start 't1xa';
Query OK, 0 rows affected (0.04 sec)
mysql> begin;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the??ACTIVE state
mysql> lock table t read;
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the??ACTIVE state
1
2
3
4
5
6
7
8
mysql>xastart't1xa';
QueryOK,0rowsaffected(0.04sec)
mysql>begin;
ERROR1399(XAE07):XAER_RMFAIL:Thecommandcannotbeexecutedwhenglobaltransactionisinthe??ACTIVEstate
mysql>locktabletread;
ERROR1399(XAE07):XAER_RMFAIL:Thecommandcannotbeexecutedwhenglobaltransactionisinthe??ACTIVEstate
開啟了本地事務就無法使用xa事務
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> xa start 'rrrr';
ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
1
2
3
4
5
mysql>begin;
QueryOK,0rowsaffected(0.00sec)
mysql>xastart'rrrr';
ERROR1400(XAE09):XAER_OUTSIDE:Someworkisdoneoutsideglobaltransaction
b. xa start之后必須xa end,否則不能執行xa commit和xa rollback
所以如果在執行xa事務過程中有語句出錯了,你也需要先xa end一下,然后才能xa rollback。
mysql> xa start 'tt';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'tt';
ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
mysql> xa end 'tt';
Query OK, 0 rows affected (0.00 sec)
mysql> xa rollback 'tt';
Query OK, 0 rows affected (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
mysql>xastart'tt';
QueryOK,0rowsaffected(0.00sec)
mysql>xarollback'tt';
ERROR1399(XAE07):XAER_RMFAIL:ThecommandcannotbeexecutedwhenglobaltransactionisintheACTIVEstate
mysql>xaend'tt';
QueryOK,0rowsaffected(0.00sec)
mysql>xarollback'tt';
QueryOK,0rowsaffected(0.00sec)
四、MySQL 5.7對分布式事務的支持
一直以來,MySQL數據庫是支持分布式事務的,但是只能說是有限的支持,具體表現在:
已經prepare的事務,在客戶端退出或者服務宕機的時候,2PC的事務會被回滾。
在服務器故障重啟提交后,相應的Binlog被丟失。
上述問題存在于MySQL數據庫長達數十年的時間,直到MySQL-5.7.7版本,官方才修復了該問題。下面將會詳細介紹下該問題的具體表現和官方修復方法,這里分別采用官方MySQL-5.6.27版本(未修復)和MySQL-5.7.9版本(已修復)進行驗證。
先來看下存在的問題,我們先創建一個表如下:
CREATE TABLE t(
id INT AUTO_INCREMENT PRIMARY KEY,
a INT
) ENGINE=InnoDB;
1
2
3
4
CREATETABLEt(
idINTAUTO_INCREMENTPRIMARYKEY,
aINT
)ENGINE=InnoDB;
對于上述表,通過如下操作進行數據插入:
mysql> XA START 'mysql56';
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t VALUES(1,1);
Query OK, 1 row affected (0.02 sec)
mysql> XA END 'mysql56';
Query OK, 0 rows affected (0.00 sec)
mysql> XA PREPARE 'mysql56';
Query OK, 0 rows affected (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
mysql>XASTART'mysql56';
QueryOK,0rowsaffected(0.00sec)
mysql>INSERTINTOtVALUES(1,1);
QueryOK,1rowaffected(0.02sec)
mysql>XAEND'mysql56';
QueryOK,0rowsaffected(0.00sec)
mysql>XAPREPARE'mysql56';
QueryOK,0rowsaffected(0.00sec)
通過上面的操作,用戶創建了一個分布式事務,并且prepare沒有返回錯誤,說明該分布式事務可以被提交。通過命令XA?RECOVER查看顯示如下結果:
mysql> XA RECOVER;
+----------+--------------+--------------+---------+
| formatID | gtrid_length | bqual_length | data ? ?|
+----------+--------------+--------------+---------+
| 1 ? ? ? ?| 7 ? ? ? ? ? ?| 0 ? ? ? ? ? ?| mysql56 |
+----------+--------------+--------------+---------+
1
2
3
4
5
6
mysql>XARECOVER;
+----------+--------------+--------------+---------+
|formatID|gtrid_length|bqual_length|data??|
+----------+--------------+--------------+---------+
|1????|7??????|0??????|mysql56|
+----------+--------------+--------------+---------+
若這時候用戶退出客戶端后重連,通過命令xa recover會發現剛才創建的2PC事務不見了。即prepare成功的事務丟失了,不符合2PC協議規范!!!
產生上述問題的主要原因在于:MySQL 5.6版本在客戶端退出的時候,自動把已經prepare的事務回滾了,那么MySQL為什么要這樣做?這主要取決于MySQL的內部實現,MySQL 5.7以前的版本,對于prepare的事務,MySQL是不會記錄binlog的(官方說是減少fsync,起到了優化的作用)。只有當分布式事務提交的時候才會把前面的操作寫入binlog信息,所以對于binlog來說,分布式事務與普通的事務沒有區別,而prepare以前的操作信息都保存在連接的IO_CACHE中,如果這個時候客戶端退出了,以前的binlog信息都會被丟失,再次重連后允許提交的話,會造成Binlog丟失,從而造成主從數據的不一致,所以官方在客戶端退出的時候直接把已經prepare的事務都回滾了!
官方的做法,貌似干得很漂亮,犧牲了一點標準化的東西,至少保證了主從數據的一致性。但其實不然,若用戶已經prepare后在客戶端退出之前,MySQL發生了宕機,這個時候又會怎樣?
MySQL在某個分布式事務prepare成功后宕機,宕機前操作該事務的連接并沒有斷開,這個時候已經prepare的事務并不會被回滾,所以在MySQL重新啟動后,引擎層通過recover機制能恢復該事務。當然該事務的Binlog已經在宕機過程中被丟失,這個時候,如果去提交,則會造成主從數據的不一致,即提交沒有記錄Binlog,從上丟失該條數據。所以對于這種情況,官方一般建議直接回滾已經prepare的事務。
以上是MySQL 5.7以前版本MySQL在分布式事務上的各種問題,那么MySQL 5.7版本官方做了哪些改進?這個可以從官方的WL#6860描述上得到一些信息,我們還是本著沒有實踐就沒有發言權的態度,從具體的操作上來分析下MySQL 5.7的改進方法。還是以上面同樣的表結構進行同樣的操作如下:
mysql> XA START 'mysql57';
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t VALUES(1,1);
Query OK, 1 row affected (0.02 sec)
mysql> XA END 'mysql57';
Query OK, 0 rows affected (0.00 sec)
mysql> XA PREPARE 'mysql57';
Query OK, 0 rows affected (0.00 sec)
1
2
3
4
5
6
7
8
9
10
11
mysql>XASTART'mysql57';
QueryOK,0rowsaffected(0.00sec)
mysql>INSERTINTOtVALUES(1,1);
QueryOK,1rowaffected(0.02sec)
mysql>XAEND'mysql57';
QueryOK,0rowsaffected(0.00sec)
mysql>XAPREPARE'mysql57';
QueryOK,0rowsaffected(0.00sec)
這個時候,我們通過mysqlbinlog來查看下Master上的Binlog,結果如下:
同時也對比下Slave上的Relay log,如下:
通過上面的操作,明顯發現在prepare以后,從XA START到XA PREPARE之間的操作都被記錄到了Master的Binlog中,然后通過復制關系傳到了Slave上。也就是說MySQL 5.7開始,MySQL對于分布式事務,在prepare的時候就完成了寫Binlog的操作,通過新增一種叫XA_prepare_log_event的event類型來實現,這是與以前版本的主要區別(以前版本prepare時不寫Binlog)。
當然僅靠這一點是不夠的,因為我們知道Slave通過SQL thread來回放Relay log信息,由于prepare的事務能阻塞整個session,而回放的SQL thread只有一個(不考慮并行回放),那么SQL thread會不會因為被分布式事務的prepare階段所阻塞,從而造成整個SQL thread回放出現問題?這也正是官方要解決的第二個問題:怎么樣能使SQL thread在回放到分布式事務的prepare階段時,不阻塞后面event的回放?其實這個實現也很簡單(在xa.cc::applier_reset_xa_trans),只要在SQL thread回放到prepare的時候,進行類似于客戶端斷開連接的處理即可(把相關cache與SQL thread的連接句柄脫離)。最后在Slave服務器上,用戶通過命令XA RECOVER可以查到如下信息:
mysql> XA RECOVER;
+----------+--------------+--------------+---------+
| formatID | gtrid_length | bqual_length | data ? ?|
+----------+--------------+--------------+---------+
| 1 ? ? ? ?| 7 ? ? ? ? ? ?| 0 ? ? ? ? ? ?| mysql57 |
+----------+--------------+--------------+---------+
1
2
3
4
5
6
mysql>XARECOVER;
+----------+--------------+--------------+---------+
|formatID|gtrid_length|bqual_length|data??|
+----------+--------------+--------------+---------+
|1????|7??????|0??????|mysql57|
+----------+--------------+--------------+---------+
至于上面的事務什么時候提交,一般等到Master上進行XA COMMIT ?‘mysql57’后,slave上也同時會被提交。
總結
綜上所述,MySQL 5.7對于分布式事務的支持變得完美了,一個長達數十年的bug又被修復了,因而又多了一個升級到MySQL 5.7版本的理由。
如果您覺得本站對你有幫助,那么可以支付寶掃碼捐助以幫助本站更好地發展,在此謝過。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的mysql平台workb_MySQL分布式事务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php yield 个人小解_php 技
- 下一篇: mysql索引数据结构图解_深入理解My