提到事務刚陡,你肯定不陌生惩妇。和數(shù)據(jù)庫打交道的時候株汉,我們總是會用到事務。簡單來說歌殃,事務就是要保證一組數(shù)據(jù)庫操作乔妈,要么全部成功,要么全部失敗氓皱。有了事務褒翰,就大大簡化了業(yè)務開發(fā)的難度,使我們更容易開發(fā)出邏輯正確且高效的代碼匀泊。但是优训,在傳統(tǒng)關(guān)系數(shù)據(jù)庫之外,NoSQL存儲系統(tǒng)幾乎都不提供事務支持各聘。
最近揣非,在由于項目需要,對etcd3進行了一些調(diào)研躲因,驚喜得發(fā)現(xiàn)早敬,基于etcd3可以很容易實現(xiàn)ACID事務(設計到實現(xiàn)方式,以MySQL為例)大脉。
etcd旨在提供強一致的kv存儲搞监,作為zookeeper的一個替代品,為分布式應用提供并發(fā)協(xié)調(diào)支持镰矿,下面是etcd用來構(gòu)建ACID事務的幾個關(guān)鍵特征:
- raft
etcd使用raft協(xié)議來進行Leader選舉和操作同步琐驴,意味著無論你訪問任意節(jié)點,都將獲得最終一致的數(shù)據(jù)視圖秤标,高度可靠绝淡。
raft使用quorum機制(多數(shù)同意原則),一個提議只要被多數(shù)節(jié)點批準苍姜,就寫入raft日志文件持久化牢酵,不可撤銷。
- mvvc
etcd采用的是 "btree + bbolt"(類似leveldb的kv存儲引擎)兩級存儲結(jié)構(gòu)衙猪。在內(nèi)存中使用btree維護key/value索引馍乙,節(jié)點存的是bbolt中的鍵值k,通過這個k在bbolt查找垫释,得到的才是用戶傳進去的value值丝格。
etcd中有一個revision(修訂)的概念,類似mysql的事務id饶号,每次更新kv铁追,revision就遞增1。舉個例子茫船,假如當前etcd的revision=3琅束,執(zhí)行put("demo","abc"),revision就會變?yōu)?算谈,更新btree["demo"] = "demo:4"涩禀,更新bbolt["demo:4"] = "abc"。這樣就更新了demo然眼,也保留了demo的歷史版本艾船,比如,通過 bbolt["demo:2"] 可以訪問到demo的歷史版本高每。
- txn
etcd提供了“事務”屿岂,可以處理多個key的原子更新,這一點是非常難得的鲸匿。
etcd事務的語法是"If-Then-Else"爷怀,代替了常見的CAS操作,可以在一個事務中带欢,原子地執(zhí)行沖突檢查运授,更新多個keys的值。
了解mysql的innoDB事務的同學都清楚乔煞,mysql是依賴鎖實現(xiàn)事務的吁朦。一個事務首先要拿到它操作的數(shù)據(jù)庫記錄的鎖,才能進行后續(xù)的操作渡贾,發(fā)生沖突時逗宜,事務會阻塞,嚴重時會發(fā)生死鎖空骚。在整個事務過程中锦溪,client和mysql要進行多次交互,mysql要為client維持事務資源府怯,直到事務提交刻诊。
而etcd的實現(xiàn)方式有些不同,它的事務是基于cas方式實現(xiàn)的牺丙。在事務執(zhí)行過程中则涯,client和etcd之間沒有維護事務會話,在commit事務時冲簿,它的“沖突判斷(If)和執(zhí)行過程Then/Else”一次性提交給etcd粟判,etcd來作為一個原子過程來執(zhí)行“If-Then-Else”。所以峦剔,etcd事務不會發(fā)生阻塞档礁,無論成功,還是失敗吝沫,都會立即返回呻澜,需要應用進行失敗(發(fā)生沖突)重試递礼。因此,這也就要求業(yè)務代碼是可重試的羹幸。
etcd的事務可以看做是一種“微事務”脊髓,在它之上,可以構(gòu)建出各種有意思的應用栅受,例如将硝,我們下面談到的ACID事務。
下面是一個常用來解釋事務的轉(zhuǎn)賬業(yè)務的例子屏镊,假如要從 from 向 to 轉(zhuǎn)賬 amount依疼,業(yè)務代碼是這樣的:
func txnXfer(etcd *v3.Client, from, to string, amount uint) (error) {
// 失敗重試
for {
if ok, err := doTxnXfer(etcd, from, to amount); err != nil {
return err
} else if ok {
return nil
}
}
}
func doTxnXfer(etcd *v3.Client, from, to string, amount uint) (bool, error) {
// 獲取from,to賬戶金額
getresp, err := etcd.Txn(ctx.TODO()).Then(OpGet(from), OpGet(to)).Commit()
if err != nil {
return false, err
}
fromKV := getresp.Responses[0].GetRangeResponse().Kvs[0]
toKV := getresp.Responses[1].GetRangeResponse().Kvs[1]
fromV, toV := toUInt64(fromKV.Value), toUint64(toKV.Value)
// 驗證賬戶余額是否充足
if fromV < amount {
return false, fmt.Errorf(“insufficient value”)
}
// 發(fā)起轉(zhuǎn)賬視圖
txn := etcd.Txn(ctx.TODO()).If(
v3.Compare(v3.ModRevision(from), “=”, fromKV.ModRevision), // 事務提交時而芥,from賬戶余額沒有沒有變動
v3.Compare(v3.ModRevision(to), “=”, toKV.ModRevision)) // 事務提交時律罢,to賬戶余額沒有變動
txn = txn.Then(
OpPut(from, fromUint64(fromV - amount)), // 更新from賬戶余額
OpPut(to, fromUint64(toV - amount)) // 更新to賬戶余額
putresp, err := txn.Commit() // 提交事務
if err != nil {
return false, err
}
return putresp.Succeeded, nil
}
可以看到,使用etcd事務蔚出,我們不僅要寫轉(zhuǎn)賬業(yè)務代碼弟翘,還要構(gòu)造If條件(沖突判斷條件),處理重試骄酗。有沒有辦法能夠簡化這個過程呢稀余?答案是有。etcd的官方clientv3 sdk(go語言)提供了STM(軟件事務內(nèi)存)趋翻,幫我們自動處理了這些繁瑣的過程睛琳。
使用STM的轉(zhuǎn)賬業(yè)務代碼如下,
func stmXfer(e *v3.Client, from, to string, amount uint) error {
return <-conc.NewSTMRepeatable(context.TODO(), e, func(s *conc.STM) error {
// 取賬戶余額
fromV := toUInt64(s.Get(from))
toV := toUInt64(s.Get(to))
// 驗證賬戶余額是否充足
if fromV < amount {
return fmt.Errorf(“insufficient value”)
}
// 更新賬戶余額
s.Put(to, fromUInt64(toV + amount))
s.Put(from, fromUInt64(fromV - amount))
return nil
})
}
這段代碼是不是很清爽踏烙?我們只要編寫轉(zhuǎn)賬邏輯师骗,其他事情STM都幫我們做了。你可能很好奇讨惩,這個函數(shù)是如何被執(zhí)行的辟癌,其實,就是New(NewSTMRepeatable)出來的STM對象在內(nèi)部構(gòu)造txn事務荐捻,把我們編寫的業(yè)務函數(shù)翻譯成If-Then黍少,自動提交事務,處理失敗重試等工作处面,直到事務執(zhí)行成功厂置,或者明確的執(zhí)行失敗(出現(xiàn)異常,不能靠重試成功)魂角。
etcd官方client實現(xiàn)了四種事務模型昵济,通過分析源代碼 clientv3/concurrency/stm.go,可以了解STM各種事務的語義。
- ReadCommitted
讀提交是指访忿,一個事務提交之后瞧栗,它做的變更才會被其他事務看到。
由于etcd的kv操作(包括txn事務內(nèi)的多個keys操作)都是原子操作醉顽,所以你不可能讀到未提交的修改沼溜,ReadCommitted是etcd中的最低事務級別平挑。
Get操作:從etcd讀取keys游添,就像普通的kv操作一樣。第一次Get后通熄,在事務中緩存唆涝,后續(xù)不再從etcd讀取。
If條件:None唇辨,沒有任何沖突檢測廊酣。
- RepeatableReads
可重復讀是指,一個事務執(zhí)行過程中看到的數(shù)據(jù)赏枚,總是跟這個事務在啟動時看到的數(shù)據(jù)是一致的亡驰。
Get操作:從etcd讀取keys,就像普通的kv操作一樣饿幅。第一次Get后凡辱,在事務中緩存,后續(xù)不再從etcd讀取栗恩。
If條件:在事務提交時透乾,事務中Get的keys沒有被改動過。
MySQL事務“可重復讀”是通過在事務第一次select時建立readview磕秤,來確保事務中讀到的是到這一刻為止的最新數(shù)據(jù)乳乌,忽略后面發(fā)生的更新。而這里每個key的Get是獨立的(也可以說市咆,每個key都是獲取的當前值汉操,沒有readview的概念),在事務提交時蒙兰,如果這些keys沒有變動過磷瘤,那么事務就可以提交。
- Serializable
串行化癞己,顧名思義是對同一行記錄膀斋,“寫”會加“寫鎖”,“讀”會加“讀鎖”痹雅。當出現(xiàn)讀寫鎖沖突的時候仰担,后訪問的事務必須等前一個事務執(zhí)行完成,才能繼續(xù)執(zhí)行。
Get操作:事務中的第一個Get操作發(fā)生時摔蓝,保存服務器返回的當前revision赂苗;后續(xù)對其他keys的Get操作,指定獲取revision版本的value贮尉。
If條件:在事務提交時拌滋,事務中Get的keys沒有被改動過。
可見猜谚,這個約束比數(shù)據(jù)庫串行化的約束要低败砂,它沒有驗證事務要修改的keys是否被改動過,下面的SerializableSnapshot事務增加了這個約束魏铅。
- SerializableSnapshot
Get操作:事務中的第一個Get操作發(fā)生時昌犹,保存服務器返回的當前revision;后續(xù)對其他keys的Get操作览芳,指定獲取revision版本的value斜姥。
If條件:在事務提交時,事務中Get的keys沒有被改動過沧竟,事務中要修改的keys也沒有被改動過铸敏。
通過上面的分析,我們清楚了如何使用etcd的txn事務悟泵,構(gòu)建符合ACID語義的事務框架杈笔。如果這些語義不能滿足你的業(yè)務需求,通過擴展etcd的官方client sdk魁袜,寫一個新STM事務類型即可桩撮。
有一點要強調(diào)的是,數(shù)據(jù)庫事務是“鎖/阻塞”模式峰弹,而etcd的STM事務是“cas/重試”模式店量,這是有差別的。簡單的說鞠呈,數(shù)據(jù)庫事務不會自己重試融师,而STM事務在發(fā)生沖突是會多次重試,必須要保證業(yè)務代碼是可重試的蚁吝,且必須有明確的失敗條件(例如判斷賬戶余額是否夠轉(zhuǎn)賬)旱爆。
參考文章:
https://github.com/etcd-io/etcd/blob/master/Documentation/learning/api.md
https://coreos.com/blog/transactional-memory-with-etcd3.html
https://yuerblog.cc/2017/12/10/principle-about-etcd-v3/