go-zero学习 第六章 分布式事务dtm_gozero分布式事务(1),大专生三面蚂蚁金服

160 阅读7分钟

3. `dtmcli-go-sample` 中的具体代码



package main

import ( "fmt" "log" "time"

"github.com/dtm-labs/dtmcli"
"github.com/gin-gonic/gin"

)

// 事务参与者的服务地址 const qsBusiAPI = "/api/busi_start" const qsBusiPort = 8082

var qsBusi = fmt.Sprintf("http://localhost:%d%s", qsBusiPort, qsBusiAPI)

func main() { QsStartSvr() _ = QsFireRequest() select {} }

// QsStartSvr quick start: start server func QsStartSvr() { app := gin.New() qsAddRoute(app) log.Printf("quick start examples listening at %d", qsBusiPort) go func() { _ = app.Run(fmt.Sprintf(":%d", qsBusiPort)) }() time.Sleep(100 * time.Millisecond) }

func qsAddRoute(app *gin.Engine) { app.POST(qsBusiAPI+"/TransIn", func(c *gin.Context) { log.Printf("TransIn") c.JSON(200, "") // c.JSON(409, "") // Status 409 for Failure. Won't be retried }) app.POST(qsBusiAPI+"/TransInCompensate", func(c *gin.Context) { log.Printf("TransInCompensate") c.JSON(200, "") }) app.POST(qsBusiAPI+"/TransOut", func(c *gin.Context) { log.Printf("TransOut") c.JSON(200, "") }) app.POST(qsBusiAPI+"/TransOutCompensate", func(c *gin.Context) { log.Printf("TransOutCompensate") c.JSON(200, "") }) }

const dtmServer = "http://localhost:36789/api/dtmsvr"

// QsFireRequest quick start: fire request func QsFireRequest() string { req := &gin.H{"amount": 30} // 微服务的载荷 // DtmServer为DTM服务的地址 saga := dtmcli.NewSaga(dtmServer, dtmcli.MustGenGid(dtmServer)). // 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCompensate" Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req). // 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransInCompensate" Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req) // 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务 err := saga.Submit()

if err != nil {
	panic(err)
}
log.Printf("transaction: %s submitted", saga.Gid)
return saga.Gid

}


4. 失败情况  
 在实际的业务中,子事务可能出现失败,例如转入的子账号被冻结导致转账失败。我们对业务代码进行修改,让`TransIn`的正向操作失败,然后看看结果:



app.POST(qsBusiAPI+"/TransIn", func(c \*gin.Context) {
	logger.Infof("TransIn")
	c.JSON(409, "") // Status 409 表示失败,不再重试,直接回滚
})

在转入操作失败的情况下,`TransIn``TransOut`的补偿操作被执行,保证了最终的余额和转账前是一样的。


## 3 go-zero使用dtm参考代码


### 3.1 go-zero支持dtm 代码操作步骤


参考:[go-zero支持dtm]( ):官方讲解+代码


**注意**:这里只是从调用方角度展示了如何使用dtm的代码,而关于被调用方的代码缺失,需要结合`3.2 gozerodtm 代码操作步骤`1. 启动`Etcd`,查看所有`key`命令:`etcdctl.exe get --prefix ""`
2. 把[dtm]( ) `clone`下来
3. 找到`dtm`项目根文件夹下的`conf.sample.yml`,复制一份名称改为`conf.yml`
4.`conf.yml`中的下面这段注释放开:



MicroService: # gRPC/HTTP based microservice config Driver: 'dtm-driver-gozero' # name of the driver to handle register/discover Target: 'etcd://localhost:2379/dtmservice' # register dtm server to this url EndPoint: 'localhost:36790'


* `MicroService`:这个不要动,这个代表要对把`dtm`注册到那个微服务服务集群里面去,使微服务集群内部服务可以通过`grpc`直接跟`dtm`交互
* `Driver``'dtm-driver-gozero'`, 使用`go-zero`的注册服务发现驱动,支持`go-zero`* `Target`: `'etcd://localhost:2379/dtmservice'`将当前`dtm``server`直接注册到微服务所在的`etcd`集群中,如果`go-zero`作为微服务使用的话,就可以直接通过`etcd`拿到`dtm``server grpc`链接,直接就可以跟`dtm server`交互了。
* `EndPoint`: `'localhost:36790'` , 代表的是`dtm``server`的连接地址+端口 , 集群中的微服务可以直接通过`etcd`获得此地址跟`dtm`交互了,如果你自己去改了`dtm`源码`grpc`端口,记得这里要改下端口。


5. 启动`dtm`



前提:配置好conf.yml

go run main.go -c conf.yml


如果是用`Goland`启动,则需要将 `-c conf.yml`作为参数填写到`Program arguments`中。


6. 运行一个`go-zero`的服务



git clone github.com/dtm-labs/dt… && cd dtmdriver-clients cd gozero/trans && go run trans.go


7. 发起一个`go-zero`使用`dtm`的事务



在dtmdriver-clients的目录下

cd gozero/app && go run main.go


8.`trans`的日志中看到记录信息,就是事务正常完成了



2023/07/24 21:45:47 transfer out 30 cents from 1 2023/07/24 21:45:47 transfer in 30 cents to 2
2023/07/24 21:45:47 transfer out 30 cents from 1 2023/07/24 21:45:47 transfer out 30 cents from 1


### ※3.2 gozerodtm 代码操作步骤


参考:[gozerodtm]( ):Mikaelemmmm代码


**项目介绍:**


* `order-api``http`服务入口。
* `order-srv`是订单的`rpc`服务,与`dtm-gozero-order`数据库中`order`表交互。
* `stock-srv`是库存的`rpc`服务,与`dtm-gozero-stock`数据库中`stock`表交互。


**整体流程:**  
 `http`调用`order-api`中立即下单接口,然后`order-api`立即下单接口会去调用`order-srv`创建订单并且调用`stock-srv`扣减库存,因为`order-srv``stock-srv`是2个独立`grpc`服务,所以使用`dtm`来做分布式事务协调。


前5步与3.1完全一致。


1. 创建数据库表  
 在项目的`datasql`目录下分别有:`order-srv`服务的`dtm-gozero-order.sql``stock-srv`服务的`dtm-gozero-stock.sql`,在数据库按照创建两个数据库并执行相关`sql`2. 修改数据库连接配置  
 分别修改 `order-srv``stock-srv`服务的`yaml`中的数据库连接配置。
3. 修改启动类读取配置文件的路径  
 分别修改`order-api``order-srv``stock-srv`中读取`yaml`配置的路径。
4. 启动服务  
 依次启动`order-srv``stock-srv``order-api`服务
5. 请求测试  
 使用`POSTMAN`测试:  
 请求地址:<http://localhost:8889/order/quickCreate>  
 请求方式:`POST`  
 数据格式:`JSON`  
 请求数据:`{"userId":2,"goodsId":1,"num":20}`
6. 测试结果  
 HTTP请求响应成功,状态是200,但是查看[DTM服务]( )发现,操作失败,且数据库中数据未改变。  
 原因排查:  
 依次查看`order-api``order-srv``stock-srv`服务后,发现在`order-srv`服务有报错,主要报错信息:



{"level":"error","ts":"2023-07-25T14:08:24.891+0800","caller":"dtmimp/utils.go:207","msg":"used: 0 ms exec error: Error 1146: Table 'dtm_barrier.barrier' doesn't exist for insert i gnore into dtm_barrier.barrier(trans_type, gid, branch_id, op, barrier_id, reason) values(?,?,?,?,?,?)


推断是缺少`dtm_barrier`数据库以及`barrier`数据表。


7. 创建`dtm_barrier`数据库以及`barrier`数据表



/* Navicat Premium Data Transfer

Source Server : 本机 Source Server Type : MySQL Source Server Version : 50737 Source Host : 127.0.0.1:3357 Source Schema : dtm_barrier

Target Server Type : MySQL Target Server Version : 50737 File Encoding : 65001

Date: 25/07/2023 14:06:43 */

SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0;


-- Table structure for barrier


DROP TABLE IF EXISTS barrier; CREATE TABLE barrier ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', trans\_type varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, gid varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, branch\_id varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, op varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, barrier\_id bigint(20) NULL DEFAULT NULL, reason varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, create\_time datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (id) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'dtm子屏障数据表' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;


8. 重新请求测试  
 HTTP请求响应成功,状态是200,[DTM服务]( )中状态显示操作成功,且数据库中数据改变。


## 4 注意事项`3.1 go-zero支持dtm 代码操作步骤``※3.2 gozerodtm 代码操作步骤`有一些操作注意事项。


**重点是子事务如何处理 `回滚`、`补偿`等操作,这一块需要特别留意。**


### 4.1 grpc接口地址


在找grpc访问的方法路径时候,是在`***.pb.go`的文件中找该方法`invoke`的路径。  
 原因:当proto文件方法名字都是大写,这2者都一样如果proto中方法名字小写的,invoke中跟他的方法名就不一样了  
 ![在这里插入图片描述](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0f84a72bf3f7486b9e1e461ecfe3c178~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1773066904&x-signature=310Igb64U7Bdl0NRQk45hV7asoQ%3D)  
 仔细观察发现两个路径有大小写之分!  
 ![在这里插入图片描述](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ded83f72f0864b0aa9931578b1a4c7b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1773066904&x-signature=I1BjLC8GGhFXAA9C3MbMG4LOQhc%3D)


### ※4.2 动态调用过程




![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f42a3de12b8946459ce0477c9814fea9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1773066904&x-signature=qe7Hrk0IDieGqw1ZtGrwzS5UoIQ%3D)
![img](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a73db279d7e4368a5e0fdb2ff5fcb5c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo5a2m5Lmg5LmL5b-DQUk=:q75.awebp?rk3s=f64ab15b&x-expires=1773066904&x-signature=%2BiYDDjZ8kayJ%2FTd5BSwho2cHuOk%3D)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://gitee.com/vip204888)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**