所谓的gozero适配dtm
以下是gozero的示例,本质不就是维护了dtm配置
go-zero支持
dtm与go-zero进行了深度合作,打造了go-zero原生支持分布式事务的解决方案,提供了极简的用户体验。
dtm从v1.6.0开始原生支持go-zero微服务框架,go-zero的版本需要v1.2.4以上
运行一个已有的示例
我们以etcd作为注册服务中心为例,按照如下步骤运行一个go-zero的示例:
- 启动etcd
bash
# 前提:已安装etcd
etcd
- 配置dtm
yaml
MicroService:
Driver: 'dtm-driver-gozero' # 配置dtm使用go-zero的微服务协议
Target: 'etcd://localhost:2379/dtmservice' # 把dtm注册到etcd的这个地址
EndPoint: 'localhost:36790' # dtm的本地地址
- 启动dtm
bash
# 前提:配置好conf.yml
go run app/main.go -c conf.yml
- 运行一个go-zero的服务
bash
git clone https://github.com/dtm-labs/dtmdriver-clients && cd dtmdriver-clients
cd gozero/trans && go run trans.go
- 发起一个go-zero使用dtm的事务
bash
# 在dtmdriver-clients的目录下
cd gozero/app && go run main.go
当您在trans的日志中看到
csharp
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
// 下面这行导入gozero的dtm驱动
import _ "github.com/dtm-labs/driver-gozero"
// 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()
整个开发接入的过程很少,前面的注释已经很清晰,就不再赘述了
正确的做法,任何go项目都可以去适配dtm
说白了就是让服务找到dtm的调用入口
1. 使用dtm,传参dtm server地址
go
gid := dtmgrpc.MustGenGid(l.svcCtx.Config.DtmServerAddr)
2. tcc事务上下文丢失解决方案
go
package dtmtool
import (
"context"
"violet/common"
"github.com/dtm-labs/client/dtmcli/dtmimp"
"github.com/dtm-labs/client/dtmgrpc"
"github.com/dtm-labs/client/dtmgrpc/dtmgimp"
"go.opentelemetry.io/otel"
)
func TccGlobalTransactionWithContext(ctx context.Context, dtm string, gid string, tccFunc dtmgrpc.TccGlobalFunc) (rerr error) {
headers := make(common.BranchHeaders)
propagator := otel.GetTextMapPropagator()
propagator.Inject(ctx, headers)
tcc := &dtmgrpc.TccGrpc{TransBase: *dtmimp.NewTransBase(gid, "tcc", dtm, "")}
tcc.Context = ctx
tcc.BranchHeaders = headers
rerr = dtmgimp.DtmGrpcCall(&tcc.TransBase, "Prepare")
if rerr != nil {
return rerr
}
defer dtmimp.DeferDo(&rerr, func() error {
return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Submit")
}, func() error {
tcc.RollbackReason = rerr.Error()
return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Abort")
})
rerr = tccFunc(tcc)
return
}
func SagaTransactionWithContext(ctx context.Context, dtm string, gid string) *dtmgrpc.SagaGrpc {
headers := make(common.BranchHeaders)
propagator := otel.GetTextMapPropagator()
propagator.Inject(ctx, headers)
saga := dtmgrpc.NewSagaGrpc(dtm, gid)
saga.Context = ctx
saga.BranchHeaders = headers
return saga
}
3. 完整示例
go
gid := dtmgrpc.MustGenGid(l.svcCtx.Config.DtmServerAddr)
err = dtmtool.TccGlobalTransactionWithContext(l.ctx, l.svcCtx.Config.DtmServerAddr, gid, func(tcc *dtmgrpc.TccGrpc) error {
financeRpcFullAddr := k8stool.CombineFullAddr(l.svcCtx.Config.Namespace, l.svcCtx.Config.FinanceRpcAddr)
userRpcFullAddr := k8stool.CombineFullAddr(l.svcCtx.Config.Namespace, l.svcCtx.Config.UserRpcAddr)
transferRequest := &financeRpc.CreateTransferApplyReq{
UserId: userId,
UserName: userName,
OutPlatform: req.FromAccountPlatform,
InPlatform: req.ToAccountPlatform,
OutAmount: amount.String(),
InAmount: inAmount,
OutCurrency: fromCurrency,
InCurrency: toCurrency,
OutAccount: req.FromAccount,
InAccount: req.ToAccount,
OrderSource: orderSource,
Rate: rate,
}
var transferResp financeRpc.CreateTransferApplyRes
err = tcc.CallBranch(transferRequest,
financeRpcFullAddr+financeRpc.TransferApplication_TccTryCreateTransferApply_FullMethodName,
financeRpcFullAddr+financeRpc.TransferApplication_TccConfirmCreateTransferApply_FullMethodName,
financeRpcFullAddr+financeRpc.TransferApplication_TccCancelCreateTransferApply_FullMethodName,
&transferResp)
if err != nil {
logx.WithContext(l.ctx).Errorf("划转记录失败,err:%v", err)
return err
}
if transferResp.Status.Code != 0 {
return common.NewErr(int(transferResp.Status.Code), transferResp.Status.Message)
}
resp = &types.TransactionResponse{
Amount: amount.String(),
AmountReceived: inAmount,
OrderId: transferResp.OrderId,
OrderTime: transferResp.CreateTime,
}
if req.FromAccountPlatform == comment.AccountWallet {
// 钱包调整记录
walletAdjustmentReq := &walletAdjust.CreateWalletAdjustmentReq{
UserId: userId,
UserName: userName,
UserType: userType,
ChangeBeforeAmount: fromBalance.String(),
AdminUser: comment.AdminUser,
ChangeType: int32(comment.OUT_Amount),
Wallet: req.FromAccount,
WalletCurrency: fromCurrency,
ChangeAmount: amount.String(),
ChangeMethod: int32(comment.INNER_TRANS),
TradeOrder: &transferResp.OrderId,
}
var walletAdjustmentRes walletAdjust.Status
err = tcc.CallBranch(walletAdjustmentReq,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletAdjustment_FullMethodName,
&walletAdjustmentRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("钱包调整记录失败,err:%v", err)
return err
}
if walletAdjustmentRes.Status.Code != 0 {
return common.NewErr(int(walletAdjustmentRes.Status.Code), walletAdjustmentRes.Status.Message)
}
// 钱包流水
walletTransHisReq := &walletAdjust.WalletTransactionHistory{
UserId: userId,
UserName: userName,
UserType: userType,
WalletId: req.FromAccount,
Currency: fromCurrency,
Amount: req.Amount,
ChangeBeforeAmount: fromBalance.String(),
ChangeType: int32(comment.OUT_Amount),
Remark: "划转,转出到交易账户",
ChangeMethod: common.TransactionTypeTransferOut,
OrderId: transferResp.OrderId,
}
var walletTransHisRes walletAdjust.WalletTransactionHistoryID
err = tcc.CallBranch(walletTransHisReq,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletTransactionHistory_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletTransactionHistory_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletTransactionHistory_FullMethodName,
&walletTransHisRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("tcc write wallet transaction history error: %v", err)
return err
}
if walletTransHisRes.Status.Code != 0 {
return common.NewErr(int(walletTransHisRes.Status.Code), walletTransHisRes.Status.Message)
}
// 钱包减钱
var adjustWalletBalance userRpc.Status
err = tcc.CallBranch(&userRpc.RWalletBalanceAdjustRequest{
WalletId: req.FromAccount,
Type: 2,
Amount: amount.String(),
},
userRpcFullAddr+userRpc.RWallet_TccTryAdjustRWalletBalance_FullMethodName,
userRpcFullAddr+userRpc.RWallet_TccConfirmAdjustRWalletBalance_FullMethodName,
userRpcFullAddr+userRpc.RWallet_TccCancelCreateRWallet_FullMethodName,
&adjustWalletBalance)
if err != nil {
logx.WithContext(l.ctx).Errorf("钱包余额调整失败,err:%v", err)
return err
}
if adjustWalletBalance.Status.Code != 0 {
return common.NewErr(int(adjustWalletBalance.Status.Code), adjustWalletBalance.Status.Message)
}
// 账户调整记录
accountAdjustmentReq := &accountAdjust.CreateAccountAdjustmentReq{
UserId: userId,
UserName: userName,
UserType: userType,
ChangeType: int32(comment.IN_Amount),
AdminUser: comment.AdminUser,
Account: req.ToAccount,
AccountPlatform: req.ToAccountPlatform,
Currency: toCurrency,
BeforeAmount: toBalance.String(),
ChangeAmount: inAmount,
ChangeMethod: int32(comment.INNER_TRANS),
TradeOrder: &transferResp.OrderId,
}
var accountAdjustmentRes accountAdjust.CreateAccountAdjustmentRes
err = tcc.CallBranch(accountAdjustmentReq,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccTryCreateAccountAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccConfirmCreateAccountAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccCancelCreateAccountAdjustment_FullMethodName,
&accountAdjustmentRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("账户调整记录失败,err:%v", err)
return err
}
if accountAdjustmentRes.Status.Code != 0 {
return common.NewErr(int(accountAdjustmentRes.Status.Code), accountAdjustmentRes.Status.Message)
}
// 账户加钱
payload := &finance_task.AccountAdjustBalance{
ApplyId: transferResp.OrderId,
AdjustmentID: accountAdjustmentRes.OrderId,
}
taskInfo, _ := l.svcCtx.StaskClient.EnqueueContext(l.ctx, payload.CreateTransactionTaskFactory())
logx.WithContext(l.ctx).Infof("task id: %+v", taskInfo.ID)
} else {
// 钱包调整记录
walletAdjustmentReq := &walletAdjust.CreateWalletAdjustmentReq{
UserId: userId,
UserName: userName,
UserType: userType,
ChangeBeforeAmount: toBalance.String(),
AdminUser: comment.AdminUser,
ChangeType: int32(comment.IN_Amount),
Wallet: req.ToAccount,
WalletCurrency: toCurrency,
ChangeAmount: inAmount,
ChangeMethod: int32(comment.INNER_TRANS),
TradeOrder: &transferResp.OrderId,
}
var walletAdjustmentRes walletAdjust.Status
err = tcc.CallBranch(walletAdjustmentReq,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletAdjustment_FullMethodName,
&walletAdjustmentRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("钱包调整记录失败,err:%v", err)
return err
}
if walletAdjustmentRes.Status.Code != 0 {
return common.NewErr(int(walletAdjustmentRes.Status.Code), walletAdjustmentRes.Status.Message)
}
// 钱包流水
walletTransHisReq := &walletAdjust.WalletTransactionHistory{
OrderId: transferResp.OrderId,
UserId: userId,
UserName: userName,
UserType: userType,
WalletId: req.ToAccount,
Currency: toCurrency,
Amount: inAmount,
ChangeBeforeAmount: toBalance.String(),
ChangeType: int32(comment.IN_Amount),
Remark: "划转,交易账户转入",
ChangeMethod: common.TransactionTypeTransferIn,
}
var walletTransHisRes walletAdjust.WalletTransactionHistoryID
err = tcc.CallBranch(walletTransHisReq,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletTransactionHistory_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletTransactionHistory_FullMethodName,
financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletTransactionHistory_FullMethodName,
&walletTransHisRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("tcc write wallet transaction history error: %v", err)
return err
}
if walletTransHisRes.Status.Code != 0 {
return common.NewErr(int(walletTransHisRes.Status.Code), walletTransHisRes.Status.Message)
}
// 钱包加钱
var adjustWalletBalance userRpc.Status
err = tcc.CallBranch(&userRpc.RWalletBalanceAdjustRequest{
WalletId: req.ToAccount,
Type: 1,
Amount: inAmount,
},
userRpcFullAddr+userRpc.RWallet_TccTryAdjustRWalletBalance_FullMethodName,
userRpcFullAddr+userRpc.RWallet_TccConfirmAdjustRWalletBalance_FullMethodName,
userRpcFullAddr+userRpc.RWallet_TccCancelCreateRWallet_FullMethodName,
&adjustWalletBalance)
if err != nil {
logx.WithContext(l.ctx).Errorf("钱包余额调整失败,err:%v", err)
return err
}
if adjustWalletBalance.Status.Code != 0 {
return common.NewErr(int(adjustWalletBalance.Status.Code), adjustWalletBalance.Status.Message)
}
// 账户调整记录
accountAdjustmentReq := &accountAdjust.CreateAccountAdjustmentReq{
UserId: userId,
UserName: userName,
UserType: userType,
ChangeType: int32(comment.OUT_Amount),
AdminUser: comment.AdminUser,
Account: req.FromAccount,
AccountPlatform: req.FromAccountPlatform,
Currency: fromCurrency,
BeforeAmount: fromBalance.String(),
ChangeAmount: amount.String(),
ChangeMethod: int32(comment.INNER_TRANS),
TradeOrder: &transferResp.OrderId,
}
var accountAdjustmentRes accountAdjust.CreateAccountAdjustmentRes
err = tcc.CallBranch(accountAdjustmentReq,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccTryCreateAccountAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccConfirmCreateAccountAdjustment_FullMethodName,
financeRpcFullAddr+financeRpc.AccountAdjustment_TccCancelCreateAccountAdjustment_FullMethodName,
&accountAdjustmentRes)
if err != nil {
logx.WithContext(l.ctx).Errorf("账户调整记录失败,err:%v", err)
return err
}
if accountAdjustmentRes.Status.Code != 0 {
return common.NewErr(int(accountAdjustmentRes.Status.Code), accountAdjustmentRes.Status.Message)
}
payload := &finance_task.AccountAdjustBalance{
ApplyId: transferResp.OrderId,
AdjustmentID: accountAdjustmentRes.OrderId,
}
taskInfo, _ := l.svcCtx.StaskClient.EnqueueContext(l.ctx, payload.CreateTransactionTaskFactory())
logx.WithContext(l.ctx).Infof("task id: %+v", taskInfo.ID)
}
return nil
})
4. 为什么要注册上下文?
为的是子事务屏障可以使用正确的上下文信息,子事务屏障如何使用呢?要从db入手
go
package sql
import (
"context"
"database/sql"
"fmt"
"github.com/dtm-labs/client/dtmcli"
"github.com/dtm-labs/client/dtmcli/dtmimp"
"github.com/dtm-labs/client/dtmgrpc/dtmgimp"
"github.com/zeromicro/go-zero/core/logx"
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
"gorm.io/gorm"
"violet/common"
)
type barrierDBWrapper struct {
ctx context.Context
DB *gorm.DB
}
type sqlResult struct {
rowsAffected int64
err error
}
func (ss *sqlResult) LastInsertId() (int64, error) {
return 0, nil
}
// RowsAffected dtm_barrier 只会用了RowsAffected,没有使用 LastInsertId 所以没有实现 LastInsertId
func (ss *sqlResult) RowsAffected() (int64, error) {
return ss.rowsAffected, ss.err
}
func NewDBWrapper(ctx context.Context, db *gorm.DB) *barrierDBWrapper {
return &barrierDBWrapper{
ctx: ctx,
DB: db,
}
}
func (bb *barrierDBWrapper) Exec(query string, args ...interface{}) (sql.Result, error) {
result := bb.DB.Exec(query, args...)
return &sqlResult{
rowsAffected: result.RowsAffected,
err: result.Error,
}, result.Error
}
func (bb *barrierDBWrapper) QueryRow(query string, args ...interface{}) *sql.Row {
return bb.DB.WithContext(bb.ctx).Raw(query, args...).Row()
}
type BarrierGormFunc func(gtx *gorm.DB) error
type DtmBarrier struct {
ctx context.Context
TransType string
Gid string
BranchID string
Op string
BarrierID int
DBType string // DBTypeMysql | DBTypePostgres
BarrierTableName string
}
func NewBarrierFromGRPC(ctx context.Context) (*DtmBarrier, error) {
tb := dtmgimp.TransBaseFromGrpc(ctx)
barrier := &DtmBarrier{
TransType: tb.TransType,
Gid: tb.Gid,
BranchID: tb.BranchID,
Op: tb.Op,
ctx: ctx,
}
if barrier.TransType == "" || barrier.Gid == "" || barrier.BranchID == "" || barrier.Op == "" {
return nil, fmt.Errorf("invalid trans info: %v", barrier)
}
return barrier, nil
}
func (bb *DtmBarrier) String() string {
return fmt.Sprintf("transInfo: %s %s %s %s", bb.TransType, bb.Gid, bb.BranchID, bb.Op)
}
func (bb *DtmBarrier) newBarrierID() string {
bb.BarrierID++
return fmt.Sprintf("%02d", bb.BarrierID)
}
// CallWithDB 结合gorm 对dtm barrier 的封装
// 注意:
// 1. BarrierGormFunc中对数据库的CREATE、UPDATE、DELETE必须使用参数gtx而不是d.DB, SELECT 两者都可以
// 2. CallWithDB 函数中使用gdb已经启动了一个事务,而且会自动提交,所以不必再BarrierGormFunc启动新事物
func (bb *DtmBarrier) CallWithDB(db *gorm.DB, busiCall BarrierGormFunc) (rerr error) {
tx := db.WithContext(bb.ctx).Begin()
defer dtmimp.DeferDo(&rerr, func() error {
return tx.Commit().Error
}, func() error {
return tx.Rollback().Error
})
rerr = bb.Call(db, busiCall)
return
}
func (bb *DtmBarrier) CallWithDBandTracing(db *gorm.DB, operationName string, busiCall BarrierGormFunc) (rerr error) {
c, span := common.StartSpan(bb.ctx, "sql", func(r oteltrace.Span) {
r.SetAttributes(attribute.Key("sql.method").String(operationName))
})
var err error
defer func() {
common.EndSpan(span, err)
}()
bb.ctx = c
// 执行数据库操作并记录错误
err = bb.CallWithDB(db, busiCall)
return err
}
func (bb *DtmBarrier) Call(gtx *gorm.DB, busiCall BarrierGormFunc) (rerr error) {
bid := bb.newBarrierID()
originOp := map[string]string{
dtmimp.OpCancel: dtmimp.OpTry, // tcc
dtmimp.OpCompensate: dtmimp.OpAction, // saga
dtmimp.OpRollback: dtmimp.OpAction, // workflow
}[bb.Op]
originAffected, oerr := dtmimp.InsertBarrier(NewDBWrapper(bb.ctx, gtx), bb.TransType, bb.Gid, bb.BranchID, originOp, bid, bb.Op, bb.DBType, bb.BarrierTableName)
currentAffected, rerr := dtmimp.InsertBarrier(NewDBWrapper(bb.ctx, gtx), bb.TransType, bb.Gid, bb.BranchID, bb.Op, bid, bb.Op, bb.DBType, bb.BarrierTableName)
logx.WithContext(bb.ctx).Debugf("originAffected: %d currentAffected: %d", originAffected, currentAffected)
if rerr == nil && bb.Op == dtmimp.MsgDoOp && currentAffected == 0 { // for msg's DoAndSubmit, repeated insert should be rejected.
return dtmcli.ErrDuplicated
}
if rerr == nil {
rerr = oerr
}
if (bb.Op == dtmimp.OpCancel || bb.Op == dtmimp.OpCompensate || bb.Op == dtmimp.OpRollback) && originAffected > 0 || // null compensate
currentAffected == 0 { // repeated request or dangled request
return
}
if rerr == nil {
rerr = busiCall(gtx)
}
return
}
5. 使用子事务屏障
go
func (d *DAO) ConfirmCreateDepositApply(ctx context.Context, apply *DepositApplication) (*DepositApplication, error) {
barrier, err := sql.NewBarrierFromGRPC(ctx)
if err != nil {
logx.WithContext(ctx).Error(err)
// codes.Internal 错误会重试
return nil, err
}
//只修改状态
err = barrier.CallWithDBandTracing(d.DB, "ConfirmCreateDepositApply", func(gtx *gorm.DB) error {
err = gtx.Model(&DepositApplication{}).Where("gid = ? and trans_status = ?", barrier.Gid, common.DtmTransactionStatusPending).
Update("trans_status", common.DtmTransactionStatusCompleted).Error
if err != nil {
return err
}
return nil
})
if err != nil {
logx.Errorw("ConfirmCreateDepositApply error", logx.Field("err", err))
}
return apply, err
}
本教程到此为止,有什么不懂的请私信,有时间逐一解答