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

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

### ※4.2 动态调用过程


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