4

seata-golang 接入指南

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzUzNzYxNjAzMg%3D%3D&%3Bmid=2247500996&%3Bidx=1&%3Bsn=b6de9068d72fe8b75ca8182fd784eebd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

yUB73eF.jpg!mobile

作者 | 刘晓敏

seata-golang 是一个分布式事务框架,实现了 AT 模式和 TCC 模式,AT 模式相较 TCC 模式对代码的入侵性更小、需要开发的接口更少;但 AT 模式对事务操作的数据持有全局锁,从这点来说,TCC 模式性能更好。

seata 的 AT 模式将全局锁放在 transaction coordinator 也就是事务协调器上,依赖于具体锁接口的存储实现方式可以是 file/db/redis 等,而不是数据库锁,每个分支事务提交时立即释放数据库锁,这样对数据库的压力也就减小了,变相得提升了数据库的性能。seata AT 模式和 TCC 模式的原理见:

下面以 seats-golang samples  为例,就 AT 模式和 TCC 模式如何接入到业务中做一个说明。

AT 模式接入

目录下,有三个微服务:product_svc、order_svc、aggregation_svc。

  • product_svc 负责创建订单时扣减库存。

  • order_svc 负责创建订单时写入订单主表和订单明细表。

  • aggregation_svc 通过 http 请求调用 order_svc 和 product _svc 的接口。

FjuAneu.png!mobile

1. 全局事务代理

熟悉 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 方法的返回错误信息决定提交还是回滚。

Ff6Ff27.png!mobile

2. 传递全局事务 ID

可以通过如下三种方式传递全局事务 ID。

1)Http

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") )用于分支事务处 理。

2)Dubbo

如果使用 dubbo 协议 rpc 通信,则需要把 XID 注入到 attachment 中传递到下游。

iQvaaeQ.png!mobile

如果使用 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{}
}

上面的 fil ter 通过 extension.SetFilter("SEATA", getSeataFilter) 方法可将其 注入到 dubbo-go 的 filter 链。

3)GRPC

grpc 可通过 metadata 传递 XID。

客户端首先将 XID 放入 md := metadata.Pairs("XID", rootContext.GetXID()) ,再将 metadata 传入 context: metadata.NewOutgoingContext(context.Background(), md)

服务端 则通过 md, ok := metadata.FromIncomingContext(ctx) 获取到  metadata,再从中取出 XID。

3. 事务分支处理

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}")

开启分支事务时,调用流程如下:

  • 调用 seata-golang 的 db 操作对象的 Begin 方法获取分支事务对象 tx, err := dao.Begin(ctx)

  • 执行 sql 语句则使用该分支事务对象 tx 的 Exec 方法 func (tx *Tx) Exec(query string, args ...interface{}) (sql.Result, error)

  • 执行完 sql 操作逻辑后,可根据返回的结果,调用 tx.Commit()tx.Rollback() 来提交或回滚分支操作。

最后,整个分支事务是否成功提交,执行成功还是失败需要返回结果给调用方,也就是全局事务的发起方,transaction manager 会根据返回的结果决定是否提交或回滚整个全局事务。

TCC 模式接入

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({代理类实例}) 方法 ,框架会为代理类实现上述逻辑。开发者可在 目录查看 tcc 模式的示例。

NfQz6zz.png!mobile

总结陈述

除了项目结构目录内的 samples,还有一个 dubbo-go 的例子 。对于上文讲述的接入方法,还希望读者结合代码多多理解,融汇贯通。

当前 seata-golang 与最新的 seata java 1.4 版本协议上完全打通,如果有公司在技术栈上既有使用 java 语言也有使用 golang 语言,可接入 seata 框架来解决您的分布式事务后顾之忧。

如果你有任何疑问,欢迎钉钉扫码加入交流群【钉钉群号 33069364】:

参考资料

  • seata 官方

    https://seata.io

  • java 版 seata

    https://github.com/seata/seata

  • seata-golang 项目地址

    https://github.com/opentrx/seata-golang

  • seata-golang go 夜读 b 站分享

    https://www.bilibili.com/video/BV1oz411e72T

  • 基于 getty 的 seata-golang 通信模型详解

    http://seata.io/zh-cn/blog/seata-golang-communication-mode.html

作者简介

刘晓敏  (GitHubID dk-lockdown),目前就职于 h3c 成都分公司,擅长使用 Java/Go 语言,在云原生和微服务相关技术方向均有涉猎,目前专攻分布式事务。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK