<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:分布式事務(wù)的 6 種解決方案,你能說幾種

          共 7656字,需瀏覽 16分鐘

           ·

          2021-08-19 01:19

          來源:cnblogs.com/mushroom/p/13788039.html

          介紹

          在分布式系統(tǒng)、微服務(wù)架構(gòu)大行其道的今天,服務(wù)間互相調(diào)用出現(xiàn)失敗已經(jīng)成為常態(tài)。如何處理異常,如何保證數(shù)據(jù)一致性,成為微服務(wù)設(shè)計過程中,繞不開的一個難題。在不同的業(yè)務(wù)場景下,解決方案會有所差異,常見的方式有:

          1. 阻塞式重試;
          2. 2PC、3PC 傳統(tǒng)事務(wù);
          3. 使用隊列,后臺異步處理;
          4. TCC 補償事務(wù);
          5. 本地消息表(異步確保);
          6. MQ 事務(wù)。

          本文側(cè)重于其他幾項,關(guān)于 2PC、3PC 傳統(tǒng)事務(wù),網(wǎng)上資料已經(jīng)非常多了,這里不多做重復(fù)。

          阻塞式重試

          在微服務(wù)架構(gòu)中,阻塞式重試是比較常見的一種方式。偽代碼示例:

          m := db.Insert(sql)

          err := request(B-Service,m)

          func request(url string,body interface{}){
            for i:=0; i<3; i ++ {
              result, err = request.POST(url,body)
              if err == nil {
                  break
              }else {
                log.Print()
              }
            }
          }

          如上,當(dāng)請求 B 服務(wù)的 API 失敗后,發(fā)起最多三次重試。如果三次還是失敗,就打印日志,繼續(xù)執(zhí)行下或向上層拋出錯誤。這種方式會帶來以下問題

          1. 調(diào)用 B 服務(wù)成功,但由于網(wǎng)絡(luò)超時原因,當(dāng)前服務(wù)認為其失敗了,繼續(xù)重試,這樣 B 服務(wù)會產(chǎn)生 2 條一樣的數(shù)據(jù)。
          2. 調(diào)用 B 服務(wù)失敗,由于 B 服務(wù)不可用,重試 3 次依然失敗,當(dāng)前服務(wù)在前面代碼中插入到 DB 的一條記錄,就變成了臟數(shù)據(jù)。
          3. 重試會增加上游對本次調(diào)用的延遲,如果下游負載較大,重試會放大下游服務(wù)的壓力。

          第一個問題:通過讓 B 服務(wù)的 API 支持冪等性來解決。

          第二個問題:可以通過后臺定時腳步去修正數(shù)據(jù),但這并不是一個很好的辦法。

          第三個問題:這是通過阻塞式重試提高一致性、可用性,必不可少的犧牲。

          阻塞式重試適用于業(yè)務(wù)對一致性要求不敏感的場景下。如果對數(shù)據(jù)一致性有要求的話,就必須要引入額外的機制來解決。

          異步隊列

          在解決方案演化的過程中,引入隊列是個比較常見也較好的方式。如下示例:

          m := db.Insert(sql)

          err := mq.Publish("B-Service-topic",m)

          在當(dāng)前服務(wù)將數(shù)據(jù)寫入 DB 后,推送一條消息給 MQ,由獨立的服務(wù)去消費 MQ 處理業(yè)務(wù)邏輯。和阻塞式重試相比,雖然 MQ 在穩(wěn)定性上遠高于普通的業(yè)務(wù)服務(wù),但在推送消息到 MQ 中的調(diào)用,還是會有失敗的可能性,比如網(wǎng)絡(luò)問題、當(dāng)前服務(wù)宕機等。這樣還是會遇到阻塞式重試相同的問題,即 DB 寫入成功了,但推送失敗了。

          理論上來講,分布式系統(tǒng)下,涉及多個服務(wù)調(diào)用的代碼都存在這樣的情況,在長期運行中,調(diào)用失敗的情況一定會出現(xiàn)。這也是分布式系統(tǒng)設(shè)計的難點之一。

          TCC 補償事務(wù)

          在對事務(wù)有要求,且不方便解耦的情況下,TCC 補償式事務(wù)是個較好的選擇。

          TCC 把調(diào)用每個服務(wù)都分成 2 個階段、 3 個操作:

          • 階段一、Try 操作:對業(yè)務(wù)資源做檢測、資源預(yù)留,比如對庫存的檢查、預(yù)扣。
          • 階段二、Confirm 操作:提交確認 Try 操作的資源預(yù)留。比如把庫存預(yù)扣更新為扣除。
          • 階段二、Cancel 操作:Try 操作失敗后,釋放其預(yù)扣的資源。比如把庫存預(yù)扣的加回去。

          TCC 要求每個服務(wù)都實現(xiàn)上面 3 個操作的 API,服務(wù)接入 TCC 事務(wù)前一次調(diào)用就完成的操作,現(xiàn)在需要分 2 階段完成、三次操作來完成。

          比如一個商城應(yīng)用需要調(diào)用 A 庫存服務(wù)、B 金額服務(wù)、C 積分服務(wù),如下偽代碼:

              m := db.Insert(sql)
              aResult, aErr := A.Try(m)
           bResult, bErr := B.Try(m)
           cResult, cErr := C.Try(m)
           if cErr != nil {
            A.Cancel()
            B.Cancel()
              C.Cancel()
           } else {
            A.Confirm()
            B.Confirm()
            C.Confirm()
           }

          代碼中分別調(diào)用 A、B、C 服務(wù) API 檢查并保留資源,都返回成功了再提交確認(Confirm)操作;如果 C 服務(wù) Try 操作失敗后,則分別調(diào)用 A、B、C 的 Cancel API 釋放其保留的資源。

          TCC 在業(yè)務(wù)上解決了分布式系統(tǒng)下,跨多個服務(wù)、跨多個數(shù)據(jù)庫的數(shù)據(jù)一致性問題。但 TCC 方式依然存在一些問題,實際使用中需要注意,包括上面章節(jié)提到的調(diào)用失敗的情況。

          空釋放

          上面代碼中如果 C.Try() 是真正調(diào)用失敗,那下面多余的 C.Cancel() 調(diào)用會出現(xiàn)釋放并沒有鎖定資源的行為。這是因為當(dāng)前服務(wù)無法判斷調(diào)用失敗是不是真的鎖定 C 資源了。如果不調(diào)用,實際上成功了,但由于網(wǎng)絡(luò)原因返回失敗了,這會導(dǎo)致 C 的資源被鎖定,一直得不到釋放。

          空釋放在生產(chǎn)環(huán)境經(jīng)常出現(xiàn),服務(wù)在實現(xiàn) TCC 事務(wù) API 時,應(yīng)支持空釋放的執(zhí)行。

          時序

          上面代碼中如果 C.Try() 失敗,接著調(diào)用 C.Cancel() 操作。因為網(wǎng)絡(luò)原因,有可能會出現(xiàn) C.Cancel() 請求會先到 C 服務(wù),C.Try() 請求后到,這會導(dǎo)致空釋放問題,同時引起 C 的資源被鎖定,一直得不到釋放。

          所以 C 服務(wù)應(yīng)拒絕釋放資源之后的 Try() 操作。具體實現(xiàn)上,可以用唯一事務(wù)ID來區(qū)分第一次 Try() 還是釋放后的 Try()。

          調(diào)用失敗

          Cancel 、Confirm 在調(diào)用過程中,還是會存在失敗的情況,比如常見的網(wǎng)絡(luò)原因。

          Cancel() 或 Confirm() 操作失敗都會導(dǎo)致資源被鎖定,一直得不到釋放。這種情況常見解決方案有:

          1. 阻塞式重試。但有同樣的問題,比如宕機、一直失敗的情況。
          2. 寫入日志、隊列,然后有單獨的異步服務(wù)自動或人工介入處理。但一樣會有問題,寫日志或隊列時,會存在失敗的情況。

          理論上來講非原子性、事務(wù)性的二段代碼,都會存在中間態(tài),有中間態(tài)就會有失敗的可能性。

          本地消息表

          本地消息表最初是 ebay 提出的,它讓本地消息表與業(yè)務(wù)數(shù)據(jù)表處于同一個數(shù)據(jù)庫中,這樣就能利用本地事務(wù)來滿足事務(wù)特性。

          具體做法是在本地事務(wù)中插入業(yè)務(wù)數(shù)據(jù)時,也插入一條消息數(shù)據(jù)。然后在做后續(xù)操作,如果其他操作成功,則刪除該消息;如果失敗則不刪除,異步監(jiān)聽這個消息,不斷重試。

          本地消息表是一個很好的思路,可以有多種使用方式:

          配合MQ

          示例偽代碼:

           messageTx := tc.NewTransaction("order")
           messageTxSql := tx.TryPlan("content")

            m,err := db.InsertTx(sql,messageTxSql)
            if err!=nil {
              return err
            }

            aErr := mq.Publish("B-Service-topic",m)
            if aErr!=nil { // 推送到 MQ 失敗
              messageTx.Confirm() // 更新消息的狀態(tài)為 confirm
            }else {
              messageTx.Cancel() // 刪除消息
            }
          // 異步處理 confirm 的消息,繼續(xù)推送
          func OnMessage(task *Task){
             err := mq.Publish("B-Service-topic", task.Value())
             if err==nil {
               messageTx.Cancel()
             }
          }

          上面代碼中其 messageTxSql 是插入本地消息表的一段 SQL :

          insert into `tcc_async_task` (`uid`,`name`,`value`,`status`) values ('?','?','?','?')

          它和業(yè)務(wù) SQL 在同一個事務(wù)中去執(zhí)行,要么成功,要么失敗。

          成功則推送到隊列,推送成功,則調(diào)用 messageTx.Cancel() 刪除本地消息;推送失敗則標(biāo)記消息為 confirm。本地消息表中 status 有 2 種狀態(tài) tryconfirm, 無論哪種狀態(tài)在 OnMessage 都可以監(jiān)聽到,從而發(fā)起重試。

          本地事務(wù)保障消息和業(yè)務(wù)一定會寫入數(shù)據(jù)庫,此后的執(zhí)行無論宕機還是網(wǎng)絡(luò)推送失敗,異步監(jiān)聽都可以進行后續(xù)處理,從而保障了消息一定會推到 MQ。

          而 MQ 則保障一定會到達消費者服務(wù)中,利用 MQ 的 QOS 策略,消費者服務(wù)一定能處理,或繼續(xù)投遞到下一個業(yè)務(wù)隊列中,從而保障了事務(wù)的完整性。

          配合服務(wù)調(diào)用

          示例偽代碼:

           messageTx := tc.NewTransaction("order")
           messageTxSql := tx.TryPlan("content")

            body,err := db.InsertTx(sql,messageTxSql)
            if err!=nil {
              return err
            }

            aErr := request.POST("B-Service",body)
            if aErr!=nil { // 調(diào)用 B-Service 失敗
              messageTx.Confirm() // 更新消息的狀態(tài)為 confirm
            }else {
              messageTx.Cancel() // 刪除消息
            }
          // 異步處理 confirm 或 try 的消息,繼續(xù)調(diào)用 B-Service
          func OnMessage(task *Task){
            // request.POST("B-Service",body)
          }

          這是本地消息表 + 調(diào)用其他服務(wù)的例子,沒有 MQ 的引入。這種使用異步重試,并用本地消息表保障消息的可靠性,解決了阻塞式重試帶來的問題,在日常開發(fā)中比較常見。

          如果本地沒有要寫 DB 的操作,可以只寫入本地消息表,同樣在 OnMessage中處理:

          messageTx := tc.NewTransaction("order")
          messageTx := tx.Try("content")
          aErr := request.POST("B-Service",body)
          // ....

          消息過期

          配置本地消息表的 TryConfirm 消息的處理器:

          TCC.SetTryHandler(OnTryMessage())
          TCC.SetConfirmHandler(OnConfirmMessage())

          在消息處理函數(shù)中要判斷當(dāng)前消息任務(wù)是否存在過久,比如一直重試了一小時,還是失敗,就考慮發(fā)郵件、短信、日志告警等方式,讓人工介入。

          func OnConfirmMessage(task *tcc.Task) {
          if time.Now().Sub(task.CreatedAt) > time.Hour {
              err := task.Cancel()  // 刪除該消息,停止重試。
             // doSomeThing() 告警,人工介入
              return
           }
          }

          Try 處理函數(shù)中,還要單獨判斷當(dāng)前消息任務(wù)是否存在過短,因為 Try狀態(tài)的消息,可能才剛剛創(chuàng)建,還沒被確認提交或刪除。這會和正常業(yè)務(wù)邏輯的執(zhí)行重復(fù),意味著成功的調(diào)用,也會被重試;為盡量避免這種情況,可以檢測消息的創(chuàng)建時間是否很短,短的話可以跳過。

          重試機制必然依賴下游 API 在業(yè)務(wù)邏輯上的冪等性,雖然不處理也可行,但設(shè)計上還是要盡量避免干擾正常的請求。

          獨立消息服務(wù)

          獨立消息服務(wù)是本地消息表的升級版,把本地消息表抽離成一個獨立的服務(wù)。所有操作之前先在消息服務(wù)添加個消息,后續(xù)操作成功則刪除消息,失敗則提交確認消息。

          然后用異步邏輯去監(jiān)聽消息,做對應(yīng)的處理,和本地消息表的處理邏輯基本一致。但由于向消息服務(wù)添加消息,無法和本地操作放到一個事務(wù)里,所以會存在添加消息成功,后續(xù)失敗,則此時的消息就是個無用消息。

          如下示例場景:

            err := request.POST("Message-Service",body)
            if err!=nil {
              return err
            }
            aErr := request.POST("B-Service",body)
            if aErr!=nil {
              return aErr
            }

          這個無用的消息,需要消息服務(wù)去確認這個消息是否執(zhí)行成功,沒有則刪除,有繼續(xù)執(zhí)行后續(xù)邏輯。相比本地事務(wù)表 tryconfirm ,消息服務(wù)在前面多了一種狀態(tài) prepare

          MQ 事務(wù)

          有些 MQ 的實現(xiàn)支持事務(wù),比如 RocketMQ 。MQ 的事務(wù)可以看作獨立消息服務(wù)的一種具體實現(xiàn),邏輯完全一致。

          所有操作之前先在 MQ 投遞個消息,后續(xù)操作成功則 Confirm 確認提交消息,失敗則Cancel刪除消息。MQ 事務(wù)也會存在 prepare狀態(tài),需要 MQ 的消費處理邏輯來確認業(yè)務(wù)是否成功。

          總結(jié)

          從分布式系統(tǒng)實踐中來看,要保障數(shù)據(jù)一致性的場景,必然要引入額外的機制處理。

          TCC 的優(yōu)點是作用于業(yè)務(wù)服務(wù)層,不依賴某個具體數(shù)據(jù)庫、不與具體框架耦合、資源鎖的粒度比較靈活,非常適用于微服務(wù)場景下。缺點是每個服務(wù)都要實現(xiàn) 3 個 API,對于業(yè)務(wù)侵入和改動較大,要處理各種失敗異常。開發(fā)者很難完整處理各種情況,找個成熟的框架可以大大降低成本,比如阿里的 Fescar。

          本地消息表的優(yōu)點是簡單、不依賴其他服務(wù)的改造、可以很好的配合服務(wù)調(diào)用和 MQ 一起使用,在大多業(yè)務(wù)場景下都比較實用。缺點是本地數(shù)據(jù)庫多了消息表,和業(yè)務(wù)表耦合在一起。文中本地消息表方式的示例,來源于作者寫的一個庫,有興趣的同學(xué)可以參考下 https://github.com/mushroomsir/tcc

          MQ 事務(wù)和獨立消息服務(wù)的優(yōu)點是抽離出一個公共的服務(wù)來解決事務(wù)問題,避免每個服務(wù)都有消息表和服務(wù)耦合在一起,增加服務(wù)自身的處理復(fù)雜性。缺點是支持事務(wù)的 MQ 很少;且每次操作前都先調(diào)用 API 添加個消息,會增加整體調(diào)用的延遲,在絕大多數(shù)正常響應(yīng)的業(yè)務(wù)場景下,是一種多余的開銷。

          程序汪資料鏈接

          歡迎添加程序汪個人微信 itwang007  進粉絲群或圍觀朋友圈

          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕在线观看第一页 | 国产在线观看无码免费视频 | 黄色免费在线观看视频 | 人人爱爱人人 | 亚洲中文字幕色 |