跨Mysql、Redis、Mongo的分散式事務

葉東富發表於2022-04-24

Mysql、Redis、Mongo都是非常流行的儲存,並且各自有自己的優勢。在實際的應用中,常常會同時使用多種儲存,也會遇見在多種儲存中保證資料一致性的需求,例如保證資料庫中的庫存和Redis中的庫存一致等。

本文基於分散式事務框架https://github.com/dtm-labs/dtm給出了一個跨Mysql、Redis、Mongo多種儲存引擎的一個可執行的分散式事務例項,希望能夠幫助大家解決這方面的問題。

這種靈活的組合多個儲存引擎形成一個分散式事務的能力,也是dtm首創做到的,目前未看到其他的分散式事務框架有這樣的能力。

問題場景

我們先來看問題場景,假定現在使用者參加一次活動,將自己的餘額,充值話費,同時活動會贈送商城積分。其中餘額儲存在Mysql,話費儲存在Redis,商城積分儲存在Mongo,並且由於活動限時,因此可能出現參加活動失敗的情況,所以需要支援回滾。

對於上述問題場景,可以使用DTM的Saga事務,下面我們就來詳細講解方案。

準備資料

首先是準備資料,為了方便使用者快速上手相關的例子,我們已經把相關的資料準備好了,地址在en.dtm.pub,裡面包括Mysql、Redis、Mongo,具體的連線使用者名稱密碼可以在https://github.com/dtm-labs/d...找到。

如果您想要自己在本地準備相關的資料環境,可以通過 https://github.com/dtm-labs/dtm/blob/main/helper/compose.store.yml 啟動Mysql、Redis、Mongo,然後通過https://github.com/dtm-labs/dtm/tree/main/sqls下面的指令碼準備本例子的資料,其中busi.*為業務資料,barrier.*為DTM使用的輔助表

編寫業務程式碼

我們先看最熟悉的Mysql的業務程式碼

func SagaAdjustBalance(db dtmcli.DB, uid int, amount int) error {
    _, err := dtmimp.DBExec(db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid)
    return err
}

這段程式碼主要是進行資料庫中使用者餘額的調整

對於Saga事務模式來說,當我們回滾時,我們需要反向調整餘額,這部分的處理,我們可以依舊呼叫上述的SagaAdjustBalance,只需要傳入負數的金額即可。

對於Redis和Mongo,業務程式碼的處理也是類似的,只需要對相應的餘額進行增減即可

如何做冪等

對於Saga事務模式來說,當我們的子事務服務出現臨時故障,出現故障就會進行重試,這個故障可能出現在子事務提交前,也可能出現在子事務提交之後,因此子事務服務就需要做到冪等。

DTM 提供了輔助表和輔助的函式,用於幫助使用者快速實現冪等。對於Mysql,他會在業務資料庫中建立輔助表barrier,當使用者開啟事務調整餘額時,會先在barrier表中寫入gid,如果這是一個重複請求,那麼寫入gid時,會發現重複而失敗,此時跳過使用者業務上的餘額調整,保證冪等。輔助函式的使用程式碼如下:

app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
        return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)
    })
}))

Mongo處理冪等的原理與Mysql相近,不再贅述

Redis處理冪等的原理與Mysql不同,主要是因為事務的原理不同。Redis的事務主要是通過lua的原子執行來保證的。DTM的輔助函式會通過lua指令碼來調整餘額,調整餘額前,會在redis中查詢gid,如果存在,則跳過業務上的餘額調整;如果不存在,則執行業務上的餘額調整。輔助函式的使用程式碼如下:

app.POST(BusiAPI+"/SagaRedisTransOut", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), -reqFrom(c).Amount, 7*86400)
}))

如何做補償

對於Saga來說,我們還需要處理補償操作,但補償操作並不是簡單的反向調整,也有很多坑需要注意,否則很容易補償出錯。
一方面,補償需要考慮冪等,因為在補償過程中,也同樣需要考慮故障重試的情況,與前一小節中的冪等處理一樣。另一方面,補償還需要考慮空補償,因為正向分支返回失敗,這個失敗可能是在正向的資料已經調整完成提交之後的失敗,也可能是還沒有提交就返回了失敗。對於資料已提交的失敗,我們需要執行反向操作,對於資料未提交的失敗,我們需要跳過反向操作,即處理空補償。

DTM 提供的輔助表與輔助函式中,一方面會根據正向操作插入的gid判斷是否為空補償,另一方面還會再插入gid+'compensate',判斷補償是否為重複操作。如果是正常補償操作,那麼會執行業務上的補償,如果是空補償或者重複補償,則會跳過補償業務上的補償。

Mysql的程式碼如下:

app.POST(BusiAPI+"/SagaBTransInCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).Call(txGet(), func(tx *sql.Tx) error {
        return SagaAdjustBalance(tx, TransInUID, -reqFrom(c).Amount, "")
    })
}))

Redis的程式碼如下:

app.POST(BusiAPI+"/SagaRedisTransOutCom", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {
    return MustBarrierFromGin(c).RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), reqFrom(c).Amount, 7*86400)
}))

補償程式碼與前面的正向操作程式碼幾乎一樣,僅僅是把金額乘以-1。DTM 的輔助函式會在一個函式內部同時包含了冪等與補償的相關邏輯

其他異常

編寫子事務以及子事務的補償時,其實還有一種異常情況是懸掛,可能出現在全域性事務超時回滾,或者重試到達上線後回滾,正常情況是先正向操作再補償,但是極端情況可能出現先補償再正向操作,因此正向操作還需要判斷補償是否已執行,已執行的情況下,也需要跳過業務操作。

對於DTM的使用者而言,這些異常都已經被優雅的妥善處理,您作為使用者,只需要按照上述的MustBarrierFromGin(c).Call進行呼叫即可,完全不再需要關心這些異常。DTM 處理這些異常的原理在這裡進行了詳細的講述:異常與子事務屏障

發起分散式事務

前面編寫完了各個子事務服務,下面這部分程式碼發起一個Saga全域性事務:

saga := dtmcli.NewSaga(dtmutil.DefaultHTTPServer, dtmcli.MustGenGid(dtmutil.DefaultHTTPServer)).
  Add(busi.Busi+"/SagaBTransOut", busi.Busi+"/SagaBTransOutCom", &busi.TransReq{Amount: 50}).
  Add(busi.Busi+"/SagaMongoTransIn", busi.Busi+"/SagaMongoTransInCom", &busi.TransReq{Amount: 30}).
  Add(busi.Busi+"/SagaRedisTransIn", busi.Busi+"/SagaRedisTransOutIn", &busi.TransReq{Amount: 20})
err := saga.Submit()

在這部分程式碼中,建立了一個Saga全域性事務,該Saga事務包括3個子事務:

  • 從Mysql中轉出50
  • 向Mongo中轉入30
  • 向Redis中轉入20

在整個事務過程中,如果所有的子事務都順利完成,那麼全域性事務成功;如果有一個子事務返回了業務上的失敗,那麼全域性事務回滾。

執行

如果您想要完整執行一個上面的示例,步驟如下:

  1. 執行dtm
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
  1. 執行成功的例子
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb
  1. 執行失敗的例子
git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb_rollback

您可以對例子進行修改,模擬各種臨時的故障,空補償的情況,以及其他各種異常,當整個全域性事務最終完成時,資料是一致的。

小結

本文給出了一個跨Mysql、Redis、Mongo的分散式事務例子,詳細講解了其中需要處理的問題,以及解決方案。

本文的原理適合於所有支援ACID事務的儲存引擎,您可以將它快速擴充套件,用於其他引擎,例如TiKV等。

歡迎訪問https://github.com/dtm-labs/dtm 並star支援我們

相關文章