Golang基于DTM的分布式事务SAGA实战

SAGA介绍

SAGA是“长时间事务”运作效率的方法,大致思路是把一个大事务分解为可以交错运行的一系列子事务的集合。原本提出 SAGA 的目的,是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式

优势
  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现
缺点
  • 不保证隔离性

SAGA事务典型的时序图

SAGA失败的时序图

如图TM事务管理器,DTM是开源的分布式事务管理中间件

DTM的SAGA支持

dtm根据http的不同状态码来代表当前事务的处理结果

dtm事务默认无回滚时间支持,尽最大能力交付

失败重试默认为指数回避算法。需要固定时间重试需要在saga属性配置

dtm默认事务执行顺序为并发执行也是顺序执行,可以设置属性为并行执行

http状态码当前版本不能完全代表业务成功需要结合 返回msg具体看业务代码

实战

代码在宿主机运行 docker network:bridge

docker安装,安装成功后可以访问http://localhost:36789/ 打开dtm事务web-ui

代码github GitHub - Ssummer520/dtm-gin

docker run -itd  --name dtm -p 36789:36789 -p 36790:36790  yedf/dtm:latest
创建tm事务管理器提交全局事务
package main

import (
	"fmt"
	"github.com/dtm-labs/dtmcli"
	"github.com/gin-gonic/gin"
	"github.com/lithammer/shortuuid/v3"
	"log"
)

func main() {
	app := gin.Default()

	app.GET("/test", func(c *gin.Context) {
		QsFireRequest()
		log.Printf("TransOut")
		c.JSON(200, "sss")
	})
	app.Run(":1111")

}

const qsBusiAPI = "/api/busi_start"
const qsBusiPortIN = 8881
const qsBusiPortOUT = 8880
const dtmServer = "http://localhost:36789/api/dtmsvr"

var qsBusiIN = fmt.Sprintf("http://host.docker.internal:%d%s", qsBusiPortIN, qsBusiAPI)
var qsBusiOUT = fmt.Sprintf("http://host.docker.internal:%d%s", qsBusiPortOUT, qsBusiAPI)

func QsFireRequest() string {
	req := &ReqHTTP{Amount: 30} // load of micro-service
	// DtmServer is the url of dtm
	saga := dtmcli.NewSaga(dtmServer, shortuuid.New()).
		// add a TransOut sub-transaction,forward operation with url: qsBusi+"/TransOut", reverse compensation operation with url: qsBusi+"/TransOutCompensate"
		Add(qsBusiOUT+"/TransOut", qsBusiOUT+"/TransOutCompensate", req).
		// add a TransIn sub-transaction, forward operation with url: qsBusi+"/TransIn", reverse compensation operation with url: qsBusi+"/TransInCompensate"
		Add(qsBusiIN+"/TransIn", qsBusiIN+"/TransInCompensate", req)
	// submit the created saga transaction,dtm ensures all sub-transactions either complete or get revoked
	saga.RetryInterval = 1
	//saga.RequestTimeout = 10
	err := saga.Submit()

	if err != nil {
		panic(err)
	}
	return saga.Gid
}

type ReqHTTP struct {
	Amount int `json:"amount"`
}

saga全局事务属性设置

saga属性事务设置
type TransOptions struct {
	WaitResult         bool              `json:"wait_result,omitempty" gorm:"-"`     // 是否等待结果,默认为false
	TimeoutToFail      int64             `json:"timeout_to_fail,omitempty" gorm:"-"` // 事务失败的超时时间,单位:秒
	RequestTimeout     int64             `json:"request_timeout,omitempty" gorm:"-"` // 全局事务的请求超时时间,单位:秒
	RetryInterval      int64             `json:"retry_interval,omitempty" gorm:"-"`  // 重试间隔时间,单位:秒
	PassthroughHeaders []string          `json:"passthrough_headers,omitempty" gorm:"-"` // 需要传递的HTTP头部字段
	BranchHeaders      map[string]string `json:"branch_headers,omitempty" gorm:"-"`  // 自定义的分支头部字段,DTM服务器到服务API
	Concurrent         bool              `json:"concurrent" gorm:"-"`                // 是否并发执行,适用于saga和消息事务类型
}
rm1表示第一个微服务业务
package main

import (
	"fmt"
	"github.com/dtm-labs/dtmcli"
	"github.com/dtm-labs/dtmcli/dtmimp"
	"github.com/dtm-labs/dtmcli/logger"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	QsStartSvr()

}

// busi address
const qsBusiAPI = "/api/busi_start"
const qsBusiPort = 8881

// QsStartSvr quick start: start server
func QsStartSvr() {
	app := gin.Default()
	qsAddRoute(app)
	log.Printf("quick start examples listening at %d", qsBusiPort)

	app.Run(fmt.Sprintf(":%d", qsBusiPort))

}

func qsAddRoute(app *gin.Engine) {
	app.POST(qsBusiAPI+"/TransIn", func(c *gin.Context) {
		info := infoFromContext(c)
		var req ReqHTTP
		c.ShouldBindJSON(&req)
		log.Printf("TransIn:%v,gid:%v", req.Amount, info.Gid)
		c.JSON(http.StatusOK, dtmimp.OrString(MainSwitch.QueryPreparedResult.Fetch(), dtmcli.ResultSuccess)) // Status 409 for Failure. Won't be retried
	})
	app.POST(qsBusiAPI+"/TransInCompensate", func(c *gin.Context) {
		info := infoFromContext(c)
		var req ReqHTTP
		c.ShouldBindJSON(&req)
		log.Printf("TransInCompensate:%v,gid:%v", req.Amount, info.Gid)
		c.JSON(http.StatusOK, dtmimp.OrString(MainSwitch.QueryPreparedResult.Fetch(), dtmcli.ResultSuccess))
	})

}
func string2DtmError(str string) error {
	return map[string]error{
		dtmcli.ResultFailure: dtmcli.ErrFailure,
		dtmcli.ResultOngoing: dtmcli.ErrOngoing,
		dtmcli.ResultSuccess: nil,
		"":                   nil,
	}[str]
}

type mainSwitchType struct {
	TransInResult         AutoEmptyString
	TransOutResult        AutoEmptyString
	TransInConfirmResult  AutoEmptyString
	TransOutConfirmResult AutoEmptyString
	TransInRevertResult   AutoEmptyString
	TransOutRevertResult  AutoEmptyString
	QueryPreparedResult   AutoEmptyString
	NextResult            AutoEmptyString
	JrpcResult            AutoEmptyString
	FailureReason         AutoEmptyString
}

// AutoEmptyString auto reset to empty when used once
type AutoEmptyString struct {
	value string
}

// SetOnce set a value once
func (s *AutoEmptyString) SetOnce(v string) {
	s.value = v
}

// Fetch fetch the stored value, then reset the value to empty
func (s *AutoEmptyString) Fetch() string {
	v := s.value
	s.value = ""
	if v != "" {
		logger.Debugf("fetch obtain not empty value: %s", v)
	}
	return v
}

// MainSwitch controls busi success or fail
var MainSwitch mainSwitchType

func infoFromContext(c *gin.Context) *dtmcli.BranchBarrier {
	info := dtmcli.BranchBarrier{
		TransType: c.Query("trans_type"),
		Gid:       c.Query("gid"),
		BranchID:  c.Query("branch_id"),
		Op:        c.Query("op"),
	}
	return &info
}

type ReqHTTP struct {
	Amount int `json:"amount"`
}
rm2表示第二个微服务业务
package main

import (
	"fmt"
	"github.com/dtm-labs/dtmcli"
	"github.com/dtm-labs/dtmcli/dtmimp"
	"github.com/dtm-labs/dtmcli/logger"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
)

func main() {
	app := gin.Default()
	app.POST(qsBusiAPI+"/TransOut", func(c *gin.Context) {
		info := infoFromContext(c)
		var req ReqHTTP
		c.ShouldBindJSON(&req)
		log.Printf("TransOut:%v,gid:%v", req.Amount, info.Gid)
		c.JSON(http.StatusOK, dtmimp.OrString(MainSwitch.QueryPreparedResult.Fetch(), dtmcli.ResultSuccess))
	})
	app.POST(qsBusiAPI+"/TransOutCompensate", func(c *gin.Context) {
		info := infoFromContext(c)
		var req ReqHTTP
		c.ShouldBindJSON(&req)
		log.Printf("TransOutCompensate:%vgid:%v", req.Amount, info.Gid)
		c.JSON(http.StatusOK, dtmimp.OrString(MainSwitch.QueryPreparedResult.Fetch(), dtmcli.ResultSuccess))
	})
	log.Printf("quick start examples listening at %d", qsBusiPort)

	app.Run(fmt.Sprintf(":%d", qsBusiPort))
}

// busi address
const qsBusiAPI = "/api/busi_start"
const qsBusiPort = 8880

// QsStartSvr quick start: start server
func QsStartSvr() {

}

type mainSwitchType struct {
	TransInResult         AutoEmptyString
	TransOutResult        AutoEmptyString
	TransInConfirmResult  AutoEmptyString
	TransOutConfirmResult AutoEmptyString
	TransInRevertResult   AutoEmptyString
	TransOutRevertResult  AutoEmptyString
	QueryPreparedResult   AutoEmptyString
	NextResult            AutoEmptyString
	JrpcResult            AutoEmptyString
	FailureReason         AutoEmptyString
}

// AutoEmptyString auto reset to empty when used once
type AutoEmptyString struct {
	value string
}

// SetOnce set a value once
func (s *AutoEmptyString) SetOnce(v string) {
	s.value = v
}

// Fetch fetch the stored value, then reset the value to empty
func (s *AutoEmptyString) Fetch() string {
	v := s.value
	s.value = ""
	if v != "" {
		logger.Debugf("fetch obtain not empty value: %s", v)
	}
	return v
}

// MainSwitch controls busi success or fail
var MainSwitch mainSwitchType

type ReqHTTP struct {
	Amount int `json:"amount"`
}

func infoFromContext(c *gin.Context) *dtmcli.BranchBarrier {
	info := dtmcli.BranchBarrier{
		TransType: c.Query("trans_type"),
		Gid:       c.Query("gid"),
		BranchID:  c.Query("branch_id"),
		Op:        c.Query("op"),
	}
	return &info
}
结果

运行tm提交一个全局事务

rm1返回

rm2返回

dtm webui管理页面

当前业务已经消费成功

我们把这块修改为rm1 提交失败,看到rm2事务回滚

const (
	// StatusPrepared 表示全局/分支事务的状态。
	// 第一步,事务准备阶段
	StatusPrepared = "prepared"
	// StatusSubmitted 表示全局事务的状态。
	StatusSubmitted = "submitted"
	// StatusSucceed 表示全局/分支事务的状态。
	StatusSucceed = "succeed"
	// StatusFailed 表示全局/分支事务的状态。
	// 注意:将全局状态更改为失败可以停止触发(在生产环境中不推荐)
	StatusFailed = "failed"
	// StatusAborting 表示全局事务的状态。
	StatusAborting = "aborting"

	// ResultSuccess 事务/事务分支的结果成功
	ResultSuccess = dtmimp.ResultSuccess
	// ResultFailure 事务/事务分支的结果失败
	ResultFailure = dtmimp.ResultFailure
	// ResultOngoing 事务/事务分支的结果进行中
	ResultOngoing = dtmimp.ResultOngoing

	// DBTypeMysql 数据库驱动类型:MySQL
	DBTypeMysql = dtmimp.DBTypeMysql
	// DBTypePostgres 数据库驱动类型:PostgreSQL
	DBTypePostgres = dtmimp.DBTypePostgres
)

参考资料SAGA事务模式 | DTM开源项目文档

https://zhuanlan.zhihu.com/p/688088173

Go语言基于Redis实现的分布式限流是一种常见的解决方案,可以有效地控制系统的并发访问流量,防止系统被过多的请求压垮。 首先,分布式限流需要使用Redis的计数器功能。通过对每个请求进行计数,并设置一个时间窗口,可以统计在该窗口内的请求次数。当请求次数超过某个阈值时,可以拒绝该请求或者进行降级处理。 其次,为了保证分布式限流的准确性和高效性,需要使用Redis的原子操作,例如INCR、EXPIRE等。INCR命令可以原子地将计数器的值加1,并返回加1后的结果,而EXPIRE命令可以设置计数器的过期时间。通过这些原子操作,可以在多个节点之间共享计数状态,并且保证计数器的同步和高效性。 此外,为了保证系统的稳定性和可靠性,需要考虑设置适当的限流阈值和时间窗口大小。根据系统的负载情况和性能需求,可以调整这些参数,实现对系统流量的合理控制。 在实际应用中,可以使用Go语言的Redis客户端连接Redis服务器,并通过相关命令操作计数器。同时,还可以结合其他的组件和技术,如分布式锁、消息队列等,增强系统的稳定性和可扩展性。 总之,Go语言基于Redis实现的分布式限流是一种可行且有效的解决方案,可以帮助我们应对大流量的并发请求,保证系统的稳定运行。通过合理设定限流参数和灵活运用Redis的功能,我们可以实现流量控制、降级和保护系统免受恶意请求的攻击。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值