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中跟他的方法名就不一样了

仔细观察发现两个路径有大小写之分!

### ※4.2 动态调用过程
在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})
### 4.3 dtm的回滚补偿
在使用dtm的grpc时候,当我们使用saga、tcc等如果第一步尝试或者执行失败了,是希望它能执行后面的rollback的,在grpc中的服务如果发生错误了,必须返回 : status.Error(codes.Aborted, dtmcli.ResultFailure) , 返回其他错误,不会执行你的rollback操作,dtm会一直重试,如下图:

### 4.4 `barrier`的空补偿、悬挂等
这一步需要创建`dtm_barrier`数据库以及`barrier`数据表。这个其实就是为我们的业务服务做了一个检查,防止空补偿,具体可以看barrier.Call中源码,没几行代码可以看懂的。
每个与`db`交互的服务只要用到了`barrier`,都需要这个。
### 4.5 `barrier`在`rpc`中本地事务


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://gitee.com/vip204888)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**