用Go輕松完成一個XA分布式事務,保姆級教程
事務
某些業(yè)務要求,一系列操作必須全部執(zhí)行,而不能僅執(zhí)行一部分。例如,一個轉賬操作:
- -- 從id=1的賬戶給id=2的賬戶轉賬100元
- -- 第一步:將id=1的A賬戶余額減去100
- UPDATE accounts SET balance = balance - 100 WHERE id = 1;
- -- 第二步:將id=2的B賬戶余額加上100
- UPDATE accounts SET balance = balance + 100 WHERE id = 2;
這兩條SQL語句必須全部執(zhí)行,或者,由于某些原因,如果第一條語句成功,第二條語句失敗,就必須全部撤銷。
這種把多條語句作為一個整體進行操作的功能,被稱為數據庫事務。數據庫事務可以確保該事務范圍內的所有操作都可以全部 成功或者全部失敗。如果事務失敗,那么效果就和沒有執(zhí)行這些SQL一樣,不會對數據庫數據有任何改動。
微服務
如果一個事務涉及的所有操作能夠放在一個服務內部,那么使用各門語言里事務相關的庫,可以輕松的實現多個操作作為整體的事務操作。
但是有些服務,例如生成訂單涉及做很多操作,包括庫存、優(yōu)惠券、贈送、賬戶余額等。當系統(tǒng)復雜程度增加時,想要把所有這些操作放到一個服務內實現,會導致耦合度太高,維護成本非常高。
針對復雜的系統(tǒng),當前流行的微服務架構是非常好的解決方案,該架構能夠把復雜系統(tǒng)進行拆分,拆分后形成了大量微服務,獨立開發(fā),獨立維護。
雖然服務拆分了,但是訂單本身的邏輯需要多個操作作為一個整體,要么全部成功,要么全部失敗,這就帶來了新的挑戰(zhàn)。如何把散落在各個微服務中的本地事務,組成一個大的事務,保證他們作為一個整體,這就是分布式事務需要解決的問題。
分布式事務
分布式事務簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分布在不同的服務器上,且屬于不同的應用,分布式事務需要保證這些小操作要么全部成功,要么全部失敗。本質上來說,分布式事務就是為了保證不同數據庫的數據一致性。
分布式事務方案包括:
- xa
- tcc
- saga
- 可靠消息
下面我們看看最簡單的xa
XA
XA是由X/Open組織提出的分布式事務的規(guī)范,XA規(guī)范主要定義了(全局)事務管理器(TM)和(局部)資源管理器(RM)之間的接口。本地的數據庫如mysql在XA中扮演的是RM角色
XA一共分為兩階段:
第一階段(prepare):即所有的參與者RM準備執(zhí)行事務并鎖住需要的資源。參與者ready時,向TM報告已準備就緒。 第二階段 (commit/rollback):當事務管理者(TM)確認所有參與者(RM)都ready后,向所有參與者發(fā)送commit命令。
目前主流的數據庫基本都支持XA事務,包括mysql、oracle、sqlserver、postgre
我們看看本地數據庫是如何支持XA的:
第一階段 準備
- XA start '4fPqCNTYeSG'
- UPDATE `user_account` SET `balance`=balance + 30,`update_time`='2021-06-09 11:50:42.438' WHERE user_id = '1'
- XA end '4fPqCNTYeSG'
- XA prepare '4fPqCNTYeSG'
- -- 當所有的參與者完成了prepare,就進入第二階段 提交
- xa commit '4fPqCNTYeSG'
xa實踐
介紹了這么多,我們來實踐完成一個微服務上的xa事務,加深分布式事務的理解,這里采用dtm作為分布式事務的管理者,來運行其中一個xa的demo
安裝go 安裝mysql
獲取dtm
- git clone https://github.com/yedf/dtm.git
- cd dtm
配置mysql
- cp conf.sample.yml conf.yml
- vi conf.yml
運行示例
go run app/main.go xa
從日志里,能夠找到XA部分的輸出,最后成功提交完成了事務
- # 服務1輸出
- XA start '4fPqCNTYeSG'
- UPDATE `user_account` SET `balance`=balance - 30,`update_time`='2021-06-09 11:50:42.438' WHERE user_id = '1'
- XA end '4fPqCNTYeSG'
- XA prepare '4fPqCNTYeSG'
- # 服務2輸出
- XA start '4fPqCPijxyC'
- UPDATE `user_account` SET `balance`=balance + 30,`update_time`='2021-06-09 11:50:42.493' WHERE user_id = '2'
- XA end '4fPqCPijxyC'
- XA prepare '4fPqCPijxyC'
- # 服務1輸出
- xa commit '4fPqCNTYeSG'
- #服務2輸出
- xa commit '4fPqCPijxyC'
整個交互的時序詳情如下
代碼如下:
- // 微服務的處理函數:
- app.POST(BusiAPI+"/TransInXa", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
- return XaClient.XaLocalTransaction(c.Request.URL.Query(), func(db *sql.DB, xa *dtmcli.Xa) (interface{}, error) {
- _, err := dtmcli.DBExec(db, "update dtm_busi.user_account set balance=balance+? where user_id=?", reqFrom(c).Amount, 2)
- return dtmcli.MapSuccess, err
- })
- }))
- app.POST(BusiAPI+"/TransOutXa", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
- return XaClient.XaLocalTransaction(c.Request.URL.Query(), func(db *sql.DB, xa *dtmcli.Xa) (interface{}, error) {
- _, err := dtmcli.DBExec(db, "update dtm_busi.user_account set balance=balance-? where user_id=?", reqFrom(c).Amount, 1)
- return dtmcli.MapSuccess, err
- })
- }))
- // 開啟XA事務
- err := XaClient.XaGlobalTransaction(gid, func(xa *dtmcli.Xa) (*resty.Response, error) {
- resp, err := xa.CallBranch(&TransReq{Amount: 30}, Busi+"/TransOutXa")
- if err != nil {
- return resp, err
- }
- return xa.CallBranch(&TransReq{Amount: 30}, Busi+"/TransInXa")
- })
總結
至此,一個完整的xa分布式事務介紹完成。
關于分布式事務更多更全面的知識,請參考 分布式事務最經典的七種解決方案
在這篇簡短的文章里,我們大致介紹了 事務,分布式事務,微服務處理XA事務。有興趣的同學可以通過 dtm 繼續(xù)研究分布式事務。