做者 | 劉曉敏
來源|阿里巴巴雲原生公衆號html
seata-golang 是一個分佈式事務框架,實現了 AT 模式和 TCC 模式,AT 模式相較 TCC 模式對代碼的***性更小、須要開發的接口更少;但 AT 模式對事務操做的數據持有全局鎖,從這點來講,TCC 模式性能更好。java
seata 的 AT 模式將全局鎖放在 transaction coordinator 也就是事務協調器上,依賴於具體鎖接口的存儲實現方式能夠是 file/db/redis 等,而不是數據庫鎖,每一個分支事務提交時當即釋放數據庫鎖,這樣對數據庫的壓力也就減少了,變相得提高了數據庫的性能。seata AT 模式和 TCC 模式的原理見:[Seata 是什麼?]git
下面以 seats-golang samples 爲例,就 AT 模式和 TCC 模式如何接入到業務中作一個說明。github
在 samples/at 目錄下,有三個微服務:product_svc、order_svc、aggregation_svc。golang
product_svc 負責建立訂單時扣減庫存。redis
order_svc 負責建立訂單時寫入訂單主表和訂單明細表。sql
熟悉 seata java 框架的都知道,seata java 框架經過掃描 @GlobalTransactional 註解,動態生成 AOP 切面,代理被 @GlobalTransactional 標記的方法,實現全局事務的開啓、提交或者回滾。數據庫
不一樣於做爲解釋型語言的 Java,Go 是一種編譯型語言,因此 seata-golang 使用了反射技術實現動態代理功能,被代理的對象須要實現 GlobalTransactionProxyService 接口。框架
type GlobalTransactionProxyService interface { GetProxyService() interface{} GetMethodTransactionInfo(methodName string) *TransactionInfo }
aggregation_svc 中的 Svc struct 有一個方法 CreateSo
,該方法經過對 order_svc 和 product_svc 的調用實現了建立訂單和扣減庫存。seata-golang 要代理該 *Svc 對象,須要建立一個代理對象,被代理的方法要在代理對象中做爲一個空方法成員,等待 seata-golang 去動態實現。分佈式
type ProxyService struct { *Svc CreateSo func(ctx context.Context, rollback bool) error }
代理對象 ProxyService 經過組合方式內置被代理對象 Svc,在開發者調用 tm.Implement(svc.ProxySvc)
方法後,seata-golang 會經過 Svc 實現的 GlobalTransactionProxyService 接口獲取動態建立 CreateSo 方法所須要的事務信息,而後根據這些事務信息去動態建立 CreateSo 方法:開啓事務 -> 執行被代理 *Svc 對象的 CreateSo 方法邏輯 -> 根據被代理的 CreateSo 方法的返回錯誤信息決定提交仍是回滾。
能夠經過以下三種方式傳遞全局事務 ID。
在 aggregation_svc 這個服務裏,Seata-golang 經過 request header (req.Header.Set("XID", rootContext.GetXID())
)將 XID (全局事務 ID)傳遞到了 order_svc 和 product_svc,order_svc 和 product_svc 則從 Request Header 取出 XID (c.Request.Header.Get("XID")
)用於分支事務處理。
若是使用 dubbo 協議 rpc 通訊,則須要把 XID 注入到 attachment 中傳遞到下游。
若是使用 dubbo-go 框架,dubbo-go 會從 context 中讀取 attachment 將其序列化傳遞給服務端。能夠採用以下的方式,將 XID 傳遞出去:
context.WithValue(ctx, "attachment", map[string]string{ "XID": rootContext.GetXID(), }
dubbo-go 服務端則從 attachment 中取出 XID,再注入到 context 中,分支事務的業務方法則能夠從 context 中獲取 XID 用於分支事務處理。
// SeataFilter ... type SeataFilter struct { } // Invoke ... func (sf *SeataFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { xid := invocation.AttachmentsByKey("XID", "") if xid != "" { return invoker.Invoke(context.WithValue(ctx, "XID", xid), invocation) } return invoker.Invoke(ctx, invocation) } // OnResponse ... func (sf *SeataFilter) OnResponse(ctx context.Context, result protocol.Result, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result { return result } // GetSeataFilter ... func getSeataFilter() filter.Filter { return &SeataFilter{} }
上面的 filter 經過 extension.SetFilter("SEATA", getSeataFilter)
方法可將其注入到 dubbo-go 的 filter 鏈。
grpc 可經過 metadata 傳遞 XID。
客戶端首先將 XID 放入 md := metadata.Pairs("XID", rootContext.GetXID())
,再將 metadata 傳入 context:metadata.NewOutgoingContext(context.Background(), md)
。
服務端則經過 md, ok := metadata.FromIncomingContext(ctx)
獲取到 metadata,再從中取出 XID。
AT 模式除了要對發起全局事務的方法作代理,還須要對數據源作代理。
seata 經過代理數據源,對 sql 語句進行解析,來獲取修改數據的修改前和修改後的數據,供 transaction coordinator 回滾時使用。對數據源的代理,只須要將你建立的 sql driver 實例注入到 seata-golang 的 db 操做對象中:
db, err := exec.NewDB(config.GetATConfig(), {你的 sql driver 實例})
若是你使用了 xorm 或者 gorm,則可從 xorm 對象或者 gorm 對象中取出 sql driver 實例,用上面的方法構造出 seata-golang 的 db 操做對象。這意味着你能夠同時使用 orm 框架和 seata-golang 框架,當你的操做須要用到事務時,用 seata-golang 的 db 操做對象去執行 sql 語句。
經過上一節的介紹,開發者已經能夠在服務端拿到上游傳遞過來的 XID 了。爲了將分支事務加入到全局事務組中,開發者須要使用獲取的 XID 構造一個 RootContext:
rootContext := &context.RootContext{Context: ctx} rootContext.Bind("{上游獲取到的 XID}")
開啓分支事務時,調用流程以下:
tx, err := dao.Begin(ctx)
。func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error)
。tx.Commit()
或 tx.Rollback()
來提交或回滾分支操做。最後,整個分支事務是否成功提交,執行成功仍是失敗須要返回結果給調用方,也就是全局事務的發起方,transaction manager 會根據返回的結果決定是否提交或回滾整個全局事務。
TCC 模式相較 AT 模式,約束會多一些。TCC 模式首先要求開發者實現 TccService 接口,還要求接口三個方法的參數都封裝到一個 BusinessActionContext 裏。
開發者調用 Try 方法,seata-golang 框架調用 Confirm/Cancel 方法。框架根據全部分支事務 Try 方法是否都執行成功,來決定發起全局提交或回滾。全局提交則由框架自動調用每一個事務分支的 Confirm 方法,全局回滾則調用加入事務組的全部事務分支的 Cancel 方法。
type TccService interface { Try(ctx *context.BusinessActionContext) (bool, error) Confirm(ctx *context.BusinessActionContext) bool Cancel(ctx *context.BusinessActionContext) bool }
在調用 Try 方法以前,事務分支要加入事務組,且須要把 Try 方法執行的上下文即 BusinessActionContext 存到 Transaction coordinator,這樣框架在提交或回滾時,才能把 BusinessActionContext 參數傳遞給 confirm、cancel 方法,這部分邏輯仍然經過代理實現。因此開發者還須要建立一個代理類,並實現接口 TccProxyService:
type TccProxyService interface { GetTccService() TccService }
經過調用 tcc.ImplementTCC({代理類實例})
方法,框架會爲代理類實現上述邏輯。開發者可在 samples/tcc 目錄查看 tcc 模式的示例。
除了項目結構目錄內的 samples,還有一個 dubbo-go 的例子 dubbo-go-seata。對於上文講述的接入方法,還但願讀者結合代碼多多理解,融匯貫通。
當前 seata-golang 與最新的 seata java 1.4 版本協議上徹底打通,若是有公司在技術棧上既有使用 java 語言也有使用 golang 語言,可接入 seata 框架來解決您的分佈式事務後顧之憂。
劉曉敏 (GitHubID dk-lockdown),目前就任於 h3c 成都分公司,擅長使用 Java/Go 語言,在雲原生和微服務相關技術方向均有涉獵,目前專攻分佈式事務。