天天看點

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

作者:閃念基因

摘要

seadt 是 Shopee Financial Products 團隊使用 Golang,針對真實的業務場景提供的分布式事務解決方案。

上一篇文章TCC應用與實作介紹了seadt TCC的應用和具體實作,在seadt TCC的基礎上,本章繼續介紹seadt-SAGA的設計與實作。

seadt在TCC設計之初已經考慮好SAGA的擴充,SAGA事務設計在底層架構、狀态機等均與TCC保持一緻,僅在調用流程中有SAGA獨特之處。

1. SAGA介紹

SAGA概念來源于一篇資料庫論文Sagas。SAGA事務是由多個短時事務組成的長時事務。在分布式事務場景下,我們可以把SAGA分布式事務看作是由多個本地事務組成的事務,每個本地事務都有一個與之對應的補償事務。SAGA事務執行流程如下:

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔1 SAGA事務

備注:T 代表正向接口, C 代表對應的補償接口。在SAGA事務的執行過程中,如果某一步執行失敗/出現異常,SAGA事務會被終止;同時會調用對應的補償事務完成相關的恢複操作。這樣保證SAGA事務中的多個事務要麼都是執行成功,要麼通過補償恢複成為事務執行之前的狀态。

1.1 SAGA-應用場景

分布式事務處理有多種模型,大家熟知的有TCC、SAGA、AT等。前兩篇檔案已經介紹了seadt中TCC的設計與實作,那麼TCC與SAGA除了接口不同外,還有下面差異:

TCC SAGA
事務模型 一階段送出&補償模式 二階段送出
接入場景 短事務 長事務
接入成本 高,需要提供Try,Confirm,Cancel三個接口 低,隻需要提供正向和反向兩個接口
資料隔離性 支援,通過當機資源的方式,令中間狀态對使用者無感 不支援,使用者可以查詢到資料的中間狀态

TCC主要适用于短事務,例如支付轉賬業務,支付場景的特點是一緻性要求高(業務上的隔離性)、短流程、并發高。

SAGA則更适用于長事務,例如現金貸業務,現金貸的申貸流程涉及額度扣減,優惠券使用,購買保險,進行放款等多個環節。任意一個環節失敗,都需要對前面執行過的環節進行沖正處理。可以看到現金貸業務申貸場景的特點就是流程多,流程長,還需要調用其他的服務(購買保險,進行放款),且需要保證資料最終一緻。

這個場景下SAGA事務就是一個比較合适的解決方案,可以将申貸流程的多個環節組合成一個SAGA事務,由SAGA處理每個環節的正向和反向流程,通過使用SAGA來保證資料的最終一緻性。業務隻需要提供正向和反向接口,不需要關心何時調用接口,降低業務代碼複雜度。實際上SAGA較低的接入成本,對資料最終一緻性的保證,已經讓SAGA成為目前業界比較認可的長事務解決方案。

1.2 SAGA-實作模式

SAGA事務的實作有兩種模式,編排模式和協同模式。下面用貸款申請場景舉例說明兩個模式的差別:貸款申請流程中,需要進行額度扣減和優惠券使用,并且兩個操作組成一個SAGA事務。

1.2.1 編排模式

編排模式是指,業務将SAGA事務狀态機以DSL的方式送出到TC中進行維護,由TC編排管理整個流程的流轉,由TC進行事務的正向推進,反向復原和異常處理。SAGA編排模式貸款申請流程如下:

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔2 編排模式

例如上述例子中,編排模式處理過程:

  1. 業務啟動SAGA
  2. TM向TC發起請求,啟動SAGA事務
  3. TC根據該事務DSL,調用Quota,執行分支事務A額度扣減
  4. 步驟3失敗,則TC根據事務DSL進行反向補償,調用Quota,執行分支事務A的復原事務,恢複額度,SAGA事務結束
  5. 步驟3成功,則TC根據事務DSL進行正向推進,調用Promotion,執行分支事務B進行使用優惠券
  6. 步驟5失敗,則TC根據事務DSL進行反向補償,調用Promotion,執行分支事務B的復原事務,退回優惠券,然後執行步驟4
  7. 步驟5成功,SAGA事務結束

下面為編排模式使用的僞代碼:

import seadt
func BizFunc() {
    sagaTxId := "BizTrans"
    seadt.StartSAGATx(ctx, seadt_name, req_id)
}           
// DSL僞代碼
{
    "seadt_name":"BizTrans",
    "tx_seq":[
        {
            "sub_tx_id": "UseQuota",
            "commit_method": "/Module_Name/UseQuota",
            "callback_method": "/Module_Name/UnUseQuota",
            "pre_step":"Start",
            "next_step":"UseCoupon"
        },
        {
            "sub_tx_id": "UseCoupon",
            "commit_method": "/Module_Name/UseCoupon",
            "callback_method": "/Module_Name/UnUseCoupon",
            "pre_step":"UseQuota",
            "next_step":"End"
        }
    ]
}           

編排模式的優點:

  • 可視化:可以用可視化工具來定義流程和生成流程的DSL,标準化,可讀性高
  • 中心化管理/統一管理:可以提供管理面闆,對事務進行統一管理,例如批量查詢事務狀态,批量重試事務等功能
  • 流程靈活編排:可以根據業務需求,靈活進行SAGA事務的“向前重試”或“向後補償”
  • 接口原子化:參與者提供原子化接口,實作簡單

編排模式的缺點:

  • 易用性差:業務接入前,需要先了解狀态機原理,了解對應的DSL,學習成本比較高
  • 接入成本高:如果是現有業務要接入,業務需要将原有的業務代碼轉換為DSL,對業務入侵性高
  • 實作難度大:實作狀态機引擎的研發成本比較高
  • 成本高:實作的人力投入大

術語說明: DSL:領域特定語言(domain-specific language),是僅為某個适用的領域而設計的語言,HTML就是一種用于建立網頁的DSL

1.2.2 協同模式

協同模式是指,将SAGA事務編排的功能以本地SDK的方式內建在事務發起者TM中。SAGA各個分支事務的流轉關系在事務發起者TM的業務代碼中進行編排,SAGA事務的正向推進在事務發起者側完成,SAGA事務的反向復原在事務協調者TC側推動完成,業務無需關注事務的反向復原。SAGA協同模式流程如下:

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔3 協同模式

例如上述例子中,協同模式處理過程:

  1. 事務發起者開啟SAGA事務
  2. 事務發起者調用Quota,執行分支事務A額度扣減
  3. 步驟2失敗,抛出異常,TM上報事務結果到TC,TC進行事務反向復原,調用Quota,執行分支事務A的復原事務,恢複額度,SAGA事務結束
  4. 步驟2成功,事務發起者調用Promotion,執行分支事務B使用優惠券
  5. 步驟4失敗,抛出異常,TM上報事務結果到TC,TC進行事務反向復原,調用Quota和Promotion,恢複額度和退回優惠券,SAGA事務結束
  6. 步驟4成功,SAGA事務結束

下面為協同模式使用的僞代碼:

func BizTrans(ctx context.Context, userId, loanId, couponId,Principal string) {
  saga.WithGlobalTransaction(ctx, func(ctx context.Context) {
    
     // 使用額度
     biz.RefAccount().UseQuota(ctx, userId, loanId, Principal)
     // 使用優惠券
     biz.RefPromotion().UseCoupon(ctx, userId, couponId)


     // local db op
     dao.ClFileAuthDAO().Insert(ctx, record)


      }, &seadt_model.Option{
         TimeOutSecond:   10,
         TransactionName: "loan_apply",
  })           

協同模式的優點: 易接入:業務作為事務發起者,在代碼中直接編排事務流程實作難度低:協同模式需要攔截事務參與者的請求,進行分支事務管理。這部分的功能seadt TCC已經實作,可以直接複用研發成本低:在TCC基礎上開發,10人/天以内

協同模式的缺點:

  • 無法統一管理:架構無法提供基于業務狀态的業務流程管理能力,沒法集中檢視事務編排
  • 業務耦合:協同模式需要在業務代碼中直接編排事務流程,如果要修改事務流程,就必須修改業務代碼
  • 不支援向前重試:無法通過事務架構自動完成向前重試,需要業務自行進行向前重試動作
  • 較難實作有序復原:TC無法精确感覺分支事務的順序
SAGA編排模式 SAGA協同模式
可視化 支援 不支援
流程管理 支援 不支援
向前重試 支援 不支援
向後補償 支援 支援
易用性 較低,有一定學習成本
改造成本高
自研成本

實作:全局-TC / SDK-TC

  • SAGA協同模式兩種不同的實作。
  • SDK-TC:TC與TM、RM以SDK的形式提供給業務引用。
  • 全局-TC:TC作為單獨服務部署。

推動事務反向復原的協調者可以是本地SDK,也可以是全局TC。

SDK-TC 全局-TC
事務編排 較難實作,導緻SDK臃腫 易于實作
維護成本 SDK未來版本可能很多,統一維護和更新較難 更新簡單
問題排查 不友善,資訊分散在多個服務中,需要有多個服務的日志和DB權限才能進行全鍊路定位問題 友善,可以集中檢視事務的所有資料和日志
事務資料管理 每個TM單獨啟動一個背景服務,事務資料過于分散,難以管理 擁有所有資料,可以友善的提供統一的控制台

可以看到全局TC模式的優點是比較明顯的,缺點則是使用全局TC會帶來的單點風險,是以使用全局TC模式的前提就是全局TC服務是高性能和高可用的。seadt TC這部分的高可用設計會在後續文章進行詳細介紹。

1.3 選型

在1.2節介紹SAGA的編排模式和協同模式時,我們也對兩種模式做了簡單總結。

那為什麼seadt-SAGA選擇優先支援協同模式?

seadt的初衷之一是以小步快跑的方式為我們的業務團隊提供分布式事務解決方案。基于這個初衷,我們的選型思路主要考慮兩個方面:業務接入成本和研發成本。

考慮業務接入成本是好了解的。那為什麼還要考慮研發成本呢?

因為随着業務發展,目前團隊在一些業務場景已經産生了SAGA事務的需求,我們希望在可控時間内為團隊提供穩定可靠的SAGA事務功能。

編排模式 協同模式
業務接入成本 高,如果是現有業務要接入,業務需要将原有的業務代碼轉換為DSL,對業務入侵性高 較低,業務作為事務發起者,在代碼中直接編排事務流程
研發成本 高,狀态機引擎的研發成本比較高 較低,協同模式需要攔截事務參與者的請求,進行分支事務管理。這部分的功能seadt TCC已經實作,可以直接複用

基于上述對比,seadt最終選擇了先實作協同模式,采用全局TC方式。

2. seadt-SAGA實作

2.1 接口設計

目前seadt-SAGA的接口設計如下

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔4 seadt-SAGA接口設計

2.2 互動流程

我們仍然以上面的業務場景來介紹接入seadt-SAGA後各個系統之間的互動流程

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔5 seadt-SAGA互動流程

  • 發起者 TM 向 TC 注冊全局事務
  • 發起者進行額度扣減和使用優惠券
  • 參與者 RM 注冊分支事務
  • 參與者執行額度扣減或使用優惠券的本地事務
  • 發起者 RM 執行本地業務處理
  • 發起者 TM 送出全局事務
    • 如果全局事務為Confirm,TC 記錄全局事務完成,全局事務結束;
    • 如果全局事務為Cancel,TC執行Cancel,調用參與者 RM 執行Unuse方法,進行額度增加/優惠券退回操作

2.3 業務接入

事務發起者接入,同本文1.2.2接入一緻。接下來是業務參與者的接入:

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔6 參與者接入示例

seadt-SAGA的接口設計和seadt-TCC基本相同,seadt-SDK 為參與者提供一套 SAGA 的二個接口,但是隻替參與者提供反向的Compensate API 接口 pb,并不提供正向的Forward 接口;正向的Forward接口需要業務參與者自行提供 pb 僞代碼如下:

// saga_manager.go
var quotaUseProxySagaRef *saga.SagaResourceServiceProxy


func init() {
  quotaUseProxySagaRef = saga.RegisterSagaResourceService(new(QuotaUseSagaImpl))
}


func GetQuotaUseSagaRef() *saga.SagaResourceServiceProxy {
  return quotaUseProxySagaRef
}           
// saga_resource_impl.go
type QuotaUseSagaImpl struct {
}


func (t *QuotaUseSagaImpl) Forward(ctx context.Context, payload interface{}) (bool, error) {
  biz.UseQuota(ctx, payload) 
  return true,nil
}


func (t *QuotaUseSagaImpl) Compensate(ctx context.Context, payload interface{}) bool {
  biz.UnUseQuota(ctx, payload)
  return true
}           
// service.go
func UseQuota(ctx context.Context, req *api.UseQuotaReq, resp *api.UseQuotaResp) error {
  db.WithTransaction(ctx, func(ctx context.Context) {
     resp, err := seadt.GetQuotaUseSagaRef().Forward(ctx, req)
     if err != nil {
        panic(err)
     }
  })
  return nil
}           

2.4 狀态機

與TCC相同,SAGA分布式事務中也有兩個核心的狀态機,主事務狀态機、分支事務狀态機。

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔7 全局事務狀态機

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔8 分支事務狀态機

SAGA的主事務狀态機和TCC這樣的兩階段送出事務不一樣,SAGA事務的事務發起者一旦送出全局事務狀态為Commit,即表示全局事務結束,全局事務的狀态會直接從Commit流轉為Committed完成。

SAGA的分支事務狀态機和TCC的差別就更大了,SAGA分支事務隻有三種狀态: Prepared,Confirmed,Canceled,并且Confirmed狀态是可以流轉到Canceled狀态。

TM 與 RM 狀态矩陣(行代表主事務,列代表分支事務):

分支\主 Prepared Committing Committed SRollbacking Rollbacked
- Y N N Y N
Prepared N Y N N Y N
Confirmed N N Y Y Y N
Canceled N N N N Y Y

RM 與 RM 狀态矩陣:

分支\主 Prepared Committed Canceled
Y Y Y Y
Prepared Y Y Y Y
Confirmed Y Y Y Y
Canceled Y Y Y Y

從上面兩個狀态矩陣可以得知,SAGA事務确實是隻保證資料最終一緻性,不保證資料的隔離性,即使主事務已經是Rollbacking狀态,分支事務也可以是Confirmed或者Canceled,這些狀态都是使用者可見的,SAGA隻保證這些分支事務最終是Canceled。

  1. 當主事務是Rollbacking時,分支事務可以是任何狀态
  2. 分支事務互相獨立,分支事務之間的狀态也是任意的,沒有限制

SAGA事務作為主流的長事務解決方案,适用的場景非常多,适合大多數業務系統。

我們考慮團隊自身情況,選擇自研SAGA的協同模式-全局TC方式,最終會朝着編排模式發展。

讀者朋友如果有SAGA需求,可以根據本文介紹的SAGA幾種模式,選擇适合自身業務的模式。seadt團隊會繼續分享自研過程中遇到的實際問題與對應解決方案。

附錄:重點剖析-事務復原

SAGA事務要求,當分支事務是有序執行時,分支事務的復原也必須是有序執行的,復原執行的順序和正向操作是相反。對應的,當某些分支事務之間是并行執行的,那麼這些分支事務的復原也可以是并行執行的。

Backward crash recovery for parallel sagas is similar to that for sequential sagas. Within each process of the parallel saga, transactions are compensated for (or undone) in reverse order just as with sequential sagas. In addition all compensations in a child process must occur before any compensations for transactions in the parent that were executed before the child was created (forked). (Note that only transaction execution order within a process and fork and join information constrain the order of compensation. If T1 and T2 have executed in parallel processes and T2 has read data written by T1, compensating for T1 does not force us to compensate for T2 first.) —— 引自: Hector Garcaa-Molrna, Kenneth Salem. “SAGAS ”

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔9 SAGA事務

SAGA事務復原實作的難點在于:

  • 如何判斷事務之間是有序的,還是并行的?
  • 如果是有序的,事務之間的順序是什麼?

為了解決上述的兩個問題,我們需要引入一個概念Happened-before:

In computer science, the happened-before relation (denoted: ->) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow). This involves ordering events based on the potential causal relationship of pairs of events in a concurrent system, especially asynchronous distributed systems. It was formulated by Leslie Lamport.

The happened-before relation is formally defined as the least strict partial order on events such that:

  • If events a and b occur on the same process, a -> b if the occurrence of event a preceded the occurrence of event b.
  • If event a is the sending of a message and event b is the reception of the message sent in event a, a -> b.
  • If two events happen in different isolated processes (that do not exchange messages directly or indirectly via third-party processes), then the two processes are said to be concurrent, that is neither a -> b nor b -> a is true.

—— 引自: “Happened-before wiki ”

從Happened-before關系定義可以知道,我們可以通過建構事務之間的Happened-before關系感覺到事務之間是并行的,還是有序的;若有序,順序是什麼。

同時我們也知道了Happened-before關系是一種嚴格偏序關系,然後嚴格偏序與有向無環圖有直接的對應關系。一個集合上的嚴格偏序的關系圖就是一個有向無環圖。

是以實作SAGA事務復原,就是建構一個滿足Happened-before關系的事務集,或者生成一個事務的有向無環圖,兩個方案本質是一個方案的兩種實作

  1. 編排模式-復原

編排模式下,事務復原是易于實作的。因為TC可以通過解析業務送出的DSL,獲得SAGA事務的狀态機,進而獲得一個滿足Happened-before關系的事務集,基于這個事務集就可以實作事務復原

  1. 協同模式-復原

協同模式下,事務復原的實作會比編排模式困難。因為協同模式下,SAGA事務的正向推進在事務發起者側完成,SAGA事務的反向復原在事務協調者側推動完成。作為反向復原的推動者TC無法直接感覺事務的Happened-before關系,需要通過其他方式感覺事務的Happened-before關系

對此,我們有兩種獲得事務之間Happened-before關系的方案:向量時鐘、事務序列号。

方案一:向量時鐘

标準的向量時鐘算法是可以準确刻畫事件順序的,即可以根據向量時鐘,得到事務的嚴格偏序集,感覺事務之間的Happened-before關系,向量時鐘算法如下:

對每個節點,定義一個向量VC ,向量的長度是 n , n 是節點數目。

  1. 初始化各個節點 P i 的向量, 全部抹零:V C i = [ 0 , … , 0 ]
  2. 節點 P i 每發生一個事件時, 其向量的第 i 個元素自增:V C i [ i ] + = 1
  3. 當節點 P i 發消息給節點 P j 時,需要在消息上附帶自己的向量 V C i
  4. 當節點 P j 接收到消息時,對齊對方的時鐘,并在自己的時鐘上自增:對 [ 0 , n ) 上的任意一個整數 k 執行 V C j [ k ] = M a x ( V C j [ k ] , V C i [ k ] ) , 接着,對應第2點:V C j [ j ] + = 1

下面我們來看看,如何使用向量時鐘算法得到事務的嚴格偏序集:

先假設事務有1個TM(Trans),兩個RM(Quota和Promotion)

  1. 定義Trans為P0,Quota為P1,Promotion為P2
  2. TM和所有RM都初始化一個向量VCi = [0,0,0]
  3. Trans建立事務,視為一個事件A,Trans的向量更新為[1,0,0]
  4. Trans調用Quota,Quota建立分支事務,視為一個事件B,Quota的向量更新為[2,1,0]
  5. Trans成功調用Quota後,接着調用Promotion,Promotion建立分支事務,視為事件C,Promotion的向量更新為[4.2,1]
seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔10 向量時鐘算法示例

通過向量時鐘算法,是可以得到事務的嚴格偏序集,然後通過将關鍵事件發生時的向量時鐘上報TC,TC側就可以通過比較事件A,B,C的向量值得到事件之間的Happened-before關系:事件A[1,0,0] < 事件B[2,1,0] < 事件C[4.2,1] => A -> B -> C,進而實作事務復原。

但标準向量時鐘算法有個缺點,隻考慮了固定數量的節點,沒有考慮節點的動态添加和銷毀,而分支事務是動态建立的,是以要使用向量時鐘算法獲得事務的Happened-before關系,需要對算法進行一些改變。

在初始化各個節點Pi的向量時,為每個節點配置設定一個全局唯一辨別,采用HashMap的方式記錄各個節點的時鐘值,通過比較相同Key的Value大小得到向量之間的大小關系。

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔11 改良版向量時鐘算法示例

這樣就可以通過向量時鐘獲得事務之間的Happened-before關系。

方案二:事務序列号通過配置設定給每個分支事務一個可比較的事務序列号,來判斷分支事務間的順序,以此保證SAGA事務的有序反向復原。在下文我們稱事務序列号的集合為事務序列,一個SAGA事務對應一個事務序列。

seadt:金融級分布式事務解決方案(三)—— SAGA設計與實作

圖檔12 事務序列号方案示例

seadt使用的全局TC模式,可以準确的為全局事務裡每個分支事務配置設定一個序号。序列号是TC在注冊分支事務時,為分支事務配置設定的全局事務内唯一的序列号,事務序列号滿足以下兩個性質:

  1. 序列号嚴格遞增
  2. 若分支事務A的序列号Seq-a < 分支事務B的序列号Seq-b,分支事務A和B不一定有因果關系;若分支事務A和B有因果關系(A先于B發生),則一定有分支事務A的序列号Seq-a < 分支事務B的序列号Seq-b

上述的事務序列号方案,借助中心節點(全局TC),建構了事務的全序集合(事務序列)來描述事務順序,基于建構的事務全序集合可以串行執行事務的反向復原,可以保證後發生的事務一定比先發生的事務先完成復原操作,進而實作SAGA事務的有序復原。

因為事務序列号方案建構的是全序關系(類似于邏輯時鐘算法的效果),而Happened-before是嚴格偏序關系,是以全序關系是無法展現兩個事務間的并行關系,如果兩個事件并不相關,那麼這個全序集合給出的大小關系則沒有意義,無法通過比較事務序列号來決定什麼事務之間是并行的,也就無法并行復原這些分支事務,是以隻能串行復原所有分支事務。

對比兩個方案,向量時鐘方案可以建構出标準的Happened-before關系,可以準确的對事務進行有序復原和并行復原。但缺點是向量時鐘方案需要每個節點維護一個事務時鐘變量,并且在節點互動的過程中不斷傳播更新這個時鐘變量,會帶來一定的存儲成本和傳輸成本。同時向量時鐘方案也需要對seadt現有接口互動邏輯和seadt-sdk的底層實作都進行較大的改動。

事務序列号方案的優點是實作比較簡單,對現有seadt的接口互動邏輯改動較小,seadt-sdk無感覺,隻需要進行seadt-tc的改造。雖然隻能進行有序串行復原,不能實作并行復原,但我們考慮到反向復原流程是業務無感覺的異步流程,我們認為目前沒必要為了提升異步流程的效率,提高代碼的複雜度。事務序列号方案的缺點是可以接受的。

綜上所述,我們最後選擇了事務序列号方案。

參考文章:

  1. saga論文:https://github.com/mltds/sagas-report
  2. 偏序關系:https://zh.wikipedia.org/wiki/%E5%81%8F%E5%BA%8F%E5%85%B3%E7%B3%BB
  3. 全序關系:https://zh.wikipedia.org/wiki/%E5%85%A8%E5%BA%8F%E5%85%B3%E7%B3%BB
  4. Happened-before:https://en.wikipedia.org/wiki/Happened-before
  5. 邏輯時鐘與向量時鐘:https://writings.sh/post/logical-clocks

作者:Yongchang

來源:微信公衆号:Shopee技術團隊

出處:https://mp.weixin.qq.com/s/VGz3YIHPzkniCWNDsc8Xgw