使用etcd杖挣,是如何實現(xiàn)ACID事務的?

提到事務刚陡,你肯定不陌生惩妇。和數(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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市窘茁,隨后出現(xiàn)的幾起案子怀伦,更是在濱河造成了極大的恐慌,老刑警劉巖山林,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件房待,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機桑孩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門拜鹤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人流椒,你說我怎么就攤上這事敏簿。” “怎么了宣虾?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵惯裕,是天一觀的道長。 經(jīng)常有香客問我安岂,道長轻猖,這世上最難降的妖魔是什么帆吻? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任域那,我火速辦了婚禮,結(jié)果婚禮上猜煮,老公的妹妹穿的比我還像新娘次员。我一直安慰自己,他們只是感情好王带,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布淑蔚。 她就那樣靜靜地躺著,像睡著了一般愕撰。 火紅的嫁衣襯著肌膚如雪刹衫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天搞挣,我揣著相機與錄音带迟,去河邊找鬼。 笑死囱桨,一個胖子當著我的面吹牛仓犬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舍肠,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼搀继,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了翠语?” 一聲冷哼從身側(cè)響起叽躯,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肌括,沒想到半個月后点骑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年畔况,在試婚紗的時候發(fā)現(xiàn)自己被綠了鲸鹦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡跷跪,死狀恐怖馋嗜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吵瞻,我是刑警寧澤葛菇,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站橡羞,受9級特大地震影響眯停,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卿泽,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一莺债、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧签夭,春花似錦齐邦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至慎宾,卻和暖如春丐吓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背趟据。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工券犁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人之宿。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓族操,卻偏偏與公主長得像,于是被迫代替她去往敵國和親比被。 傳聞我的和親對象是個殘疾皇子色难,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容