深度剖析Saga分散式事務

葉東富發表於2021-11-23

saga是分散式事務領域裡一個非常重要的事務模式,特別適合解決出行訂票這類的長事務,本文將深度剖析saga事務的設計原理,以及在解決訂票問題上的最佳實踐

saga的理論來源

saga這種事務模式最早來自這篇論文:sagas

在這篇論文裡,作者提出了將一個長事務,分拆成多個子事務,每個子事務有正向操作Ti,反向補償操作Ci。

假如所有的子事務Ti依次成功完成,全域性事務完成

假如子事務Ti失敗,那麼會呼叫Ci, Ci-1, Ci-2 ....進行補償

論文闡述了上述這部分基本的saga邏輯之後,提出了下面幾種場景的技術處理

回滾與重試

對於一個SAGA事務,如果執行過程中遭遇失敗,那麼接下來有兩種選擇,一種是進行回滾,另一種是重試繼續。

回滾的機制相對簡單一些,只需要在進行下一步之前,把下一步的操作記錄到儲存點就可以了。一旦出現問題,那麼從儲存點處開始回滾,反向執行所有的補償操作即可。

假如有一個持續了一天的長事務,被伺服器重啟這類臨時失敗中斷後,此時如果只能進行回滾,那麼業務是難以接受的。 此時最好的策略是在儲存點處重試並讓事務繼續,直到事務完成。

往前重試的支援,需要把全域性事務的所有子事務事先編排好並儲存,然後在失敗時,重新讀取未完成的進度,並重試繼續執行。

併發執行

對於長事務而言,併發執行的特性也是至關重要的,一個序列耗時一天的長事務,在並行的支援下,可能半天就完成了,這對業務的幫助很大。

某些場景下併發執行子事務,是業務必須的要求,例如訂多張及票,而機票確認時間較長時,不應當等前一個票已經確認之後,再去定下一張票,這樣會導致訂票成功率大幅下降。

在子事務併發執行的場景下,支援回滾與重試,挑戰會更大,涉及了較複雜的儲存點。

saga的實現分類

目前看到市面上已經有很多的saga實現,他們都具備saga的基本功能。

這些實現,可以大致可以分為兩類

狀態機實現

這一類的典型實現有seata的saga,他引入了一個DSL語言定義的狀態機,允許使用者做以下操作:

  • 在某一個子事務結束後,根據這個子事務的結果,決定下一步做什麼
  • 能夠把子事務執行的結果儲存到狀態機,並在後續的子事務中作為輸入
  • 允許沒有依賴的子事務之間併發執行

這種方式的優點是:

  • 功能強大,事務可以靈活自定義

缺點是:

  • 狀態機的使用門檻非常高,需要了解相關DSL,可讀性差,出問題難除錯。官方例子是一個包含兩個子事務的全域性事務,Json格式的狀態機定義大約有95行,較難入門。
  • 介面入侵強,只能使用特定的輸入輸出介面引數型別,在雲原生時代,對強型別的gRPC不友好

非狀態機實現

這一類的實現有eventuate的saga,dtm的saga。

在這一類的實現中,沒有引入新的DSL來實現狀態機,而是採用函式介面的方式,定義全域性事務下的各個分支事務:

優點:

  • 簡單易上手,易維護

缺點:

  • 難以做到狀態機的事務靈活自定義

PS:eventuate的作者將基於事件訂閱協作的模式,也稱為saga,因為他的影響力大,因此許多文章在介紹saga模式的時候都會提這個。但事實上這個模式與原先的saga論文相關不大,也與各家實現的saga模式相關不大,所以這裡沒有專門去論述這種模式

還有許多其他的saga實現,例如servicecomb-pack,Camel,hmily.由於精力有限,沒有一一研究。後續做了更多研究後,會繼續更新文章

dtm的saga設計

dtm支援TCC和saga模式,這兩個模式有不同的特點,各自適應不同的業務場景,相互補充。

image.png

上述這張表,很好的比較了TCC和SAGA這兩種事務模式。

TCC的定位是一致性要求較高的短事務。一致性要求較高的事務一般都是短事務(一個事務長時間未完成,在使用者看來一致性是比較差的,一般沒有必要採用TCC這種高一致性的設計),因此TCC的事務分支編排放在了AP端(即程式程式碼裡),由使用者靈活呼叫。這樣使用者可以根據每個分支的結果,做靈活的判斷與執行。

SAGA的定位是一致性要求較低的長事務/短事務。對於類似訂機票這種這樣的場景,持續時間長,可能持續幾分鐘到一兩天,就需要把整個事務的編排儲存到伺服器,避免發起全域性事務的APP因為升級、故障等原因,導致事務編排資訊丟失。

狀態機提供的靈活性對於在客戶端編排的TCC是沒必要的,但是對於儲存在伺服器端的saga是有意義的。我在最初設計saga的時候,進行了較詳細的權衡取捨。狀態機的這種方式,上手難度非常高,使用者容易望而卻步。我找了一些使用者做需求調研,總結出來的核心需求有:

  • 子事務併發執行,降低延時。例如旅遊訂票業務的預定往返機票,因為訂票可能需要較長時間才能夠確認,等去的機票定好之後再訂返程票,容易導致訂不上。
  • 有些操作無法回滾,需要放在可回滾的子事務之後,保證一旦執行,就能夠最終成功。

在這兩項核心需求下,dtm的saga最終沒有采用狀態機,但是支援了子事務的併發執行以及指定子事務之間的順序關係。

下面我們以一個實際問題作為例子,講解dtm中saga的用法

對於訂票類業務,子事務的執行結果不是立即返回的,通常是預定機票後,過一段時間第三方才通知結果。對於這種情況dtm的saga提供了良好的支援,它支援子事務返回進行中的結果,並支援指定重試時間間隔。訂票的子事務可以在自己的邏輯中,如果未下訂單,則下訂單;如果已下訂單,那麼此時就是重試的請求,可以去第三方查詢結果,最後返回成功/失敗/進行中。

解決問題例項

我們以一個真實使用者案例,來講解dtm的saga最佳實踐。

問題場景:一個使用者出行旅遊的應用,收到一個使用者出行計劃,需要預定去三亞的機票,三亞的酒店,返程的機票。

要求:

  1. 兩張機票和酒店要麼都預定成功,要麼都回滾(酒店和航空公司提供了相關的回滾介面)
  2. 預訂機票和酒店是併發的,避免序列的情況下,因為某一個預定最後確認時間晚,導致其他的預定錯過時間
  3. 預定結果的確認時間可能從1分鐘到1天不等

上述這些要求,正是saga事務模式要解決的問題,我們來看看dtm怎麼解決(以Go語言為例)。

首先我們根據要求1,建立一個saga事務,這個saga包含三個分支,分別是,預定去三亞機票,預定酒店,預定返程機票

        saga := dtmcli.NewSaga(DtmServer, gid).
            Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketInfo1).
            Add(Busi+"/BookHotel", Busi+"/BookHotelRevert", bookHotelInfo2).
            Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketBackInfo3)

然後我們根據要求2,讓saga併發執行(預設是順序執行)

  saga.EnableConcurrent()

最後我們處理3裡面的“預定結果的確認時間”不是即時響應的問題。由於不是即時響應,所以我們不能夠讓預定操作等待第三方的結果,而是提交預定請求後,就立即返回狀態-進行中。我們的分支事務未完成,dtm會重試我們的事務分支,我們把重試間隔指定為1分鐘。

  saga.SetOptions(&dtmcli.TransOptions{RetryInterval: 60})
  saga.Submit()
// ........
func bookTicket() string {
    order := loadOrder()
    if order == nil { // 尚未下單,進行第三方下單操作
        order = submitTicketOrder()
        order.save()
    }
    order.Query() // 查詢第三方訂單狀態
    return order.Status // 成功-SUCCESS 失敗-FAILURE 進行中-ONGOING
}

高階用法

在實際應用中,還遇見過一些業務場景,需要一些額外的技巧進行處理

支援重試與回滾

dtm要求業務明確返回以下幾個值:

  • SUCCESS表示分支成功,可以進行下一步
  • FAILURE 表示分支失敗,全域性事務失敗,需要回滾
  • ONGOING表示進行中,後續按照正常的間隔進行重試
  • 其他表示系統問題,後續按照指數退避演算法進行重試

部分第三方操作無法回滾

例如一個訂單中的發貨,一旦給出了發貨指令,那麼涉及線下相關操作,那麼很難直接回滾。對於涉及這類情況的saga如何處理呢?

我們把一個事務中的操作分為可回滾的操作,以及不可回滾的操作。那麼把可回滾的操作放到前面,把不可回滾的操作放在後面執行,那麼就可以解決這類問題

        saga := dtmcli.NewSaga(DtmServer, dtmcli.MustGenGid(DtmServer)).
            Add(Busi+"/CanRollback1", Busi+"/CanRollback1Revert", req).
            Add(Busi+"/CanRollback2", Busi+"/CanRollback2Revert", req).
            Add(Busi+"/UnRollback1", Busi+"/UnRollback1NoRevert", req).
            EnableConcurrent().
            AddBranchOrder(2, []int{0, 1}) // 指定step 2,需要在0,1完成後執行

超時回滾

saga屬於長事務,因此持續的時間跨度很大,可能是100ms到1天,因此saga沒有預設的超時時間。

dtm支援saga事務單獨指定超時時間,到了超時時間,全域性事務就會回滾。

    saga.SetOptions(&dtmcli.TransOptions{TimeoutToFail: 1800})

在saga事務中,設定超時時間一定要注意,這類事務裡不能夠包含無法回滾的事務分支,否則超時回滾這類的分支會有問題。

其他分支的結果作為輸入

前面的設計環節講了為什麼dtm沒有支援這樣的需求,那麼如果極少數的實際業務有這樣的需求怎麼處理?例如B分支需要A分支的執行結果

dtm的建議做法是,在ServiceA再提供一個介面,讓B可以獲取到相關的資料。這種方案雖然效率稍低,但是易理解已維護,開發工作量也不會太大。

PS:有個小細節請注意,儘量在你的事務外部進行網路請求,避免事務時間跨度變長,導致併發問題。

小結

本文總結了saga相關的理論知識、設計原則,對比了saga的不同實現及其優缺點。最後以一個現實中的問題案例,詳細講解dtm的saga事務使用

dtm是一個一站式的分散式事務解決方案,支援事務訊息、SAGA、TCC、XA等多種事務模式,支援Go、Java、Python、PHP、C#、Node等語言SDK。

專案文件還詳細講解了分散式事務相關的基礎知識、設計理念和最新理論,是學習分散式事務的絕佳資料。

歡迎大家訪問yedf/dtm,給我們Issue、PR、Star。

相關文章