如何優雅地實現多數據庫的發件箱模式
發件箱模式簡介
一個微服務可能需要執行 “存數據庫” 和“發送事件”兩個步驟。例如發佈一篇文章後,需要更新作者的發文統計信息。業務上要求兩個操作同時失敗,或者同時成功,而不能出現一個成功一個失敗。假如最終文章發佈了,更新發文統計失敗了,就會導致數據不一致。
發件箱模式是解決這個問題的最常用模式,其原理爲:
-
本地業務作爲一個事務運行,在提交事務之前,將事件寫入到消息表;提交事務時,會同時提交業務,以及事件
-
通過輪詢消息表或者監聽 binlog 方式,將事件發給消息隊列
-
輪詢方式:每隔 1s 或者 0.2s 取出消息表中事件,發給消息隊列,然後刪除事件
-
監聽 binlog 方式:通過 Debezium 等數據庫工具,監聽數據庫的 binlog,獲取事件,發送給消息隊列
- 編寫消費者,處理事件
由於 1 中,業務和事件的提交是在同一個事務,保證了兩者會同時提交。
在步驟 2,3 中,都是不會失敗的操作,如果中間發生宕機事件等,都會重試,並最終成功。
對於前述的發文後提交統計信息場景,上述方案保證了統計信息被最終更新,數據會達到最終一致
多數據庫的問題
在當今流行的微服務架構下,通常一個微服務會採用一個單獨的數據庫。當多個服務需要使用發件箱模式時,那麼傳統的發件箱架構就比較難以維護。
-
採用輪詢方式獲取事件:需要在輪詢任務中,編寫多個數據庫的輪詢任務
-
採用監聽 binlog 獲取事件:需要監聽多個數據庫的 binlog
上述兩種獲取事件的方式,在面對數量較多的數據庫,可維護性差。而且該架構的彈性並不好,假如數據庫多,而時間產生的事件少,也會導致該架構的負載高,浪費資源。最理想的架構負載是,只跟發送的事件數量相關,跟其他因素無關。
解決方案
開源分佈式事務框架 https://github.com/dtm-labs/dtm 裏面的二階段消息,可以很好的處理這個問題。下面是一個跨行轉賬業務的使用示例:
msg := dtmcli.NewMsg(DtmServer, gid).
Add(busi.Busi+"/TransIn", &TransReq{Amount: 30})
err := msg.DoAndSubmitDB(busi.Busi+"/QueryPreparedB", db, func(tx *sql.Tx) error { return busi.SagaAdjustBalance(tx, busi.TransOutUID, -req.Amount, "SUCCESS")
})
這部分代碼中
-
首先生成一個 DTM 的 msg 全局事務,傳遞 dtm 的服務器地址和全局事務 id
-
給 msg 添加一個分支業務邏輯,這裏的業務邏輯爲餘額轉入操作 TransIn,然後帶上這個服務需要傳遞的數據,金額 30 元
-
然後調用 msg 的 DoAndSubmitDB,這個函數保證業務成功執行和 msg 全局事務提交,要麼同時成功,要麼同時失敗
-
第一個參數爲回查 URL,詳細含義稍後說
-
第二個參數爲 sql.DB,是業務訪問的數據庫對象
-
第三個參數是業務函數,我們這個例子中的業務是給 A 扣減 30 元餘額
成功流程
DoAndSubmitDB 是如何保證業務成功執行與 msg 提交的原子性的呢?請看如下的時序圖:
一般情況下,時序圖中的 5 個步驟會正常完成,整個業務按照預期進行,全局事務完成。這裏面有個新的內容需要解釋一下,就是 msg 的提交是按照兩個階段發起的,第一階段調用 Prepare,第二階段調用 Commit,DTM 收到 Prepare 調用後,不會調用分支事務,而是等待後續的 Submit。只有收到了 Submit,開始分支調用,最終完成全局事務。
異常情況
在分佈式系統中,各類的宕機和網絡異常都是需要考慮的,下面我們來看看可能發生的問題:
首先我們要達到的最重要目標是業務成功執行和 msg 事務是原子操作,那麼假如前面時序圖中,當Prepare
消息發送成功之後,Submit
消息發送成功之前,出現異常宕機會如何?這個時候 dtm 會檢測到該事務超時,會進行回查。對於開發人員來說,該回查很簡單,只需要粘貼如下代碼即可:
app.GET(BusiAPI+"/QueryPreparedB", dtmutil.WrapHandler2(func(c *gin.Context) interface{} { return MustBarrierFromGin(c).QueryPrepared(dbGet())
}))
如果您使用的不是 go 框架 gin,那麼您需要根據您的框架做一些小修改,但是該代碼是通用的,適合您的每個業務。
回查的主要原理主要是通過消息表,但是 dtm 的回查經過仔細的論證,能夠處理以下情況:
-
回查時,本地事務未開始
-
回查時,本地事務還在進行中
-
回查時,本地事務已回滾
-
回查時,本地事務已提交
詳細的回查原理有些複雜,已申請了專利,這裏不做詳細介紹,詳情可以參考 https://dtm.pub/practice/msg.html
多數據庫支持
該方案下,如果您需要處理多數據庫,運維層面,只需要給相應的庫創建好消息表;代碼層面,只需要在回查的地方,傳入不同的數據庫連接即可。
對比於原有的輪詢表,以及監聽 binlog 方案,運維成本大大降低。該架構的負載僅僅與事件數量相關,跟數據庫數量等其他因素無關,具備了更好的彈性。
更多存儲引擎的支持
dtm 的二階段消息,不僅提供了數據庫的支持DoAndSubmitDB
,還提供了 NoSQL 的支持
Mongo 支持
下面這段代碼,可以保證 Mongo 下的業務和消息兩者同時提交
err := msg.DoAndSubmit(busi.Busi+"/RedisQueryPrepared", func(bb *dtmcli.BranchBarrier) error { return bb.MongoCall(MongoGet(), func(sc mongo.SessionContext) error { return SagaMongoAdjustBalance(sc, sc.Client(), TransOutUID, -reqFrom(c).Amount, reqFrom(c).TransOutResult)
})
})
Redis 支持
下面這段代碼,可以保證 Redis 下的業務和消息兩者同時提交
err := msg.DoAndSubmit(busi.Busi+"/RedisQueryPrepared", func(bb *dtmcli.BranchBarrier) error { return bb.RedisCheckAdjustAmount(busi.RedisGet(), busi.GetRedisAccountKey(busi.TransOutUID), -30, 86400)
})
dtm 的回查方案可以很容易的擴展到其他各種各樣的支持事務的存儲引擎
方案特點
二階段消息下具備以下特點:
-
優雅的支持了多數據庫
-
不僅支持 SQL 數據庫,還支持了 Mongo,Redis 等 NoSQL
-
代碼簡短,比通常的發件箱模式代碼量大幅減少
-
整個架構和開發過程不涉及消息隊列,只涉及 api,更容易上手
-
負載僅僅與消息量有關,與涉及的數據庫數量無關
對比 RocketMQ 事務消息
回查的這種形式最早是在 RocketMQ 的事務消息中提出的,但是作者全網查找了回查的例子,以及各種案例,都未找到能夠把各種異常情況都處理好的回查方案。已找到的方案中,都未能夠正確處理” 本地事務還在進行中 “的這種情況,都會存在極端情況導致數據不一致,詳情參考 https://dtm.pub/practice/msg.html。
另外 dtm 的二階段消息,不需要引入隊列,或者也可以結合其他的消息隊列使用,因此使用範圍更廣
小結
本文介紹的 dtm 二階段消息,更好的支持多數據庫的情況。該架構方案,具備諸多優點,可以完美的替代發件箱模式,給開發者帶來更簡單易用的架構。
聯繫我們
歡迎訪問我們的項目,並 star 支持我們:
https://github.com/dtm-labs/dtm
關注【分佈式事務】公衆號,獲得更多分佈式事務相關知識
分佈式事務 介紹分佈式事務相關理論與實踐知識。 開源項目 dtm-labs/dtm 的相關信息發佈。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jxy8X--cfe5xU7CSHJjPAA