vitess兩階段提交事務
Vitess之前,先複習一下事務的四個基本特性
原子性
:一個事務對狀態的改變是原子的,要麼都發生,要麼都不發生,這些改變包括資料庫的改變、訊息以及對轉換器的操作。一致性
:一個事務是對狀態的一個正確改變。作為一組操作沒有違反任何與狀態相關的完整性約束。這要求事務是一個正確的程式。隔離性
:儘管事務是併發執行的,但看起來是單個執行的,即對於一個事務T,任何其他事務要麼在T之前執行,要麼在T之後執行,但不會既在T之前執行,又在T之後執行。永久性
:一旦一個事務成功完成(提交),它對狀態的改變不會受其他失敗的影響。
舉一個銀行取錢事務的例子。如果同時完成了取出錢和賬戶的更改,那就是原子的。如果賬戶減少的錢等於取出的錢,那麼就是一致的。如果這個過程不受其他程式併發讀寫你賬戶的程式的影響(比如你的女朋友們正在併發的刷你的銀行卡),那麼它是隔離的。一旦事務完成了,(無論完成之後機器當機、斷電、還是網路異常)賬戶的餘額必然會反映取款後的情況,那麼它是永久性的。
事務是資料庫的核心特性,MySQL、ORACLE、PostgreSQL這些資料庫都是支援事務的。國內網際網路公司更多的使用MySQL。
但是網際網路公司的資料一般比較大,單機資料庫服務難以承擔這麼大的資料量。這時候一般會做分庫,將原來一個資料庫的資料拆分到多個庫(這裡簡單認為一個MySQL例項上只有一個庫),比使用者表做拆分,根據使用者id,id對64取餘數,然後根據得到的餘數定位特定的分庫。比如id等於1的使用者的資訊就在第1個庫,id等於2的使用者資料就在第2庫,這樣就解決了單機資料容量問題。
現在使用者1要下單了,需要白條扣款
和生成訂單
兩個步驟。兩個步驟需要保證原子性的,要麼扣款完成、訂單生成,要麼不扣款、不下單(all-or-nothing
)。如果訂單和白條表都是按照使用者id做拆分的那最好,所有操作都會在同一個庫上面進行,使用單機事務,MySQL就保證了原子性。如果白條表是按照訂單id拆分的,那很可能不在第1分庫了。這時候就涉及到分散式事務。
本來事務的執行流程應該是這樣:
BEGIN; 生成訂單; 白條扣款; COMMIT/ROLLBACK
問題就出在分散式場景中的提交事務。我們需要連線兩個庫開啟兩個分庫的兩個事務,TX1和TX2, TX1生成訂單
寫入分庫1,TX2執行白條扣款
寫入分庫2。
在業務程式碼裡執行時間線(t1到t5表示先後的時間點)會是這樣的:
(t1)SESSION1 BEGIN TX1; (t2)SESSION2 BEGIN TX2; (t3)SESSION1 生成訂單,訂單資訊寫入分庫1; (t4)SESSION2白條扣款寫入分庫2; (t5)SESSION1 COMMIT TX1; (t6)SESSION2 COMMIT TX2.
如果t5執行正常,給客戶下單了,分庫2當機了,提交TX2失敗,TX2自動回滾。分散式事務只提交了一部分,訂單生成了,但是白條沒扣款(我做遊戲的時候都是先扣錢、扣錢成功再發裝備)。這就經典的是部分提交(PARTIAL COMMIT
)問題。
在分散式場景中保證原子性避免部分提交,有一個辦法,那就是兩階段提交(TWO PHASE COMMIT
)
上面的分散式事務涉及到了分庫1和分庫2。其實還有一個角色,就是執行業務邏輯的worker
節點,worker在兩階段提交過程中也是一個重要的角色,我們稱之為事務管理器(Transaction Manager
),事務管理器在兩階段提交過程中是一個協調者的角色。事務管理器負責整個事務的開始、執行、回滾或者提交。事務管理器異常也可能導致整個事務的異常,比如worker節點在t5之後,t6之前當機,也會導致整個事務的部分提交。我們也給分庫1和分庫2一個專業的名字,叫做資源管理器(Resource Manager
),資源管理器在分散式事務中是參與者的角色,負責執行協調者下達的指令,此外資源管理器是能夠支援本地事務的。
MySQL提供了XA分散式事務介面供外部協調者呼叫。
關於XA事務的兩個概念:
資源管理器(
Resource Manager
):用來管理系統資源,是通向事務資源的途徑。資料庫就是一種資源管理器。資源管理還應該具有管理事務提交或回滾的能力。事務管理器(
Transaction Manager
):事務管理器是分散式事務的核心管理者。事務管理器與每個資源管理器(Resource Manager
)進行通訊,協調並完成事務的處理。事務的各個分支由唯一命名進行標識。
上面例子中的生成訂單白條扣款的分散式事務中,MySQL伺服器相當於XA事務資源管理器,與MySQL連結的客戶端,也就是執行業務邏輯的worker節點相當於事務管理器。
但是MySQL早期的版本XA事務存在BUG,Vitess自己實現了整套兩階段提交,所以Vitess是學習兩階段提交的一個很好的資源。Vitess地址:
先引入Vitess的幾個概念:
image
上面是Vitess的整體架構,兩階段提交過程中涉及到的模組主要是vtgate
和 vttablet
、MySQL
。
vttablet
和MySQL
其實可以看做一個整體,我們稱之為Shard
。Shard在兩階段提交中是參與者,也就是資源管理器。
vtgate
是事務管理器,也就是事務的協調者,業務的應用Application
連線到vtgate
上面,vtgate
透過路由將業務的請求轉發到後端的一個或者多個Shard,然後在將結果彙總發生Application
。Application
發起一個事務,比如插入兩條資料,根據vtgate
做路由後可能將兩條資料插入到一個或者兩個Shard。
上圖中的三個Shard是三個資源管理器,也是兩階段提交的參與者。vtgate是事務管理器,是兩階段提交的的協調者。
vtgate到vttablet請求是透過grpc實現,vttablet維護了到MySQL的連線池。
對於非事務請求,vtgate請求傳送到vttablet,vttablet隨便從連線池裡拿到一個連線,執行SQL,返回結果給vtgate,然後將連線放回到連線池,這樣一個請求單獨佔用連線的時間很短。
對於事務請求,vtgate發生請求到vttablet,vttablet會從事務連線池裡拿到一個連線,並生產一個本地事務id,執行SQL,將執行結果和本地事務id返回給vtgate,最後將事務連線放入一個單獨的activePool
,這樣下次vtgate需要在該事務裡繼續執行SQL,只需要請求帶著事務id,透過事務id從activePool
拿到事務連線,執行SQL即可,直到vtgate提交或者回滾事務,vttablet才將連線從activePool
放回到事務連線池。
下面的程式碼就是vttablet根據本地事務id從activePool
拿連線。
// Get fetches the connection associated to the transactionID.// You must call Recycle on TxConnection once done.func (axp *TxPool) Get(transactionID int64, reason string) (*TxConnection, error) { v, err := axp.activePool.Get(transactionID, reason) if err != nil { return nil, vterrors.Errorf(vtrpcpb.Code_ABORTED, "transaction %d: %v", transactionID, err) } return v.(*TxConnection), nil}
Vitess的兩階段提交實現
回到上面的分散式事務,vtgate要在第一個Shard和第二個Shard寫入資料,vtgate在兩個SESSION裡面開啟兩個事務,執行並記錄SQL。
(t1)SESSION1 BEGIN TX1 IN Shard1; (t2)SESSION2 BEGIN TX2 IN Shard2 (t3)SESSION1 生成訂單寫入Shard1未提交,將對應SQL語句作為redo log記錄在記憶體中 (t4)SESSION2白條扣款寫入Shard2未提交,將對應SQL語句作為redo log記錄在記憶體中
除了把事務中執行的SQL記錄在記憶體中之外,前面四個步驟和之前沒有任何太多區別,區別就在最後的提交階段。下面我們結合程式碼看Vitess是如何做兩階段提交的。
下面是Vitess中vtgate作為協調者的程式碼模組,結合程式碼分析Vitess是如何做兩階段提交的
1 func (txc *TxConn)commit2PC(ctx context.Context, session *SafeSession)error {2 if len(session.ShardSessions) <=1 {3 return txc.commitNormal(ctx, session)4 }56 participants :=make([]*querypb.Target, 0, len(session.ShardSessions)-1)7 for _, s :=range session.ShardSessions[1:] {8 participants = append(participants, s.Target)9 }10 11 mmShard := session.ShardSessions[0]12 dtid := dtids.New(mmShard)13 err := txc.gateway.CreateTransaction(ctx, mmShard.Target, dtid, participants)14 if err != nil {15 // Normal rollback is safe because nothing was prepared yet.16 txc.Rollback(ctx, session)17 return err18 }1920 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {21 return txc.gateway.Prepare(ctx, s.Target, s.TransactionId, dtid)22 })2324 if err != nil {25 if resumeErr := txc.Resolve(ctx, dtid); resumeErr != nil {26 log.Warningf("Rollback failed after Prepare failure: %v", resumeErr)27 }28 // Return the original error even if the previous operation fails.29 return err30 }31 err = txc.gateway.StartCommit(ctx, mmShard.Target, mmShard.TransactionId, dtid)32 if err != nil {33 return err34 }35 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {36 return txc.gateway.CommitPrepared(ctx, s.Target, dtid)37 })38 if err != nil {39 return err40 }41 return txc.gateway.ConcludeTransaction(ctx, mmShard.Target, dtid)42 }
從程式碼2~4(表示第2行到第4行,下同)可以看到,對於只涉及一個Shard的事務,不會涉及部分提交問題,MySQL的事務就可以保證原子性,不需要走兩階段提交,直接提交即可。
兩階段提交主要分為以下幾個步驟
1、生成分散式的事務id (11~12)
分散式的事務id為第一個參與者的Shard資訊+該Shard的本地事務id,這裡我們簡單的認為是Shard1:TX1
,我們將該Shard1稱之Shard1:TX1
這個分散式事務的MetadataManager Shard
,因為Shard1上管理該分散式事務的後設資料,縮寫成mmShard
。
注意Shard1
是Shard1:TX1
這個事務的mmShard
,因為該分散式事務的第一個參與者是Shard1
。
如果分散式事務的參與者是Shard3
上面的TX3
和Shard2
上面的TX2
,Shard3
是第一個參與者,那麼該分散式事務的id為Shard3:TX3
。Shard3
為該分散式事務的mmShard
。這樣mmShard能夠比較均衡的分散式在所有Shard中,一方面可以避免對mmShard操作造成熱點,另外也可以避免單個Shard故障影響整個Vitess叢集。
分散式事務id定義成Shard:TX
是有意義的,這樣後續的流程中可以根據分散式事務id直接知道該分散式事務的後設資料資訊在哪個Shard上面,方便讀取。
2、在mmShard記錄後設資料資訊 (13~18)
記錄分散式事務的後設資料資訊到mmShard的MySQL資料庫中(也就是記錄到Shard1的MySQL中),記錄的資訊包括分散式事務id、事務當前狀態、開始時間,其他參與者(participants
)
分散式事務id 事務當前狀態 事務開始時間 事務的其他所有參與者資訊 Shard1:TX1 初始Prepare狀態 比如1989-09-20 00:00:00 Shard2:TX2
此處所有的記錄操作不是在Shard1的事務TX1,而是單獨使用了一個連線,因為記錄的後設資料資訊需要直接提交寫入磁碟,如果放入TX1中就連同TX1一起提交了。
3、Prepare階段 (20-22)
給所有參與者傳送Prepare指令,參與者接收到帶有本身事務id的Prepare指令之後
1. 透過本地事務id(TX1、TX2)將事務連線從普通的事務連線池拿出來放到一個叫做preparedPool
的特有連線池,preparedPool
透過 分散式事務id作為key,也就是Shard1和Shard2都是透過Shard1:TX1
作為key,SESSION1和SESSION2 使用的事務連線作為value儲存到preparedPool
中。
注意普通事務連線池和preparedPool
不一樣的地方是,普通事務連線池以本地事務id作為key儲存、查詢的,preparedPool
中的連線是分散式事務id作為key來儲存查詢的。問題1,為什麼需要一個特殊的preparedPool
呢?
2. 將之前事務TX1、TX2裡記錄的redo log,也就是在事務裡執行的SQL對於的全域性事務id號 Shard1:TX1
,記錄到本地資料庫中,注意是需要使用單獨的新的連線,寫入資料庫並提交(此時不能使用TX1、TX2使用的連線提交redo log,這樣就把TX1、TX2一起提交了)。redo log寫入到資料庫之後,即便TX1、TX2沒有提交、即便vttablet發升重啟、如果需要我們也可以重新執行redo log。
4、StartCommit階段 (30~34)
在步驟2,我們將事務的後設資料資訊記錄到了mmShard(也就是Shard1)的MySQL資料庫中,記錄的狀態是Prepare狀態。現在將Prepare狀態修改成為Committed
狀態。這是一個關鍵步驟,修改該狀態之前出任何異常我們都認為事務尚未提交,修改成為Committed
狀態之後,出任何異常我們都認為事務已經提交。出現異常之後,我們需要做的就是根據記錄下的該狀態做相應的補償措施。
分散式事務id 事務當前狀態 事務開始時間 事務的其他所有參與者資訊 Shard1:TX1 Committed狀態 比如1989-09-20 00:00:00 Shard2:TX2
在Shard1上記錄事務狀態標記為Committed
之後,我們就認為兩階段提交成功了,所以這時候我們可以順便把Shard1上面的TX1也提交了。使用單獨的連線先修改事務狀態再提交TX1,那不如直接在TX1裡修改當前事務狀態,直接提交TX1,效果一樣一樣的。
在是否在TX1裡直接修改狀態,也是一個有意思的問題,如果直接在TX1裡修改狀態,之後馬上提交,那麼修改狀態和提交操作在一個事務裡,兩個行為必然是原子操作,如果沒有在TX1裡修改事務狀態,而是使用了單獨的連線修改,那麼我們在做異常處理的時候就需要多考慮一種情況,狀態修改成功但是TX1未提交成功。
5、CommitPrepare階段(35~37)
vtgate給出mmShard之外的其他參與者傳送CommitPrepare
指令,分散式事務id Shard1:TX1
作為引數
因為Shard1是mmShard,mmShard在StartCommit
階段已經把事務提交了,這裡以Shard2為例子說明,Shard2上面的vttablet接受到CommitPrepare
指令之後,根據分散式事務id Shard1:TX1
去preparedPool
裡面取出之前的事務連線,TX2執行了白條扣款
但是尚未提交。之前提到,在Prepare階段,我們還在本地資料庫裡記錄了redo log和對應的全域性事務id,這時候拿到了事務連線,先根據全域性事務id刪除redo log,然後執行Commit。
和StartCommit步
驟一樣,刪除redo log直接放到TX2事務中,這樣能保證兩個操作的原子性,避免部分成功,否則我們還需要多處理一種異常。
6、ConcludeTransaction結束分散式事務(41~41)
只需要根據分散式事務id Shard1:TX1
刪除Shard1上面的後設資料資訊
異常分析
上面是正常的兩階段提交流程,但是兩階段最核心的任務是處理異常,下面我們看看Vitess是如何處理各種異常的。
1. 建立分散式事務異常(14~17)
對所有Shard傳送Rollback
指令,這樣Shard1回滾TX1,Shard2回滾TX2。如果是Shard1節點異常導致的寫入分散式事務異常,那麼Shard1回滾也會報錯,不過沒關係,反正事務TX1沒有提交。Shard1回滾異常也不影響其他Shard執行回滾操作。
// Rollback rolls back the current transaction. There are no retries on this operation.func (txc *TxConn) Rollback(ctx context.Context, session *SafeSession) error { if !session.InTransaction() { return nil } defer session.Reset() return txc.runSessions(session.ShardSessions, func(s *vtgatepb.Session_ShardSession) error { return txc.gateway.Rollback(ctx, s.Target, s.TransactionId) }) }
對萬一在mmShard已經將分散式事務的後設資料寫入了呢?這個問題和後面的ConcludeTransaction
類似。
2. Prepare異常(24~27)
建立事務,寫事務後設資料資訊到Shard1(mmShard)成功,Prepare階段異常,因為當前事務狀態仍然是Prepared狀態(未提交狀態),按照我們理解如果Prepare異常直接rollback就好了,但是在commit2PC函式中並未直接rollback,而是呼叫了Resolve
函式,為什麼呢不直接rollback而是Resolve呢?我們先看Resolve函式的實現。
// Resolve resolves the specified 2PC transaction.1func (txc *TxConn) Resolve(ctx context.Context, dtid string) error {2 mmShard, err := dtids.ShardSession(dtid)3 if err != nil {4 return err5 }67 transaction, err := txc.gateway.ReadTransaction(ctx, mmShard.Target, dtid)8 if err != nil {9 return err10 }11 if transaction == nil || transaction.Dtid == "" {12 // It was already resolved.13 return nil14 }15 switch transaction.State {16 case querypb.TransactionState_PREPARE:17 // If state is PREPARE, make a decision to rollback and18 // fallthrough to the rollback workflow.19 if err := txc.gateway.SetRollback(ctx, mmShard.Target, transaction.Dtid, mmShard.TransactionId); err != nil {20 return err21 }22 fallthrough23 case querypb.TransactionState_ROLLBACK:24 if err := txc.resumeRollback(ctx, mmShard.Target, transaction); err != nil {25 return err26 }27 case querypb.TransactionState_COMMIT:28 if err := txc.resumeCommit(ctx, mmShard.Target, transaction); err != nil {29 return err30 }31 default:32 // Should never happen.33 return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "invalid state: %v", transaction.State)34 }35 return nil36}
建立首先根據事務idShard1:TX1
拿到mmShard,也就是Shard1,然後去Shard1讀取該事務的後設資料資訊,發現事務是Prepare狀態,尚未提交:
分散式事務id 事務當前狀態 事務開始時間 事務的其他所有參與者資訊 Shard1:TX1 Prepared狀態 比如1989-09-20 00:00:00 Shard2:TX2
建立首先修改mmShard事務狀態為回滾狀態SetRollback
分散式事務id 事務當前狀態 事務開始時間 事務的其他所有參與者資訊 Shard1:TX1 Rollbck狀態 比如1989-09-20 00:00:00 Shard2:TX2
建立Resolve函式中22行fallthrough
表示,如果SetRollback
成功無異常,那麼繼續執行下一個case,即resumeRollback
建立透過下面的resumeRollback函式我們明白了,不能簡單的執行Rollback操作是因為可能一些分片已經Prepare成功並記錄了redo log,我們需要刪除redo log;
此外,上面的Prepare階段提到過,Prepare會把事務連線以分散式事務idShard1:TX1
作為key,儲存到 preparedPool
中,現在回滾Prepare,還需要將preparedPool
中的以Shard1:TX1
為key的連線放回到普通的事務連線池,這樣後面的事務請求可以繼續使用,避免連線洩露。
所以我們是需要做一些對之前Prepare操作的清理工作,對每個Shard執行RollbackPrepared
操作,刪除redo log,將連線從preparedPool
放回事務連線池避免連線洩露,。
1 func (txc *TxConn) resumeRollback(ctx context.Context, target *querypb.Target, transaction *querypb.TransactionMetadata) error {2 err := txc.runTargets(transaction.Participants, func(t *querypb.Target) error {3 return txc.gateway.RollbackPrepared(ctx, t, transaction.Dtid, 0)4 })5 if err != nil {6 return err7 }8 return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)9}
最後,刪除mmShard上面記錄的事務後設資料資訊,resumeRollback函式第9行刪除mmShard的後設資料資訊,結束分散式事務。
3. StartCommit異常 (32~34)
建立前面提到過,StartCommit就是修改分散式事務狀態,這是一個原子性的操作,修改事務狀態只要修改成為Committed狀態,就認為事務是提交狀態。如果狀態還是prepared狀態,那麼就認為事務沒有提交。
當然StartCommit也可以發生異常,返回錯誤,甚至資料庫已經修改成功,但是網路或者其他原因,vtgate沒有收到,仍然需要進行錯誤處理。此處的錯誤處理邏輯和CommitPrepared是一樣的。此時給客戶端返回一個錯誤碼。
對於這種異常的兩階段提交,Vitess採取了補償策略。
建立Vitess的策略是在每個Shard的vttablet上開啟一個watchDog
。watchDog
定時去去讀儲存在MySQL上面的分散式事務後設資料資訊,只有事務執行完成才會將後設資料資訊刪除。
前面我們提到,mmShard記錄了事務的開始時間,那vttablet又定義了一個事務超時時間,比如10秒。對於超過10秒未刪除的分散式事務,vttablet自動給vtgate叢集(注意這裡是叢集,可以是任何一個vtgate)傳送ResolveTransaction
指令,告訴vtgate可能有事務異常了,需要vtgate看看咋回事,ResolveTransaction傳遞一個分散式事務id作引數,比如Shard1:TX1
。vtgate收到ResolveTransaction指令後,對分散式事務進行Resolve,Resolve函式前面已經介紹過一次,現在又派上用場了。
回到上面的StartCommit異常,可能StartCommit已經修改事務狀態未成功,事務狀態仍然為prepare。這時候和處理prepare異常一致,RollbackPrepare。
如果修改事務狀態成功,事務狀態已經成為Committed狀態。從Resolve函式中(27~30)可以看到,執行resumeCommit。
1 func (txc *TxConn) resumeCommit(ctx context.Context, target *querypb.Target, transaction *querypb.TransactionMetadata) error {2 err := txc.runTargets(transaction.Participants, func(t *querypb.Target) error {3 return txc.gateway.CommitPrepared(ctx, t, transaction.Dtid)4 })5 if err != nil {6 return err7 }8 return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)9 }
從程式碼中可以看出,resumeCommit其實就是再StartCommit的基礎上繼續往下執行,繼續對每個Shard執行CommitPrepared,然後執行ConcludeTransaction刪除mmShard上的事務員資料。這時候某些Shard可能已經成功執行過CommitPrepared,這樣可能會重複執行。嗯,沒錯,CommitPrepared函式是冪等的,呼叫一次和呼叫多次效果一致。
4. CommitPrepared異常 (38~40)
最後一步驟,提交事務,Shard1提交TX1
,Shard2提交TX2
。
此處CommitPrepared函式的引數是分散式事務id,即Shard1:TX1
。Shard1和Shard2的vttablet接受到vtgate傳送的rpc請求後根據引數分散式事務id去preparedPool
裡拿到TX1和TX2所使用的事務連線,然後使用該連線,也就是在TX1和TX2兩個事務中,分別刪除對於的redo log,然後提交事務,同樣在TX1和TX2中刪除redo log保證了,刪除redo log和提交事務是一個原子操作。
該流程可能是commit本地事務異常,也可能是commit都成功返回成功資訊時候網路異常等。
無論何種異常gate收到的是一個錯誤,這時候,gate只能給客戶端返回一個錯誤碼。
對於CommitPrepared異常處理策略和上面一樣,vttablet上面的watchDog發現本地MySQL有記錄分散式事務超時,根據傳送分散式事務id到vtgate,vtgate對分散式事務進行Resolve。
5. ConcludeTransaction異常
ConcludeTransaction
只是將mmShard的後設資料刪除,刪除操作可能成功可能失敗,最終返回給客戶端一個錯誤碼。
對於ConcludeTransaction
和上面3、4異常處理策略一樣。都會執行resumeCommit
。
resumeCommit
會呼叫冪等的CommitPrepared
,然後呼叫ConcludeTransaction
刪除mmShard上的分散式事務後設資料資訊。
同樣ConcludeTransaction
操作也是冪等的。
6. 其他異常
事務長時間未完成
vttablet配置了分散式事務超時時間,mmShard記錄了事務開始時間,長時間未提交自動發起Resolve,避免長時間未提交的事務。vtgate當機
在commit2PC函式中,執行到任何一個步驟vtgate都可能當機,但是透過上面的錯誤分析得知,對於任何異常,要麼回滾要麼補償,Vitess都可以避免部分提交。
此外,vttablet需要知道協調者vtgate地址,對於長時間未提交事務發起Resolve。vtgate是一個叢集,避免單點故障。
Resolve的引數是分散式事務id。根據分散式事務id,我們能拿到mmShard。從mmShard上能讀到分散式事務的後設資料資訊(不管mmShard和vtgate是否發生過當機、重啟、網路異常等)。有了事務的後設資料資訊,就可以發起Resolve操作了。vttablet或者MySQL當機
vttablet或者MySQL當機、重啟、或者發生主從切換之後,需要從程式碼邏輯上保證各個介面能夠正常服務,比如下面的問題1。
幾個小問題
為什麼我們需要一個
preparedPool
而不是直接複用之前的activePool
?
因為當vttablet或者MySQL重啟後,我們需要能夠從之前記錄的redo log中恢復回來。preparedPool
是以分散式事務id作為key的。當vttablet重啟,初始化過程就從MySQL中去取redo log(prepare步驟記錄下來的SQL就是redo log已經對應的分散式事務id),有redo log就說明有分散式事務未完成。然後對每個分散式事務分配一個連線,重新執行redo log,然後將該連線放到preparedPool
。這樣即便vttablet發生重啟,我們可以重新生成一個preparedPool
,vttablet仍然可以執行vtgate在Commit或者Resolve時候傳送過來的CommitPrepared
或者RollbackPrepared
指令,從而保證分散式事務的正常執行。vttablet發起Resolve對介面的呼叫和vtgate正常事務流程對介面的呼叫是否有可能併發?
有可能,vtgate第一步記錄分散式事務的後設資料資訊,但是事務超過10秒未完成。那麼vttablet自動發起Resolve,發現事務未提交呼叫RollbackPrepared
,同時有可能vtgate因為某些異常也發起了RollbackPrepared
。CommitPrepared
、ConcludeTransaction
也有一樣的問題。所以Resolve過程中呼叫的介面需要考慮到併發、冪等性等。應用端執行commit返回錯誤就是事務提交失敗麼?
不一定,透過前面的例子我們可以看到,可能兩階段提交前面都成功了,只是最後刪除mmShard上面的後設資料失敗了,還是會返回給應用端一個error。
甚至所有步驟都成功了,要正常返回給應用端OK包了,斷網了,應用端也是會收到網路異常錯誤,這就需要應用端有合適的補償機制處理這種異常。Vitess中的兩階段提交事務滿足文章開頭提到的事務四個特性麼?
不滿足,Vitess的兩階段提交只是保證了分散式事務的原子性,即便使用兩階段提交,在Vitess中是有可能讀取到部分提交結果的。 但是兩階段提交是分散式中經典問題,也是最基礎的演算法,幾乎所有的複雜的分散式演算法都會使用到兩階段提交。Vitess中只有兩階段提交一種事務模型麼?
不是,除了兩階段提交外,Vitess還支援單節點事務,單節點事務強制要求事務只能在一個Shard執行;還有多節點事務,多節點事務對部分提交問題不做處理。Vitess中兩階段提交效能如何?
Vitess兩階段提交比一般的多節點事務會多四次vtgate到vttablet的互動,我的測試結果兩階段提交帶來的延遲在5毫秒以內。線上的交易訂單系統真是例子中這樣的麼?
複雜的多,首先訂單和白條可能都不在同一個大部門,比如訂單業務屬於商城交易部門,白條業務屬於金融部門。各個服務間可能透過WebService或者mq互動。其次業務也要複雜的多,一個下單操作可能涉及到幾十個甚至上百個介面的呼叫,各個介面能夠穩定的提供服務最主要的原因是依賴於基礎平臺的各種中介軟體,好了編不下去了,其實我不知道。不是說好的兩階段提交麼,Vitess為啥分了好幾個階段啊,這還是正經兩階段提交麼?
兩階段指的是協調者和參與者之間的互動主要分兩個階段。
兩階段提交第一步,在提交之前協調者vtgate向所有參與者vttablet發出是否可提交的詢問,CreateTransaction
相當於先記錄事務狀態,然後向mmShard發起了是否可提交的詢問,Prepare請求相當於向除mmShard之外的其他參與者發起了詢問。
兩階段提交第二步,是如果所有參與都答覆可以正常提交,那麼,協調者給所有參與者發出提交指令。Vitess中詢問之後得到了可以提交的答覆,首先透過StartCommit將狀態記錄下了,這樣可以避免協調組vtgate異常導致分散式事務異常。記錄可以提交狀態之後,協調者vtgate向所有參與者發出提交指令CommitPrepared
。Vitess做的一點最佳化是後設資料記錄在了mmShard,所以在記錄事務已經提交的步驟中,直接把mmShard上的事務提交了。
兩階段提交上面已經完成了,ConcludeTransaction
是做了一些清理後設資料的工作。
翻了一下事務處理,在講兩階段提交過程有明確指出[P14],
對於參與者的答覆,事務管理器將在日誌中記錄下這個事實。
Vitess做法是將這個狀態記錄到了mmShard。對於資源管理器重啟,事務處理[P14]中也提到:
作為重啟邏輯的一部分,資源管理器向事務管理器發出詢問,這時候事務管理器告訴他們 發生故障時每一個活動的事務的執行結果。一些可能提交了,一些可能終止了,一些可能仍在提交過程中。資源管理器可以獨立的恢復其已提交的狀態,也可以參與事務管理器對日誌重做或者撤銷的檢測。
Vitess也是資源管理器vttablet向事務管理器發出詢問,只不過事務管理器維護的事務日誌資訊還是放到了資源管理器,因為資源管理器有MySQL啊,正好儲存事務資訊不會丟。
所以Vitess的兩階段提交是正經的兩階段提交。而沒有實現上面兩點的兩階段提交是不能從錯誤正常恢復,也就不能保證原子性。
參考:
作者:許海華
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4422/viewspace-2821058/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 分散式:分散式事務(CAP、兩階段提交、三階段提交)分散式
- 分散式事務(二)之兩階段提交分散式
- 分散式事務--兩階段提交(2PC-Prepare/Commit)分散式MIT
- 分散式事務的兩階段提交和三階段提交分別有什麼優缺點?分散式
- 分散式事務處理兩階段提交機制和原理分散式
- [Mysql]兩階段提交MySql
- 分散式事務對於兩階段提交的錯誤處理分散式
- mysql兩階段提交和組提交MySql
- 分散式事務(二)之三階段提交分散式
- MySQL事務提交的三個階段介紹MySql
- 兩階段提交2PC 和 三階段提交3pc
- MySQL事務兩段式提交MySql
- 分散式基礎,啥是兩階段提交?分散式
- MySQL兩階段提交過程原理簡述MySql
- TCC和兩階段分散式事務處理的區別分散式
- 經典的兩階段提交演算法原理及缺陷演算法
- 全網最牛X的!!! MySQL兩階段提交串講MySql
- 關於2PC(二階段提交)和3PC(三階段提交)的理解
- mysql之 事務prepare 與 commit 階段分析MySqlMIT
- PHP分散式事務-兩段式提交 2PC(二)PHP分散式
- 三階段提交(Three-phase commit)MIT
- [分享] 使用 golang 理解 MySQL 的兩階段提交- 軒脈刃 de 刀光劍影GolangMySql
- 【分享】使用 golang 理解 mysql 的兩階段提交- 軒脈刃 de 刀光劍影GolangMySql
- 兩階段終止模式模式
- 事務單獨提交和
- 二階段提交協議(Two Phase Commitment Protocol)協議MITProtocol
- 08 MySQL兩階段認證MySql
- 測試階段注意事項
- SharePlex qview工具 vs OGG logdump工具探究兩個複製工具事務開始 or 事務提交複製?View
- Spring中的事務提交事件Spring事件
- .NetCore中使用分散式事務DTM的二階段訊息NetCore分散式
- Mysql 兩階段鎖和死鎖MySql
- MySQl事務建立,開始以及提交MySql
- 一致性協議之三階段提交協議
- SSL連線分為兩個階段:握手和資料傳輸階段
- 位元組跳動流式資料整合基於Flink Checkpoint兩階段提交的實踐和優化優化
- @Transactional註解管理事務和手動提交事務
- MySQL:begin後事務為什麼不提交MySql