Vitess之前漫雕,先復(fù)習(xí)一下事務(wù)的四個(gè)基本特性
-
原子性
:一個(gè)事務(wù)對(duì)狀態(tài)的改變是原子的罩抗,要么都發(fā)生拉庵,要么都不發(fā)生,這些改變包括數(shù)據(jù)庫(kù)的改變套蒂、消息以及對(duì)轉(zhuǎn)換器的操作钞支。 -
一致性
:一個(gè)事務(wù)是對(duì)狀態(tài)的一個(gè)正確改變。作為一組操作沒(méi)有違反任何與狀態(tài)相關(guān)的完整性約束操刀。這要求事務(wù)是一個(gè)正確的程序烁挟。 -
隔離性
:盡管事務(wù)是并發(fā)執(zhí)行的,但看起來(lái)是單個(gè)執(zhí)行的骨坑,即對(duì)于一個(gè)事務(wù)T撼嗓,任何其他事務(wù)要么在T之前執(zhí)行,要么在T之后執(zhí)行欢唾,但不會(huì)既在T之前執(zhí)行且警,又在T之后執(zhí)行。 -
永久性
:一旦一個(gè)事務(wù)成功完成(提交)礁遣,它對(duì)狀態(tài)的改變不會(huì)受其他失敗的影響斑芜。
????舉一個(gè)銀行取錢(qián)事務(wù)的例子。如果同時(shí)完成了取出錢(qián)和賬戶的更改亡脸,那就是原子的押搪。如果賬戶減少的錢(qián)等于取出的錢(qián)树酪,那么就是一致的。如果這個(gè)過(guò)程不受其他程序并發(fā)讀寫(xiě)你賬戶的程序的影響(比如你的女朋友們正在并發(fā)的刷你的銀行卡)大州,那么它是隔離的续语。一旦事務(wù)完成了,(無(wú)論完成之后機(jī)器宕機(jī)厦画、斷電疮茄、還是網(wǎng)絡(luò)異常)賬戶的余額必然會(huì)反映取款后的情況,那么它是永久性的根暑。
????事務(wù)是數(shù)據(jù)庫(kù)的核心特性力试,MySQL、ORACLE排嫌、PostgreSQL這些數(shù)據(jù)庫(kù)都是支持事務(wù)的畸裳。國(guó)內(nèi)互聯(lián)網(wǎng)公司更多的使用MySQL。
????但是互聯(lián)網(wǎng)公司的數(shù)據(jù)一般比較大淳地,單機(jī)數(shù)據(jù)庫(kù)服務(wù)難以承擔(dān)這么大的數(shù)據(jù)量怖糊。這時(shí)候一般會(huì)做分庫(kù),將原來(lái)一個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)拆分到多個(gè)庫(kù)(這里簡(jiǎn)單認(rèn)為一個(gè)MySQL實(shí)例上只有一個(gè)庫(kù))颇象,比用戶表做拆分伍伤,根據(jù)用戶id,id對(duì)64取余數(shù)遣钳,然后根據(jù)得到的余數(shù)定位特定的分庫(kù)扰魂。比如id等于1的用戶的信息就在第1個(gè)庫(kù),id等于2的用戶數(shù)據(jù)就在第2庫(kù)蕴茴,這樣就解決了單機(jī)數(shù)據(jù)容量問(wèn)題劝评。
????現(xiàn)在用戶1要下單了,需要白條扣款
和生成訂單
兩個(gè)步驟荐开。兩個(gè)步驟需要保證原子性的付翁,要么扣款完成、訂單生成晃听,要么不扣款百侧、不下單(all-or-nothing
)。如果訂單和白條表都是按照用戶id做拆分的那最好能扒,所有操作都會(huì)在同一個(gè)庫(kù)上面進(jìn)行佣渴,使用單機(jī)事務(wù),MySQL就保證了原子性初斑。如果白條表是按照訂單id拆分的辛润,那很可能不在第1分庫(kù)了??。這時(shí)候就涉及到分布式事務(wù)见秤。
本來(lái)事務(wù)的執(zhí)行流程應(yīng)該是這樣:
BEGIN;
生成訂單; 白條扣款;
COMMIT/ROLLBACK
????問(wèn)題就出在分布式場(chǎng)景中的提交事務(wù)砂竖。我們需要連接兩個(gè)庫(kù)開(kāi)啟兩個(gè)分庫(kù)的兩個(gè)事務(wù)真椿,TX1和TX2, TX1生成訂單
寫(xiě)入分庫(kù)1,TX2執(zhí)行白條扣款
寫(xiě)入分庫(kù)2乎澄。
在業(yè)務(wù)代碼里執(zhí)行時(shí)間線(t1到t5表示先后的時(shí)間點(diǎn))會(huì)是這樣的:
(t1)SESSION1 BEGIN TX1突硝;
(t2)SESSION2 BEGIN TX2;
(t3)SESSION1 生成訂單置济,訂單信息寫(xiě)入分庫(kù)1解恰;
(t4)SESSION2白條扣款寫(xiě)入分庫(kù)2;
(t5)SESSION1 COMMIT TX1浙于;
(t6)SESSION2 COMMIT TX2.
????如果t5執(zhí)行正常护盈,給客戶下單了,分庫(kù)2宕機(jī)了羞酗,提交TX2失敗腐宋,TX2自動(dòng)回滾。分布式事務(wù)只提交了一部分整慎,訂單生成了脏款,但是白條沒(méi)扣款(我做游戲的時(shí)候都是先扣錢(qián)、扣錢(qián)成功再發(fā)裝備??)裤园。這就經(jīng)典的是部分提交(PARTIAL COMMIT
)問(wèn)題。
????在分布式場(chǎng)景中保證原子性避免部分提交剂府,有一個(gè)辦法拧揽,那就是兩階段提交(TWO PHASE COMMIT
)
????上面的分布式事務(wù)涉及到了分庫(kù)1和分庫(kù)2。其實(shí)還有一個(gè)角色腺占,就是執(zhí)行業(yè)務(wù)邏輯的worker
節(jié)點(diǎn)淤袜,worker在兩階段提交過(guò)程中也是一個(gè)重要的角色,我們稱之為事務(wù)管理器(Transaction Manager
)衰伯,事務(wù)管理器在兩階段提交過(guò)程中是一個(gè)協(xié)調(diào)者的角色铡羡。事務(wù)管理器負(fù)責(zé)整個(gè)事務(wù)的開(kāi)始、執(zhí)行意鲸、回滾或者提交烦周。事務(wù)管理器異常也可能導(dǎo)致整個(gè)事務(wù)的異常,比如worker節(jié)點(diǎn)在t5之后怎顾,t6之前宕機(jī)读慎,也會(huì)導(dǎo)致整個(gè)事務(wù)的部分提交。我們也給分庫(kù)1和分庫(kù)2一個(gè)專業(yè)的名字槐雾,叫做資源管理器(Resource Manager
),資源管理器在分布式事務(wù)中是參與者的角色夭委,負(fù)責(zé)執(zhí)行協(xié)調(diào)者下達(dá)的指令,此外資源管理器是能夠支持本地事務(wù)的募强。
MySQL提供了XA分布式事務(wù)接口供外部協(xié)調(diào)者調(diào)用株灸。
關(guān)于XA事務(wù)的兩個(gè)概念:
資源管理器(
Resource Manager
):用來(lái)管理系統(tǒng)資源崇摄,是通向事務(wù)資源的途徑。數(shù)據(jù)庫(kù)就是一種資源管理器慌烧。資源管理還應(yīng)該具有管理事務(wù)提交或回滾的能力配猫。事務(wù)管理器(
Transaction Manager
):事務(wù)管理器是分布式事務(wù)的核心管理者。事務(wù)管理器與每個(gè)資源管理器(Resource Manager
)進(jìn)行通信杏死,協(xié)調(diào)并完成事務(wù)的處理泵肄。事務(wù)的各個(gè)分支由唯一命名進(jìn)行標(biāo)識(shí)。
上面例子中的生成訂單白條扣款的分布式事務(wù)中淑翼,MySQL服務(wù)器相當(dāng)于XA事務(wù)資源管理器腐巢,與MySQL鏈接的客戶端,也就是執(zhí)行業(yè)務(wù)邏輯的worker節(jié)點(diǎn)相當(dāng)于事務(wù)管理器玄括。
但是MySQL早期的版本XA事務(wù)存在BUG冯丙,Vitess自己實(shí)現(xiàn)了整套兩階段提交,所以Vitess是學(xué)習(xí)兩階段提交的一個(gè)很好的資源遭京。Vitess地址: https://github.com/vitessio/vitess
先引入Vitess的幾個(gè)概念:
上面是Vitess的整體架構(gòu)胃惜,兩階段提交過(guò)程中涉及到的模塊主要是vtgate
和 vttablet
、MySQL
哪雕。
vttablet
和MySQL
其實(shí)可以看做一個(gè)整體船殉,我們稱之為Shard
。Shard在兩階段提交中是參與者斯嚎,也就是資源管理器利虫。
vtgate
是事務(wù)管理器,也就是事務(wù)的協(xié)調(diào)者堡僻,業(yè)務(wù)的應(yīng)用Application
連接到vtgate
上面糠惫,vtgate
通過(guò)路由將業(yè)務(wù)的請(qǐng)求轉(zhuǎn)發(fā)到后端的一個(gè)或者多個(gè)Shard,然后在將結(jié)果匯總發(fā)生Application
钉疫。Application
發(fā)起一個(gè)事務(wù)硼讽,比如插入兩條數(shù)據(jù),根據(jù)vtgate
做路由后可能將兩條數(shù)據(jù)插入到一個(gè)或者兩個(gè)Shard牲阁。
上圖中的三個(gè)Shard是三個(gè)資源管理器固阁,也是兩階段提交的參與者。vtgate是事務(wù)管理器咨油,是兩階段提交的的協(xié)調(diào)者您炉。
??vtgate到vttablet請(qǐng)求是通過(guò)grpc實(shí)現(xiàn),vttablet維護(hù)了到MySQL的連接池役电。
??對(duì)于非事務(wù)請(qǐng)求赚爵,vtgate請(qǐng)求發(fā)送到vttablet,vttablet隨便從連接池里拿到一個(gè)連接,執(zhí)行SQL冀膝,返回結(jié)果給vtgate唁奢,然后將連接放回到連接池,這樣一個(gè)請(qǐng)求單獨(dú)占用連接的時(shí)間很短窝剖。
??對(duì)于事務(wù)請(qǐng)求麻掸,vtgate發(fā)生請(qǐng)求到vttablet,vttablet會(huì)從事務(wù)連接池里拿到一個(gè)連接赐纱,并生產(chǎn)一個(gè)本地事務(wù)id脊奋,執(zhí)行SQL,將執(zhí)行結(jié)果和本地事務(wù)id返回給vtgate疙描,最后將事務(wù)連接放入一個(gè)單獨(dú)的activePool
诚隙,這樣下次vtgate需要在該事務(wù)里繼續(xù)執(zhí)行SQL,只需要請(qǐng)求帶著事務(wù)id起胰,通過(guò)事務(wù)id從activePool
拿到事務(wù)連接淮菠,執(zhí)行SQL即可涯穷,直到vtgate提交或者回滾事務(wù),vttablet才將連接從activePool
放回到事務(wù)連接池鳄乏。
下面的代碼就是vttablet根據(jù)本地事務(wù)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的兩階段提交實(shí)現(xiàn)
回到上面的分布式事務(wù)徽龟,vtgate要在第一個(gè)Shard和第二個(gè)Shard寫(xiě)入數(shù)據(jù)绩聘,vtgate在兩個(gè)SESSION里面開(kāi)啟兩個(gè)事務(wù)踢代,執(zhí)行并記錄SQL。
(t1)SESSION1 BEGIN TX1 IN Shard1;
(t2)SESSION2 BEGIN TX2 IN Shard2
(t3)SESSION1 生成訂單寫(xiě)入Shard1未提交瓜客,將對(duì)應(yīng)SQL語(yǔ)句作為redo log記錄在內(nèi)存中
(t4)SESSION2白條扣款寫(xiě)入Shard2未提交适瓦,將對(duì)應(yīng)SQL語(yǔ)句作為redo log記錄在內(nèi)存中
????除了把事務(wù)中執(zhí)行的SQL記錄在內(nèi)存中之外,前面四個(gè)步驟和之前沒(méi)有任何太多區(qū)別谱仪,區(qū)別就在最后的提交階段。下面我們結(jié)合代碼看Vitess是如何做兩階段提交的否彩。
下面是Vitess中vtgate作為協(xié)調(diào)者的代碼模塊疯攒,結(jié)合代碼分析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 }
5
6 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 err
18 }
19
20 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {
21 return txc.gateway.Prepare(ctx, s.Target, s.TransactionId, dtid)
22 })
23
24 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 err
30 }
31 err = txc.gateway.StartCommit(ctx, mmShard.Target, mmShard.TransactionId, dtid)
32 if err != nil {
33 return err
34 }
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 err
40 }
41 return txc.gateway.ConcludeTransaction(ctx, mmShard.Target, dtid)
42 }
從代碼2~4(表示第2行到第4行,下同)可以看到列荔,對(duì)于只涉及一個(gè)Shard的事務(wù)敬尺,不會(huì)涉及部分提交問(wèn)題,MySQL的事務(wù)就可以保證原子性贴浙,不需要走兩階段提交砂吞,直接提交即可。
兩階段提交主要分為以下幾個(gè)步驟
1崎溃、生成分布式的事務(wù)id (11~12)
????分布式的事務(wù)id為第一個(gè)參與者的Shard信息+該Shard的本地事務(wù)id蜻直,這里我們簡(jiǎn)單的認(rèn)為是Shard1:TX1
,我們將該Shard1稱之Shard1:TX1
這個(gè)分布式事務(wù)的MetadataManager Shard
,因?yàn)镾hard1上管理該分布式事務(wù)的元數(shù)據(jù)概而,縮寫(xiě)成mmShard
呼巷。
????注意Shard1
是Shard1:TX1
這個(gè)事務(wù)的mmShard
,因?yàn)樵摲植际绞聞?wù)的第一個(gè)參與者是Shard1
赎瑰。
????如果分布式事務(wù)的參與者是Shard3
上面的TX3
和Shard2
上面的TX2
王悍,Shard3
是第一個(gè)參與者,那么該分布式事務(wù)的id為Shard3:TX3
餐曼。Shard3
為該分布式事務(wù)的mmShard
压储。這樣mmShard能夠比較均衡的分布式在所有Shard中,一方面可以避免對(duì)mmShard操作造成熱點(diǎn)源譬,另外也可以避免單個(gè)Shard故障影響整個(gè)Vitess集群集惋。
????分布式事務(wù)id定義成Shard:TX
是有意義的,這樣后續(xù)的流程中可以根據(jù)分布式事務(wù)id直接知道該分布式事務(wù)的元數(shù)據(jù)信息在哪個(gè)Shard上面瓶佳,方便讀取芋膘。
2、在mmShard記錄元數(shù)據(jù)信息 (13~18)
????記錄分布式事務(wù)的元數(shù)據(jù)信息到mmShard的MySQL數(shù)據(jù)庫(kù)中(也就是記錄到Shard1的MySQL中)霸饲,記錄的信息包括分布式事務(wù)id为朋、事務(wù)當(dāng)前狀態(tài)、開(kāi)始時(shí)間厚脉,其他參與者(participants
)
分布式事務(wù)id 事務(wù)當(dāng)前狀態(tài) 事務(wù)開(kāi)始時(shí)間 事務(wù)的其他所有參與者信息
Shard1:TX1 初始Prepare狀態(tài) 比如1989-09-20 00:00:00 Shard2:TX2
????此處所有的記錄操作不是在Shard1的事務(wù)TX1习寸,而是單獨(dú)使用了一個(gè)連接,因?yàn)橛涗浀脑獢?shù)據(jù)信息需要直接提交寫(xiě)入磁盤(pán)傻工,如果放入TX1中就連同TX1一起提交了霞溪。
3、Prepare階段 (20-22)
????給所有參與者發(fā)送Prepare指令中捆,參與者接收到帶有本身事務(wù)id的Prepare指令之后
????1. ?通過(guò)本地事務(wù)id(TX1鸯匹、TX2)將事務(wù)連接從普通的事務(wù)連接池拿出來(lái)放到一個(gè)叫做preparedPool
的特有連接池,preparedPool
通過(guò) 分布式事務(wù)id作為key泄伪,也就是Shard1和Shard2都是通過(guò)Shard1:TX1
作為key殴蓬,SESSION1和SESSION2 使用的事務(wù)連接作為value存儲(chǔ)到preparedPool
中。
????注意普通事務(wù)連接池和preparedPool
不一樣的地方是蟋滴,普通事務(wù)連接池以本地事務(wù)id作為key存儲(chǔ)染厅、查找的,preparedPool
中的連接是分布式事務(wù)id作為key來(lái)存儲(chǔ)查找的津函。問(wèn)題1肖粮,為什么需要一個(gè)特殊的preparedPool
呢?
????2.?將之前事務(wù)TX1尔苦、TX2里記錄的redo log涩馆,也就是在事務(wù)里執(zhí)行的SQL對(duì)于的全局事務(wù)id號(hào) Shard1:TX1
行施,記錄到本地?cái)?shù)據(jù)庫(kù)中,注意是需要使用單獨(dú)的新的連接凌净,寫(xiě)入數(shù)據(jù)庫(kù)并提交(此時(shí)不能使用TX1悲龟、TX2使用的連接提交redo log,這樣就把TX1冰寻、TX2一起提交了)须教。redo log寫(xiě)入到數(shù)據(jù)庫(kù)之后,即便TX1斩芭、TX2沒(méi)有提交轻腺、即便vttablet發(fā)升重啟、如果需要我們也可以重新執(zhí)行redo log划乖。
4贬养、StartCommit階段 (30~34)
????在步驟2,我們將事務(wù)的元數(shù)據(jù)信息記錄到了mmShard(也就是Shard1)的MySQL數(shù)據(jù)庫(kù)中琴庵,記錄的狀態(tài)是Prepare狀態(tài)∥笏悖現(xiàn)在將Prepare狀態(tài)修改成為Committed
狀態(tài)。這是一個(gè)關(guān)鍵步驟迷殿,修改該狀態(tài)之前出任何異常我們都認(rèn)為事務(wù)尚未提交儿礼,修改成為Committed
狀態(tài)之后,出任何異常我們都認(rèn)為事務(wù)已經(jīng)提交庆寺。出現(xiàn)異常之后蚊夫,我們需要做的就是根據(jù)記錄下的該狀態(tài)做相應(yīng)的補(bǔ)償措施。
分布式事務(wù)id 事務(wù)當(dāng)前狀態(tài) 事務(wù)開(kāi)始時(shí)間 事務(wù)的其他所有參與者信息
Shard1:TX1 Committed狀態(tài) 比如1989-09-20 00:00:00 Shard2:TX2
???? 在Shard1上記錄事務(wù)狀態(tài)標(biāo)記為Committed
之后懦尝,我們就認(rèn)為兩階段提交成功了知纷,所以這時(shí)候我們可以順便把Shard1上面的TX1也提交了。使用單獨(dú)的連接先修改事務(wù)狀態(tài)再提交TX1陵霉,那不如直接在TX1里修改當(dāng)前事務(wù)狀態(tài)琅轧,直接提交TX1,效果一樣一樣的踊挠。
???? 在是否在TX1里直接修改狀態(tài)鹰晨,也是一個(gè)有意思的問(wèn)題,如果直接在TX1里修改狀態(tài)止毕,之后馬上提交,那么修改狀態(tài)和提交操作在一個(gè)事務(wù)里漠趁,兩個(gè)行為必然是原子操作扁凛,如果沒(méi)有在TX1里修改事務(wù)狀態(tài),而是使用了單獨(dú)的連接修改闯传,那么我們?cè)谧霎惓L幚淼臅r(shí)候就需要多考慮一種情況谨朝,狀態(tài)修改成功但是TX1未提交成功。
5、CommitPrepare階段(35~37)
????vtgate給出mmShard之外的其他參與者發(fā)送CommitPrepare
指令字币,分布式事務(wù)id Shard1:TX1
作為參數(shù)
????因?yàn)镾hard1是mmShard则披,mmShard在StartCommit
階段已經(jīng)把事務(wù)提交了,這里以Shard2為例子說(shuō)明洗出,Shard2上面的vttablet接受到CommitPrepare
指令之后士复,根據(jù)分布式事務(wù)id Shard1:TX1
去preparedPool
里面取出之前的事務(wù)連接,TX2執(zhí)行了白條扣款
但是尚未提交翩活。之前提到阱洪,在Prepare階段,我們還在本地?cái)?shù)據(jù)庫(kù)里記錄了redo log和對(duì)應(yīng)的全局事務(wù)id菠镇,這時(shí)候拿到了事務(wù)連接冗荸,先根據(jù)全局事務(wù)id刪除redo log,然后執(zhí)行Commit利耍。
????和StartCommit步
驟一樣蚌本,刪除redo log直接放到TX2事務(wù)中,這樣能保證兩個(gè)操作的原子性隘梨,避免部分成功程癌,否則我們還需要多處理一種異常。
6出嘹、ConcludeTransaction結(jié)束分布式事務(wù)(41~41)
????只需要根據(jù)分布式事務(wù)id Shard1:TX1
刪除Shard1上面的元數(shù)據(jù)信息
異常分析
????上面是正常的兩階段提交流程席楚,但是兩階段最核心的任務(wù)是處理異常,下面我們看看Vitess是如何處理各種異常的税稼。
1. 創(chuàng)建分布式事務(wù)異常(14~17)
????對(duì)所有Shard發(fā)送Rollback
指令烦秩,這樣Shard1回滾TX1,Shard2回滾TX2郎仆。如果是Shard1節(jié)點(diǎn)異常導(dǎo)致的寫(xiě)入分布式事務(wù)異常只祠,那么Shard1回滾也會(huì)報(bào)錯(cuò),不過(guò)沒(méi)關(guān)系扰肌,反正事務(wù)TX1沒(méi)有提交抛寝。Shard1回滾異常也不影響其他Shard執(zhí)行回滾操作。
// 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)
})
}
????對(duì)萬(wàn)一在mmShard已經(jīng)將分布式事務(wù)的元數(shù)據(jù)寫(xiě)入了呢曙旭?這個(gè)問(wèn)題和后面的ConcludeTransaction
類似盗舰。
2. Prepare異常(24~27)
????創(chuàng)建事務(wù),寫(xiě)事務(wù)元數(shù)據(jù)信息到Shard1(mmShard)成功桂躏,Prepare階段異常钻趋,因?yàn)楫?dāng)前事務(wù)狀態(tài)仍然是Prepared狀態(tài)(未提交狀態(tài)),按照我們理解如果Prepare異常直接rollback就好了,但是在commit2PC函數(shù)中并未直接rollback剂习,而是調(diào)用了Resolve
函數(shù)蛮位,為什么呢不直接rollback而是Resolve呢较沪?我們先看Resolve函數(shù)的實(shí)現(xiàn)。
// 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 err
5 }
6
7 transaction, err := txc.gateway.ReadTransaction(ctx, mmShard.Target, dtid)
8 if err != nil {
9 return err
10 }
11 if transaction == nil || transaction.Dtid == "" {
12 // It was already resolved.
13 return nil
14 }
15 switch transaction.State {
16 case querypb.TransactionState_PREPARE:
17 // If state is PREPARE, make a decision to rollback and
18 // fallthrough to the rollback workflow.
19 if err := txc.gateway.SetRollback(ctx, mmShard.Target, transaction.Dtid, mmShard.TransactionId); err != nil {
20 return err
21 }
22 fallthrough
23 case querypb.TransactionState_ROLLBACK:
24 if err := txc.resumeRollback(ctx, mmShard.Target, transaction); err != nil {
25 return err
26 }
27 case querypb.TransactionState_COMMIT:
28 if err := txc.resumeCommit(ctx, mmShard.Target, transaction); err != nil {
29 return err
30 }
31 default:
32 // Should never happen.
33 return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "invalid state: %v", transaction.State)
34 }
35 return nil
36}
????創(chuàng)建首先根據(jù)事務(wù)idShard1:TX1
拿到mmShard,也就是Shard1失仁,然后去Shard1讀取該事務(wù)的元數(shù)據(jù)信息,發(fā)現(xiàn)事務(wù)是Prepare狀態(tài)尸曼,尚未提交:
分布式事務(wù)id 事務(wù)當(dāng)前狀態(tài) 事務(wù)開(kāi)始時(shí)間 事務(wù)的其他所有參與者信息
Shard1:TX1 Prepared狀態(tài) 比如1989-09-20 00:00:00 Shard2:TX2
????創(chuàng)建首先修改mmShard事務(wù)狀態(tài)為回滾狀態(tài)SetRollback
分布式事務(wù)id 事務(wù)當(dāng)前狀態(tài) 事務(wù)開(kāi)始時(shí)間 事務(wù)的其他所有參與者信息
Shard1:TX1 Rollbck狀態(tài) 比如1989-09-20 00:00:00 Shard2:TX2
????創(chuàng)建Resolve函數(shù)中22行fallthrough
表示,如果SetRollback
成功無(wú)異常萄焦,那么繼續(xù)執(zhí)行下一個(gè)case控轿,即resumeRollback
????創(chuàng)建通過(guò)下面的resumeRollback函數(shù)我們明白了,不能簡(jiǎn)單的執(zhí)行Rollback操作是因?yàn)榭赡芤恍┓制呀?jīng)Prepare成功并記錄了redo log楷扬,我們需要?jiǎng)h除redo log解幽;
????此外,上面的Prepare階段提到過(guò)烘苹,Prepare會(huì)把事務(wù)連接以分布式事務(wù)idShard1:TX1
作為key躲株,存儲(chǔ)到 preparedPool
中,現(xiàn)在回滾Prepare镣衡,還需要將preparedPool
中的以Shard1:TX1
為key的連接放回到普通的事務(wù)連接池霜定,這樣后面的事務(wù)請(qǐng)求可以繼續(xù)使用,避免連接泄露廊鸥。
????所以我們是需要做一些對(duì)之前Prepare操作的清理工作望浩,對(duì)每個(gè)Shard執(zhí)行RollbackPrepared
操作,刪除redo log惰说,將連接從preparedPool
放回事務(wù)連接池避免連接泄露磨德,。
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 err
7 }
8 return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)
9}
最后吆视,刪除mmShard上面記錄的事務(wù)元數(shù)據(jù)信息典挑,resumeRollback函數(shù)第9行刪除mmShard的元數(shù)據(jù)信息,結(jié)束分布式事務(wù)啦吧。
3. StartCommit異常 (32~34)
????創(chuàng)建前面提到過(guò)您觉,StartCommit就是修改分布式事務(wù)狀態(tài),這是一個(gè)原子性的操作授滓,修改事務(wù)狀態(tài)只要修改成為Committed狀態(tài)琳水,就認(rèn)為事務(wù)是提交狀態(tài)。如果狀態(tài)還是prepared狀態(tài)般堆,那么就認(rèn)為事務(wù)沒(méi)有提交在孝。
????當(dāng)然StartCommit也可以發(fā)生異常,返回錯(cuò)誤淮摔,甚至數(shù)據(jù)庫(kù)已經(jīng)修改成功浑玛,但是網(wǎng)絡(luò)或者其他原因,vtgate沒(méi)有收到噩咪,仍然需要進(jìn)行錯(cuò)誤處理顾彰。此處的錯(cuò)誤處理邏輯和CommitPrepared是一樣的。此時(shí)給客戶端返回一個(gè)錯(cuò)誤碼胃碾。
????對(duì)于這種異常的兩階段提交涨享,Vitess采取了補(bǔ)償策略。
????創(chuàng)建Vitess的策略是在每個(gè)Shard的vttablet上開(kāi)啟一個(gè)watchDog
仆百。watchDog
定時(shí)去去讀存儲(chǔ)在MySQL上面的分布式事務(wù)元數(shù)據(jù)信息厕隧,只有事務(wù)執(zhí)行完成才會(huì)將元數(shù)據(jù)信息刪除。
????前面我們提到俄周,mmShard記錄了事務(wù)的開(kāi)始時(shí)間吁讨,那vttablet又定義了一個(gè)事務(wù)超時(shí)時(shí)間,比如10秒峦朗。對(duì)于超過(guò)10秒未刪除的分布式事務(wù)建丧,vttablet自動(dòng)給vtgate集群(注意這里是集群,可以是任何一個(gè)vtgate)發(fā)送ResolveTransaction
指令,告訴vtgate可能有事務(wù)異常了波势,需要vtgate看看咋回事翎朱,ResolveTransaction傳遞一個(gè)分布式事務(wù)id作參數(shù),比如Shard1:TX1
尺铣。vtgate收到ResolveTransaction指令后拴曲,對(duì)分布式事務(wù)進(jìn)行Resolve,Resolve函數(shù)前面已經(jīng)介紹過(guò)一次凛忿,現(xiàn)在又派上用場(chǎng)了澈灼。
????回到上面的StartCommit異常,可能StartCommit已經(jīng)修改事務(wù)狀態(tài)未成功店溢,事務(wù)狀態(tài)仍然為prepare叁熔。這時(shí)候和處理prepare異常一致,RollbackPrepare逞怨。
????如果修改事務(wù)狀態(tài)成功者疤,事務(wù)狀態(tài)已經(jīng)成為Committed狀態(tài)。從Resolve函數(shù)中(27~30)可以看到叠赦,執(zhí)行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 err
7 }
8 return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)
9 }
????從代碼中可以看出,resumeCommit其實(shí)就是再StartCommit的基礎(chǔ)上繼續(xù)往下執(zhí)行除秀,繼續(xù)對(duì)每個(gè)Shard執(zhí)行CommitPrepared糯累,然后執(zhí)行ConcludeTransaction刪除mmShard上的事務(wù)員數(shù)據(jù)。這時(shí)候某些Shard可能已經(jīng)成功執(zhí)行過(guò)CommitPrepared册踩,這樣可能會(huì)重復(fù)執(zhí)行泳姐。嗯,沒(méi)錯(cuò)暂吉,CommitPrepared函數(shù)是冪等的胖秒,調(diào)用一次和調(diào)用多次效果一致缎患。
4. CommitPrepared異常 (38~40)
????最后一步驟,提交事務(wù)阎肝,Shard1提交TX1
挤渔,Shard2提交TX2
。
????此處CommitPrepared函數(shù)的參數(shù)是分布式事務(wù)id风题,即Shard1:TX1
判导。Shard1和Shard2的vttablet接受到vtgate發(fā)送的rpc請(qǐng)求后根據(jù)參數(shù)分布式事務(wù)id去preparedPool
里拿到TX1和TX2所使用的事務(wù)連接,然后使用該連接沛硅,也就是在TX1和TX2兩個(gè)事務(wù)中眼刃,分別刪除對(duì)于的redo log,然后提交事務(wù),同樣在TX1和TX2中刪除redo log保證了摇肌,刪除redo log和提交事務(wù)是一個(gè)原子操作擂红。
????該流程可能是commit本地事務(wù)異常,也可能是commit都成功返回成功信息時(shí)候網(wǎng)絡(luò)異常等朦蕴。
????無(wú)論何種異常gate收到的是一個(gè)錯(cuò)誤篮条,這時(shí)候,gate只能給客戶端返回一個(gè)錯(cuò)誤碼吩抓。
????對(duì)于CommitPrepared異常處理策略和上面一樣涉茧,vttablet上面的watchDog發(fā)現(xiàn)本地MySQL有記錄分布式事務(wù)超時(shí),根據(jù)發(fā)送分布式事務(wù)id到vtgate疹娶,vtgate對(duì)分布式事務(wù)進(jìn)行Resolve伴栓。
5. ConcludeTransaction異常
????ConcludeTransaction
只是將mmShard的元數(shù)據(jù)刪除,刪除操作可能成功可能失敗雨饺,最終返回給客戶端一個(gè)錯(cuò)誤碼钳垮。
????對(duì)于ConcludeTransaction
和上面3、4異常處理策略一樣额港。都會(huì)執(zhí)行resumeCommit
饺窿。
????resumeCommit
會(huì)調(diào)用冪等的CommitPrepared
,然后調(diào)用ConcludeTransaction
刪除mmShard上的分布式事務(wù)元數(shù)據(jù)信息移斩。
????同樣ConcludeTransaction
操作也是冪等的肚医。
6. 其他異常
事務(wù)長(zhǎng)時(shí)間未完成
????vttablet配置了分布式事務(wù)超時(shí)時(shí)間,mmShard記錄了事務(wù)開(kāi)始時(shí)間向瓷,長(zhǎng)時(shí)間未提交自動(dòng)發(fā)起Resolve肠套,避免長(zhǎng)時(shí)間未提交的事務(wù)。vtgate宕機(jī)
????在commit2PC函數(shù)中猖任,執(zhí)行到任何一個(gè)步驟vtgate都可能宕機(jī)你稚,但是通過(guò)上面的錯(cuò)誤分析得知,對(duì)于任何異常,要么回滾要么補(bǔ)償刁赖,Vitess都可以避免部分提交搁痛。
????此外,vttablet需要知道協(xié)調(diào)者vtgate地址乾闰,對(duì)于長(zhǎng)時(shí)間未提交事務(wù)發(fā)起Resolve落追。vtgate是一個(gè)集群,避免單點(diǎn)故障涯肩。
????Resolve的參數(shù)是分布式事務(wù)id。根據(jù)分布式事務(wù)id巢钓,我們能拿到mmShard病苗。從mmShard上能讀到分布式事務(wù)的元數(shù)據(jù)信息(不管mmShard和vtgate是否發(fā)生過(guò)宕機(jī)、重啟症汹、網(wǎng)絡(luò)異常等)硫朦。有了事務(wù)的元數(shù)據(jù)信息,就可以發(fā)起Resolve操作了背镇。vttablet或者M(jìn)ySQL宕機(jī)
vttablet或者M(jìn)ySQL宕機(jī)咬展、重啟、或者發(fā)生主從切換之后瞒斩,需要從代碼邏輯上保證各個(gè)接口能夠正常服務(wù)破婆,比如下面的問(wèn)題1。
幾個(gè)小問(wèn)題
為什么我們需要一個(gè)
preparedPool
而不是直接復(fù)用之前的activePool
胸囱?
????因?yàn)楫?dāng)vttablet或者M(jìn)ySQL重啟后祷舀,我們需要能夠從之前記錄的redo log中恢復(fù)回來(lái)。preparedPool
是以分布式事務(wù)id作為key的烹笔。當(dāng)vttablet重啟裳扯,初始化過(guò)程就從MySQL中去取redo log(prepare步驟記錄下來(lái)的SQL就是redo log已經(jīng)對(duì)應(yīng)的分布式事務(wù)id),有redo log就說(shuō)明有分布式事務(wù)未完成谤职。然后對(duì)每個(gè)分布式事務(wù)分配一個(gè)連接饰豺,重新執(zhí)行redo log,然后將該連接放到preparedPool
。這樣即便vttablet發(fā)生重啟允蜈,我們可以重新生成一個(gè)preparedPool
,vttablet仍然可以執(zhí)行vtgate在Commit或者Resolve時(shí)候發(fā)送過(guò)來(lái)的CommitPrepared
或者RollbackPrepared
指令冤吨,從而保證分布式事務(wù)的正常執(zhí)行。vttablet發(fā)起Resolve對(duì)接口的調(diào)用和vtgate正常事務(wù)流程對(duì)接口的調(diào)用是否有可能并發(fā)陷寝?
????有可能锅很,vtgate第一步記錄分布式事務(wù)的元數(shù)據(jù)信息,但是事務(wù)超過(guò)10秒未完成凤跑。那么vttablet自動(dòng)發(fā)起Resolve爆安,發(fā)現(xiàn)事務(wù)未提交調(diào)用RollbackPrepared
,同時(shí)有可能vtgate因?yàn)槟承┊惓R舶l(fā)起了RollbackPrepared
仔引。CommitPrepared
扔仓、ConcludeTransaction
也有一樣的問(wèn)題褐奥。所以Resolve過(guò)程中調(diào)用的接口需要考慮到并發(fā)、冪等性等翘簇。應(yīng)用端執(zhí)行commit返回錯(cuò)誤就是事務(wù)提交失敗么撬码?
????不一定,通過(guò)前面的例子我們可以看到版保,可能兩階段提交前面都成功了呜笑,只是最后刪除mmShard上面的元數(shù)據(jù)失敗了,還是會(huì)返回給應(yīng)用端一個(gè)error彻犁。
甚至所有步驟都成功了叫胁,要正常返回給應(yīng)用端OK包了,斷網(wǎng)了汞幢,應(yīng)用端也是會(huì)收到網(wǎng)絡(luò)異常錯(cuò)誤驼鹅,這就需要應(yīng)用端有合適的補(bǔ)償機(jī)制處理這種異常。Vitess中的兩階段提交事務(wù)滿足文章開(kāi)頭提到的事務(wù)四個(gè)特性么森篷?
????不滿足输钩,Vitess的兩階段提交只是保證了分布式事務(wù)的原子性,即便使用兩階段提交仲智,在Vitess中是有可能讀取到部分提交結(jié)果的买乃。 但是兩階段提交是分布式中經(jīng)典問(wèn)題,也是最基礎(chǔ)的算法坎藐,幾乎所有的復(fù)雜的分布式算法都會(huì)使用到兩階段提交为牍。Vitess中只有兩階段提交一種事務(wù)模型么?
????不是岩馍,除了兩階段提交外碉咆,Vitess還支持單節(jié)點(diǎn)事務(wù),單節(jié)點(diǎn)事務(wù)強(qiáng)制要求事務(wù)只能在一個(gè)Shard執(zhí)行蛀恩;還有多節(jié)點(diǎn)事務(wù)疫铜,多節(jié)點(diǎn)事務(wù)對(duì)部分提交問(wèn)題不做處理。Vitess中兩階段提交性能如何?
????Vitess兩階段提交比一般的多節(jié)點(diǎn)事務(wù)會(huì)多四次vtgate到vttablet的交互双谆,我的測(cè)試結(jié)果兩階段提交帶來(lái)的延遲在5毫秒以內(nèi)壳咕。線上的交易訂單系統(tǒng)真是例子中這樣的么?
????復(fù)雜的多顽馋,首先訂單和白條可能都不在同一個(gè)大部門(mén)谓厘,比如訂單業(yè)務(wù)屬于商城交易部門(mén),白條業(yè)務(wù)屬于金融部門(mén)寸谜。各個(gè)服務(wù)間可能通過(guò)WebService或者mq交互竟稳。其次業(yè)務(wù)也要復(fù)雜的多,一個(gè)下單操作可能涉及到幾十個(gè)甚至上百個(gè)接口的調(diào)用,各個(gè)接口能夠穩(wěn)定的提供服務(wù)最主要的原因是依賴于基礎(chǔ)平臺(tái)的各種中間件他爸,好了編不下去了聂宾,其實(shí)我不知道??。不是說(shuō)好的兩階段提交么诊笤,Vitess為啥分了好幾個(gè)階段啊系谐,這還是正經(jīng)兩階段提交么?
????兩階段指的是協(xié)調(diào)者和參與者之間的交互主要分兩個(gè)階段讨跟。
????兩階段提交第一步纪他,在提交之前協(xié)調(diào)者vtgate向所有參與者vttablet發(fā)出是否可提交的詢問(wèn),CreateTransaction
相當(dāng)于先記錄事務(wù)狀態(tài)晾匠,然后向mmShard發(fā)起了是否可提交的詢問(wèn)止喷,Prepare請(qǐng)求相當(dāng)于向除mmShard之外的其他參與者發(fā)起了詢問(wèn)。
????兩階段提交第二步混聊,是如果所有參與都答復(fù)可以正常提交,那么乾巧,協(xié)調(diào)者給所有參與者發(fā)出提交指令句喜。Vitess中詢問(wèn)之后得到了可以提交的答復(fù),首先通過(guò)StartCommit將狀態(tài)記錄下了沟于,這樣可以避免協(xié)調(diào)組vtgate異常導(dǎo)致分布式事務(wù)異常咳胃。記錄可以提交狀態(tài)之后,協(xié)調(diào)者vtgate向所有參與者發(fā)出提交指令CommitPrepared
旷太。Vitess做的一點(diǎn)優(yōu)化是元數(shù)據(jù)記錄在了mmShard,所以在記錄事務(wù)已經(jīng)提交的步驟中展懈,直接把mmShard上的事務(wù)提交了。
????兩階段提交上面已經(jīng)完成了供璧,ConcludeTransaction
是做了一些清理元數(shù)據(jù)的工作存崖。
????翻了一下事務(wù)處理,在講兩階段提交過(guò)程有明確指出[P14]睡毒,
????對(duì)于參與者的答復(fù)来惧,事務(wù)管理器將在日志中記錄下這個(gè)事實(shí)。
????Vitess做法是將這個(gè)狀態(tài)記錄到了mmShard演顾。對(duì)于資源管理器重啟供搀,事務(wù)處理[P14]中也提到:
????作為重啟邏輯的一部分,資源管理器向事務(wù)管理器發(fā)出詢問(wèn)钠至,這時(shí)候事務(wù)管理器告訴他們 發(fā)生故障時(shí)每一個(gè)活動(dòng)的事務(wù)的執(zhí)行結(jié)果葛虐。一些可能提交了,一些可能終止了棉钧,一些可能仍在提交過(guò)程中屿脐。資源管理器可以獨(dú)立的恢復(fù)其已提交的狀態(tài),也可以參與事務(wù)管理器對(duì)日志重做或者撤銷的檢測(cè)。
????Vitess也是資源管理器vttablet向事務(wù)管理器發(fā)出詢問(wèn)摄悯,只不過(guò)事務(wù)管理器維護(hù)的事務(wù)日志信息還是放到了資源管理器赞季,因?yàn)橘Y源管理器有MySQL啊,正好存儲(chǔ)事務(wù)信息不會(huì)丟奢驯。
????所以Vitess的兩階段提交是正經(jīng)的兩階段提交申钩。而沒(méi)有實(shí)現(xiàn)上面兩點(diǎn)的兩階段提交是不能從錯(cuò)誤正常恢復(fù)瘪阁,也就不能保證原子性撒遣。
參考:
????https://github.com/vitessio/vitess/blob/master/doc/TwoPhaseCommitDesign.md