1 參考文檔
- Go開源說第十七期 分布式事務DTM
- DTM開源項目文檔:官方文檔
- 分布式事務解決方案:7種常見解決方案匯總
-
msg
:二階段消息,適合不需要回滾的全局事務。 -
saga
:適合需要支持回滾的全局事務。 -
tcc
:適合一致性要求較高的全局事務。 -
xa
:適合性能要求不高,沒有行鎖爭搶的全局事務。
2 官方示例
參考:https://github.com/dtm-labs/dtm/blob/main/helper/README-cn.md
以下步驟均是按參考實操的。
- 運行
dtm
服務器(可以觀察事務執(zhí)行情況)
運行示例前,必須首先運行dtm
服務器。
git clone https://github.com/dtm-labs/dtm && cd dtm
go mod tidy
go run main.go
- 啟動并運行一個
saga
示例
下面運行一個類似跨行轉賬的示例,包括兩個事務分支:資金轉出(TransOut
)、資金轉入(TransIn
)。DTM
保證TransIn
和TransOut
要么全部成功,要么全部回滾,保證最終金額的正確性。
git clone https://github.com/dtm-labs/dtmcli-go-sample && cd dtmcli-go-sample
go run main.go
-
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
}
- 失敗情況
在實際的業(yè)務中,子事務可能出現失敗,例如轉入的子賬號被凍結導致轉賬失敗。我們對業(yè)務代碼進行修改,讓TransIn
的正向操作失敗,然后看看結果:
app.POST(qsBusiAPI+"/TransIn", func(c *gin.Context) {
logger.Infof("TransIn")
c.JSON(409, "") // Status 409 表示失敗,不再重試,直接回滾
})
在轉入操作失敗的情況下,TransIn
和TransOut
的補償操作被執(zhí)行,保證了最終的余額和轉賬前是一樣的。
3 go-zero使用dtm參考代碼
3.1 go-zero支持dtm 代碼操作步驟
參考:go-zero支持dtm:官方講解+代碼
注意:這里只是從調用方角度展示了如何使用dtm的代碼,而關于被調用方的代碼缺失,需要結合3.2 gozerodtm 代碼操作步驟
。
- 啟動
Etcd
,查看所有key
命令:etcdctl.exe get --prefix ""
- 把dtm
clone
下來 - 找到
dtm
項目根文件夾下的conf.sample.yml
,復制一份名稱改為conf.yml
- 把
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
的注冊服務發(fā)現驅動,支持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
端口,記得這里要改下端口。
- 啟動
dtm
# 前提:配置好conf.yml
go run main.go -c conf.yml
如果是用Goland
啟動,則需要將 -c conf.yml
作為參數填寫到Program arguments
中。
- 運行一個
go-zero
的服務
git clone https://github.com/dtm-labs/dtmdriver-clients && cd dtmdriver-clients
cd gozero/trans && go run trans.go
- 發(fā)起一個
go-zero
使用dtm
的事務
# 在dtmdriver-clients的目錄下
cd gozero/app && go run main.go
- 在
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
創(chuàng)建訂單并且調用stock-srv
扣減庫存,因為order-srv
與stock-srv
是2個獨立grpc
服務,所以使用dtm
來做分布式事務協調。
前5步與3.1完全一致。
- 創(chuàng)建數據庫表
在項目的datasql
目錄下分別有:order-srv
服務的dtm-gozero-order.sql
、stock-srv
服務的dtm-gozero-stock.sql
,在數據庫按照創(chuàng)建兩個數據庫并執(zhí)行相關sql
。 - 修改數據庫連接配置
分別修改order-srv
、stock-srv
服務的yaml
中的數據庫連接配置。 - 修改啟動類讀取配置文件的路徑
分別修改order-api
、order-srv
、stock-srv
中讀取yaml
配置的路徑。 - 啟動服務
依次啟動order-srv
、stock-srv
、order-api
服務 - 請求測試
使用POSTMAN
測試:
請求地址:http://localhost:8889/order/quickCreate
請求方式:POST
數據格式:JSON
請求數據:{"userId":2,"goodsId":1,"num":20}
- 測試結果
HTTP請求響應成功,狀態(tài)是200,但是查看DTM服務發(fā)現,操作失敗,且數據庫中數據未改變。
原因排查:
依次查看order-api
、order-srv
、stock-srv
服務后,發(fā)現在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
數據表。
- 創(chuàng)建
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;
- 重新請求測試
HTTP請求響應成功,狀態(tài)是200,DTM服務中狀態(tài)顯示操作成功,且數據庫中數據改變。
4 注意事項
在3.1 go-zero支持dtm 代碼操作步驟
和※3.2 gozerodtm 代碼操作步驟
有一些操作注意事項。
重點是子事務如何處理 回滾
、補償
等操作,這一塊需要特別留意。
4.1 grpc接口地址
在找grpc訪問的方法路徑時候,是在***.pb.go
的文件中找該方法invoke
的路徑。
原因:當proto文件方法名字都是大寫,這2者都一樣如果proto中方法名字小寫的,invoke中跟他的方法名就不一樣了
仔細觀察發(fā)現兩個路徑有大小寫之分!
※4.2 動態(tài)調用過程
在go-zero使用dtm的分布式事務時,許多的調用是從dtm服務器發(fā)起的,例如TCC的Confirm/Cancel,SAGA/MSG的所有調用。
dtm無需知道組成分布式事務的相關業(yè)務api的強類型,它是動態(tài)的調用這些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就拿到了完整信息,就能夠發(fā)起完整的調用了
Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1})
4.3 dtm的回滾補償
在使用dtm的grpc時候,當我們使用saga、tcc等如果第一步嘗試或者執(zhí)行失敗了,是希望它能執(zhí)行后面的rollback的,在grpc中的服務如果發(fā)生錯誤了,必須返回 : status.Error(codes.Aborted, dtmcli.ResultFailure) , 返回其他錯誤,不會執(zhí)行你的rollback操作,dtm會一直重試,如下圖:
4.4 barrier的空補償、懸掛等
這一步需要創(chuàng)建dtm_barrier
數據庫以及barrier
數據表。這個其實就是為我們的業(yè)務服務做了一個檢查,防止空補償,具體可以看barrier.Call中源碼,沒幾行代碼可以看懂的。
每個與db
交互的服務只要用到了barrier
,都需要這個。文章來源:http://www.zghlxwxcb.cn/news/detail-605999.html
4.5 barrier在rpc中本地事務
在rpc的業(yè)務中,如果使用了barrier的話,那么在model中與db交互時候必須要用事務,并且一定要跟barrier用同一個事務。
logic:
model:文章來源地址http://www.zghlxwxcb.cn/news/detail-605999.html
到了這里,關于go-zero學習 第六章 分布式事務dtm的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!