go-zero分布式事务实践

背景 随着业务的快速发展、业务复杂度越来越高,微服务作为最佳解决方案之一,它解耦服务,降低复杂度,增加可维护性的同时,也带来一部分新问题。
当我们需要跨服务保证数据一致性时,原先的数据库事务力不从心,无法将跨库、跨服务的多个操作放在一个事务中。这样的应用场景非常多,我们可以列举出很多:

  • 跨行转账场景,数据不在一个数据库,但需要保证余额扣减和余额增加要么同时成功,要么同时失败
  • 发布文章后,更新文章总数等统计信息。其中发布文章和更新统计信息通常在不同的微服务中
  • 微服务化之后的订单系统
  • 出行旅游需要在第三方系统同时定几张票
面对这些本地事务无法解决的场景,我们需要分布式事务的解决方案,保证跨服务、跨数据库更新数据的一致性。
go-zero与dtm强强联合,推出了在go-zero中无缝接入dtm的极简方案,让分布式事务的使用从未如此简单。
运行一个例子 我们来看一个可运行的例子,然后再看如何自己开发完成一个完整的分布式事务
下面以etcd作为注册服务中心,可以按照如下步骤运行一个go-zero的示例:
  • 配置dtm
    MicroService: Driver: 'dtm-driver-gozero' # 配置dtm使用go-zero的微服务协议 Target: 'etcd://localhost:2379/dtmservice' # 把dtm注册到etcd的这个地址 EndPoint: 'localhost:36790' # dtm的本地地址

  • 启动etcd
    # 前提:已安装etcd etcd

  • 启动dtm
    # 前提:已配置好dtm的数据库链接 go run app/main.go dev

  • 运行一个go-zero的服务
    git clone github.com/yedf/dtmdriver-clients && cd dtmdriver-clients cd gozero/trans && go run trans.go

  • 用go-zero发起一个dtm的事务
    # 在dtmdriver-clients的目录下 cd gozero/app && go run main.go

当您在trans的日志中看到
2021/12/03 15:44:05 transfer out 30 cents from 1 2021/12/03 15:44:05 transfer in 30 cents to 2 2021/12/03 15:44:05 transfer out 30 cents from 1 2021/12/03 15:44:05 transfer out 30 cents from 1

那就是事务正常完成了
开发接入 【go-zero分布式事务实践】参考yedf/dtmdriver-clients的代码
// 下面这行导入gozero的dtm驱动 import _ "github.com/yedf/dtmdriver-gozero"// 使用dtm的客户端dtmgrpc之前,需要执行下面这行调用,告知dtmgrpc使用gozero的驱动来如何处理gozero的url err := dtmdriver.Use("dtm-driver-gozero") // check err// dtm已经通过前面的配置,注册到下面这个地址,因此在dtmgrpc中使用该地址 var dtmServer = "etcd://localhost:2379/dtmservice"// 下面从配置文件中Load配置,然后通过BuildTarget获得业务服务的地址 var c zrpc.RpcClientConf conf.MustLoad(*configFile, &c) busiServer, err := c.BuildTarget()// 使用dtmgrpc生成一个消息型分布式事务并提交 gid := dtmgrpc.MustGenGid(dtmServer) msg := dtmgrpc.NewMsgGrpc(dtmServer, gid). // 事务的第一步为调用trans.TransSvcClient.TransOut // 可以从trans.pb.go中找到上述方法对应的Method名称为"/trans.TransSvc/TransOut" // dtm需要从dtm服务器调用该方法,所以不走强类型,而是走动态的url: busiServer+"/trans.TransSvc/TransOut" Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1}). Add(busiServer+"/trans.TransSvc/TransIn", &busi.BusiReq{Amount: 30, UserId: 2}) err := msg.Submit()

整个开发接入的过程很少,前面的注释已经很清晰,就不再赘述了
注意事项
在开发接入的过程中,去找*.pb.go的文件中的grpc访问的方法路径时候,一定要找invoke的路径
go-zero分布式事务实践
文章图片

go-zero分布式事务实践
文章图片

深入理解动态调用 在go-zero使用dtm的分布式事务时,许多的调用是从dtm服务器发起的,例如TCC的Confirm/Cancel,SAGA/MSG的所有调用。
dtm无需知道组成分布式事务的相关业务api的强类型,它是动态的调用这些api。
grpc的调用,可以类比于HTTP的POST,其中:
  • c.BuildTarget() 产生的target类似于URL中的Host
  • "/trans.TransSvc/TransOut" 相当于URL中的Path
  • &busi.BusiReq{Amount: 30, UserId: 1} 相当于Post中Body
  • pb.Response 相当于HTTP请求的响应
通过下面这部分代码,dtm就拿到了完整信息,就能够发起完整的调用了
Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1})
更加完整的例子 热心的社区同学Mikael帮忙写了一个内容更加丰富的例子,结合实际应用和子事务屏障,完整的演示了一个线上实际运行的分布式事务,有兴趣的同学可以参考:
https://github.com/Mikaelemmmm/gozerodtm
其他方式接入 go-zero的微服务还有非etcd的其他方式,我们依次说明他们的接入方式
直连
对于直连这种方式,您只需要在上面dtm的etcd配置基础上,将Target设置为空字符串即可。
直连的情况,不需要将dtm注册到注册中心
K8S
对于K8S这种方式,您只需要在上面dtm的etcd配置基础上,将Target设置为空字符串即可。
在K8S中,将服务注册到K8S中,是由deployment.yaml完成的,应用内部,不需要进行注册
直播分享预告 go-zero的作者和我(dtm的作者)将在12月22日晚21点,在talkgo,联合做一场《go-zero的分布式事务实践》的直播分享,将会带来更多更深入的讨论。欢迎大家届时参加。
直播地址为:https://live.bilibili.com/111...
小结 这一次go-zero与dtm的合作,在go生态中,打造了首个原生支持分布式事务的微服务解决方案,意义重大。
  • go-zero项目地址:https://github.com/zeromicro/go-zero
  • dtm项目地址:https://github.com/yedf/dtm
欢迎大家使用我们的go-zerodtm,使用我们原生的“分布式事务的微服务解决方案”,并star支持我们

    推荐阅读