如何實現跨Mysql、Redis和Mongo分散式事務? - dongfu

banq發表於2022-04-29

如何組合多個儲存引擎合併組成分散式事務
Mysql、Redis、Mongo都是非常火爆的儲存,各有各的優勢。在實際應用中,同時使用多個儲存是很常見的,保證跨多個儲存的資料一致性成為一種需求。
本文給出了一個跨多個儲存引擎實現分散式事務的示例:Mysql、Redis 和 Mongo。此示例基於分散式事務框架https://github.com/dtm-labs/dtm,希望能幫助您解決跨微服務的資料一致性問題。
靈活組合多個儲存引擎形成分散式事務的能力是DTM首先提出的,目前還沒有其他分散式事務框架宣告過這樣的能力。

假設使用者現在正在參與促銷活動:他們有餘額,充值話費,促銷活動將贈送商城積分。餘額儲存在Mysql中,賬單儲存在Redis中,商城積分儲存在Mongo中。由於推廣時間有限,存在參與失敗的可能,所以需要回滾支援。
對於上述問題場景,可以使用DTM的Saga事務,下面我們將詳細講解解決方案。

準備資料
第一步是準備資料。為了方便使用者快速上手示例,我們在en.dtm.pub中準備了所有資料,包括 Mysql、Redis 和 Mongo,具體連線使用者名稱和密碼可以在dtm-labs/ dtm 示例

編寫業務程式碼
先從最熟悉的儲存引擎Mysql的業務程式碼說起:
以下程式碼使用 Golang:其他語言,如 C#、PHP、Java 可以在這裡找到:[DTM SDKs]( https://en.dtm.pub/ref/sdk.html )

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 的正向操作,還用於補償操作,其中只需要傳入一個負數進行補償。
對於 Redis 和 Mongo,業務程式碼處理類似,只是增加或減少相應的餘額。

如何確保冪等性?
對於 Saga 事務模式,當我們在子事務服務中出現臨時故障時,將重試失敗的操作。這種失敗可能發生在子事務提交之前或之後,因此子事務操作需要是冪等的。
DTM 提供輔助表和輔助函式,幫助使用者快速實現冪等性。對於Mysql,它會在業務資料庫中建立一個輔助表barrier,當使用者啟動一個事務調整餘額時,它會先插入Gid到barrier表中。如果存在重複行,則插入失敗,然後跳過平衡調整,保證冪等性。使用輔助函式的程式碼如下:

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 指令碼調整平衡。在調整餘額之前,它會Gid在 Redis 中查詢。如果Gid存在則跳過平衡調整;如果沒有,它將記錄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,我們也需要處理補償操作,但補償不是簡單的反向調整,還有很多陷阱需要注意。

  • 一方面,補償需要考慮冪等性,因為補償中也存在上小節描述的失敗和重試。
  • 另一方面,補償也需要考慮“空補償”,因為 Saga 的前向操作可能會返回失敗,這可能發生在資料調整之前或之後。對於已經提交調整的失敗,我們需要執行反向調整,但是對於沒有提交調整的失敗,我們需要跳過反向調整。


在DTM提供的helper函式中:
  • 一方面會根據forward操作插入的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全域性事務,由3個子交易組成。
  1. 從Mysql轉出50個
  2. 轉入30個到Mongo
  3. 轉入20個到Redis

在整個交易過程中,如果所有的子交易都成功完成,那麼全域性交易就會成功;如果其中一個子交易返回業務失敗,那麼全域性交易就會回滾。

執行
如果你想執行一個完整的上述例子,步驟如下。
1.、執行DTM

git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go


2.、執行一個成功的例子

git clone https://github.com/dtm-labs/dtm-examples && cd dtm-examples
go run main.go http_saga_multidb


3、執行一個失敗的例子

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。
歡迎訪問github.com/dtm-labs/dtm:它是一個專門的專案,讓微服務中的分散式事務變得更容易。它支援多種語言,以及多種模式,如2段訊息事務、Saga、Tcc和XA。
 

相關文章