DTM分布式事务

2024-01-08 16:44
文章标签 分布式 事务 dtm

本文主要是介绍DTM分布式事务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

DTM分布式事务

从内网看到了关于事务在业务中的讨论,评论区大佬有提及DTM开源项目[https://dtm.pub/],开学开学

基础理论

一、Why DTM

​ 项目产生于实际生产中的问题,涉及订单支付的服务会将所有业务相关逻辑放到一个大的本地事务,导致大量耦合,复杂度大幅提升

​ java成熟的分布式事务解决方案,使用代价过高:大量业务用java重写

​ DTM,Distributed Transaction Manager, 其是一个分布式事务管理器,解决跨数据库、跨服务、跨语言更新数据的一致性问题。

​ DTM提供了Saga、TCC、XA和二阶段消息模式以满足不同应用场景的需求,同时首创的子事务屏障技术有效解决幂等悬挂空补偿等异常问题。

​ DTM的优点:

  • 提供简单易用的接口,拆分具体业务接入分布式事务
  • 支持多语言栈
  • 核心技术子事务屏蔽,降低处理子事务乱的难度

二、 快速上手

1、Demo

​ 了解DTM的发展和特点,quick start一下8⃣️

// 运行dtm
git clone https://github.com/dtm-labs/dtm && cd dtm
go run main.go
// 运行一个saga示例
go run qs/main.go

​ 上述Saga示例实现一个类似跨行转账的功能,包括两个事务分支:资金转出(TransOut)、资金转入(TransIn)。DTM保证TransIn和TransOut要么全成功,要么全回滚,保证最终金额的正确性。

  // 具体业务微服务地址const qsBusi = "http://localhost:8081/api/busi_saga"req := &gin.H{"amount": 30} // 微服务的载荷// DtmServer为DTM服务的地址,是一个urlDtmServer := "http://localhost:36789/api/dtmsvr"saga := dtmcli.NewSaga(DtmServer, shortuuid.New()).// 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 补偿操作为url: qsBusi+"/TransOutCompensate"Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req).// 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransIn", 补偿操作为url: qsBusi+"/TransInCompensate"Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req)// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务err := saga.Submit()

在这里插入图片描述

2、时序图

在这里插入图片描述

​ 从以上时序图可以看出,DTM整个全局事务分为如下几步:

  1. 用户定义好全局事务所有的事务分支(全局事务的组成部分称为事务分支),然后提交给DTM,DTM持久化全局事务信息后,立即返回
  2. DTM取出第一个事务分支,这里是TransOut,调用该服务并成功返回
  3. DTM取出第二个事务分支,这里是TransIn,调用该服务并成功返回
  4. DTM已完成所有的事务分支,将全局事务的状态修改为已完成

失败情况:

​ 在实际业务中,子事务可以出现失败,例如转入的子账号被冻结导致转账失败。因此可以对业务代码进行修改来模拟TransIn正向操作失败

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, "")})
}

​ 再次运行,整个事务最终失败,时序图:

在这里插入图片描述

在转入操作失败的情况下,TransIn和TransOut的补偿操作被执行,保证了最终的余额和转账前是一样的

在这里插入图片描述

三、二阶段消息Demo

业务场景:

​ 跨行转账是典型的分布式事务场景,在这里A需要跨行转账给B

假设需求场景:

​ 只有转出A可能失败,转入B是能够最终成功的

​ 二阶段消息是DTM首创的事务模式,用于替换本地事务表和事务消息这两种现有的方案

​ 二阶段消息能够保证本地事务的提交和全局事务的提交是原子性的,适合解决不需要回滚的分布式事务场景

1、核心代码
// SagaAdjustBalance 1
func SagaAdjustBalance(db dtmcli.DB, uid int, amount int, result string) error {if strings.Contains(result, dtmcli.ResultFailure) {return dtmcli.ErrFailure}_, err := dtmimp.DBExec(BusiConf.Driver, db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid)return err
}

调整用户的账号余额

app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler(func(c *gin.Context) interface{} {barrier := MustBarrierFromGin(c)return barrier.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)})}))

barrier.Call主要用于处理幂等,保证重复调用不会多次调整余额

开启事务,进行分支调用

	gid := dtmimp.GetFuncName()req := busi.GenReqHTTP(30, false, false)msg := dtmcli.NewMsg(DtmServer, gid).Add(busi.Busi+"/SagaBTransIn", req)err := msg.DoAndSubmitDB(Busi+"/QueryPreparedB", dbGet().ToSQLDB(), func(tx *sql.Tx) error {return busi.SagaAdjustBalance(tx, busi.TransOutUID, -req.Amount, "SUCCESS")})

该代码保证了DoAndSubmitDB的业务提交和全局事务提交是原子性的,保证TransOut和TransIn同时成功or失败。 其中DoAndSubmitDB第一个参数为回查URL

	app.GET(BusiAPI+"/QueryPrepared", dtmutil.WrapHandler(func(c *gin.Context) interface{} {logger.Debugf("%s QueryPrepared", c.Query("gid"))return string2DtmError(dtmimp.OrString(MainSwitch.QueryPreparedResult.Fetch(), dtmcli.ResultSuccess))}))app.GET(BusiAPI+"/QueryPreparedB", dtmutil.WrapHandler(func(c *gin.Context) interface{} {logger.Debugf("%s QueryPreparedB", c.Query("gid"))bb := MustBarrierFromGin(c)db := dbGet().ToSQLDB()return bb.QueryPrepared(db)}))
2、原子性

在这里插入图片描述

四、SAGA型事务Demo

业务场景:

​ A需要跨行转账给B

假设场景:

​ 转出A和转入B都可能成功和失败,需要最终转入转出都成功or失败

核心思想:

​ 将长事务拆分为多个本地短事务,有Saga事务协调器来协调,如果各个本地事务成功完成则正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。

1、核心代码

​ 调整用户的账户余额

func SagaAdjustBalance(db dtmcli.DB, uid int, amount int, result string) error {if strings.Contains(result, dtmcli.ResultFailure) {return dtmcli.ErrFailure}_, err := dtmimp.DBExec(BusiConf.Driver, db, "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid)return err
}

​ 正向操作/补偿操作的处理函数(源码新增的Demo并未在此作展示

		app.POST(BusiAPI+"/SagaBTransIn", dtmutil.WrapHandler(func(c *gin.Context) interface{} {barrier := MustBarrierFromGin(c)return barrier.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return SagaAdjustBalance(tx, TransInUID, reqFrom(c).Amount, reqFrom(c).TransInResult)})}))app.POST(BusiAPI+"/SagaBTransInCom", dtmutil.WrapHandler(func(c *gin.Context) interface{} {barrier := MustBarrierFromGin(c)return barrier.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return SagaAdjustBalance(tx, TransInUID, -reqFrom(c).Amount, "")})}))app.POST(BusiAPI+"/SagaBTransOut", dtmutil.WrapHandler(func(c *gin.Context) interface{} {barrier := MustBarrierFromGin(c)return barrier.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return SagaAdjustBalance(tx, TransOutUID, -reqFrom(c).Amount, reqFrom(c).TransOutResult)})}))app.POST(BusiAPI+"/SagaBTransOutCom", dtmutil.WrapHandler(func(c *gin.Context) interface{} {barrier := MustBarrierFromGin(c)return barrier.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return SagaAdjustBalance(tx, TransOutUID, reqFrom(c).Amount, "")})}))

开启事务,进行分支调用

		req := &busi.ReqHTTP{Amount: 30}// DtmServer为DTM服务的地址saga := dtmcli.NewSaga(dtmutil.DefaultHTTPServer, shortuuid.New()).// 添加一个TransOut的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransOutCom"Add(busi.Busi+"/SagaBTransOut", busi.Busi+"/SagaBTransOutCom", req).// 添加一个TransIn的子事务,正向操作为url: qsBusi+"/TransOut", 逆向操作为url: qsBusi+"/TransInCom"Add(busi.Busi+"/SagaBTransIn", busi.Busi+"/SagaBTransInCom", req)// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务logger.Debugf("busi trans submit")err := saga.Submit()
2、时序图

​ 与快速上手demo一致

在这里插入图片描述

3、处理网络异常

​ 假设提交给dtm的事务中, 调用转入操作时,出现短暂的故障——>dtm会重试未完成的操作,此时要求全局事务中的各个子事务时幂等的

​ 子事务屏障技术,提供了BranchBarrier工具类,提供Call函数来保证函数内部的业务最多被调用一次。

4、处理回滚

​ 事务失败交互的时序图:

在这里插入图片描述

  • TransIn的正向操作发生在提交之前,则补偿为空操作
  • TransIn的操作如果发生在提交后,则补偿操作会将数据提交一次

五、TCC型事务Demo

业务场景:

​ A需要跨行转账给B

假设场景:

​ 转出A和转入B都可能成功和失败,需要最终转入转出都成功or失败

​ 还有一个要求,假如发生回滚,SAGA 模式下会发生A发现自己的余额被扣减了,但是收款方B迟迟没有收到余额,那么会对A造成很大的困扰。业务上面希望不要出现这种情况

TCC分为3个阶段

  • Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
  • Confirm 阶段:如果所有分支的Try都成功了,则走到Confirm阶段。Confirm真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源
  • Cancel 阶段:如果所有分支的Try有一个失败了,则走到Cancel阶段。Cancel释放 Try 阶段预留的业务资源。

在这里插入图片描述

1、核心代码

​ 冻结/解冻资金操作,会检查约束balance+trading_balance >= 0,如果约束不成立,执行失败(trading_balance 表示被冻结的金额

func tccAdjustTrading(db dtmcli.DB, uid int, amount int) error {affected, err := dtmimp.DBExec(BusiConf.Driver, db, `update dtm_busi.user_accountset trading_balance=trading_balance+?where user_id=? and trading_balance + ? + balance >= 0`, amount, uid, amount)if err == nil && affected == 0 {return fmt.Errorf("update error, maybe balance not enough")}return err
}func tccAdjustBalance(db dtmcli.DB, uid int, amount int) error {affected, err := dtmimp.DBExec(BusiConf.Driver, db, `update dtm_busi.user_accountset trading_balance=trading_balance-?,balance=balance+? where user_id=?`, amount, amount, uid)if err == nil && affected == 0 {return fmt.Errorf("update user_account 0 rows")}return err
}

​ Try/Confirm/Cancel处理函数:

		app.POST(BusiAPI+"/TccBTransOutTry", dtmutil.WrapHandler(func(c *gin.Context) interface{} {req := reqFrom(c)if req.TransOutResult != "" {return string2DtmError(req.TransOutResult)}bb := MustBarrierFromGin(c)if req.Store == Redis {return bb.RedisCheckAdjustAmount(RedisGet(), GetRedisAccountKey(TransOutUID), req.Amount, 7*86400)} else if req.Store == Mongo {return bb.MongoCall(MongoGet(), func(sc mongo.SessionContext) error {return SagaMongoAdjustBalance(sc, sc.Client(), TransOutUID, -req.Amount, "")})}return bb.CallWithDB(pdbGet(), func(tx *sql.Tx) error {return tccAdjustTrading(tx, TransOutUID, -req.Amount)})}))app.POST(BusiAPI+"/TccBTransOutConfirm", dtmutil.WrapHandler(func(c *gin.Context) interface{} {if reqFrom(c).Store == Redis || reqFrom(c).Store == Mongo {return nil}return MustBarrierFromGin(c).CallWithDB(pdbGet(), func(tx *sql.Tx) error {return tccAdjustBalance(tx, TransOutUID, -reqFrom(c).Amount)})}))app.POST(BusiAPI+"/TccBTransOutCancel", dtmutil.WrapHandler(TccBarrierTransOutCancel))app.POST(BusiAPI+"/TccBTransInTry", dtmutil.WrapHandler(func(c *gin.Context) interface{} {req := reqFrom(c)if req.TransInResult != "" {return string2DtmError(req.TransInResult)}return MustBarrierFromGin(c).CallWithDB(pdbGet(), func(tx *sql.Tx) error {return tccAdjustTrading(tx, TransInUID, req.Amount)})}))app.POST(BusiAPI+"/TccBTransInConfirm", dtmutil.WrapHandler(func(c *gin.Context) interface{} {return MustBarrierFromGin(c).CallWithDB(pdbGet(), func(tx *sql.Tx) error {return tccAdjustBalance(tx, TransInUID, reqFrom(c).Amount)})}))app.POST(BusiAPI+"/TccBTransInCancel", dtmutil.WrapHandler(func(c *gin.Context) interface{} {return MustBarrierFromGin(c).CallWithDB(pdbGet(), func(tx *sql.Tx) error {return tccAdjustTrading(tx, TransInUID, -reqFrom(c).Amount)})}))

开启事务,进行分支调用

	req := busi.GenReqHTTP(30, false, false)gid := dtmimp.GetFuncName()// TccGlobalTransaction 会开启一个全局事务err := dtmcli.TccGlobalTransaction(DtmServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) {// CallBranch 会将事务分支的Confirm/Cancel注册到全局事务上,然后直接调用Try_, err := tcc.CallBranch(req, Busi+"/TccBTransOutTry", Busi+"/TccBTransOutConfirm", Busi+"/TccBTransOutCancel")assert.Nil(t, err)return tcc.CallBranch(req, Busi+"/TccBTransInTry", Busi+"/TccBTransInConfirm", Busi+"/TccBTransInCancel")})
2、处理网络异常

​ 同SAGA部分

3、处理回滚

​ 事务失败交互的时序图:

在这里插入图片描述

​ 跟成功的TCC差别就在于,当某个子事务返回失败后,后续就回滚全局事务,调用各个子事务的Cancel操作,保证全局事务全部回滚

  • TransIn的正向操作发生在提交之前,则补偿为空操作
  • TransIn的操作如果发生在提交后,则补偿操作会将数据提交一次

这篇关于DTM分布式事务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/584130

相关文章

集中式版本控制与分布式版本控制——Git 学习笔记01

什么是版本控制 如果你用 Microsoft Word 写过东西,那你八成会有这样的经历: 想删除一段文字,又怕将来这段文字有用,怎么办呢?有一个办法,先把当前文件“另存为”一个文件,然后继续改,改到某个程度,再“另存为”一个文件。就这样改着、存着……最后你的 Word 文档变成了这样: 过了几天,你想找回被删除的文字,但是已经记不清保存在哪个文件了,只能挨个去找。真麻烦,眼睛都花了。看

MySql 事务练习

事务(transaction) -- 事务 transaction-- 事务是一组操作的集合,是一个不可分割的工作单位,事务会将所有的操作作为一个整体一起向系统提交或撤销请求-- 事务的操作要么同时成功,要么同时失败-- MySql的事务默认是自动提交的,当执行一个DML语句,MySql会立即自动隐式提交事务-- 常见案例:银行转账-- 逻辑:A给B转账1000:1.查询

开源分布式数据库中间件

转自:https://www.csdn.net/article/2015-07-16/2825228 MyCat:开源分布式数据库中间件 为什么需要MyCat? 虽然云计算时代,传统数据库存在着先天性的弊端,但是NoSQL数据库又无法将其替代。如果传统数据易于扩展,可切分,就可以避免单机(单库)的性能缺陷。 MyCat的目标就是:低成本地将现有的单机数据库和应用平滑迁移到“云”端

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因

laravel框架实现redis分布式集群原理

在app/config/database.php中配置如下: 'redis' => array('cluster' => true,'default' => array('host' => '172.21.107.247','port' => 6379,),'redis1' => array('host' => '172.21.107.248','port' => 6379,),) 其中cl

基于MySQL实现的分布式锁

概述 在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。 但是到了分布式系统的时代,这种

Kafka 分布式消息系统详细介绍

Kafka 分布式消息系统 一、Kafka 概述1.1 Kafka 定义1.2 Kafka 设计目标1.3 Kafka 特点 二、Kafka 架构设计2.1 基本架构2.2 Topic 和 Partition2.3 消费者和消费者组2.4 Replica 副本 三、Kafka 分布式集群搭建3.1 下载解压3.1.1 上传解压 3.2 修改 Kafka 配置文件3.2.1 修改zookeep

spring事务属性的xml格式配置

实际是使用代理做的事务优化 <!--配置事务的属性--><tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--匹配所有以add开头的方法--><tx:method name="add*" propagation="REQUIRED" /> <tx:metho

Spring Cloud整合Seata实现分布式事务

文章目录 1.Seata1.1 官网1.2 下载1.3 通过安装包运行seata1.3.1 解压seata-server-1.3.0.zip1.3.2 修改 conf/file.conf 配置文件1.3.3 修改conf/registry.conf配置文件1.3.4 添加seata配置信息到nacos1.3.5 配置seata服务端数据库表结构1.3.6 启动seata 2.Spring

ELK+Spring Cloud搭建分布式日志中心

ELK+Spring Cloud搭建分布式日志中心 1.ELK简介2.资源包下载3.Elasticsearch安装3.1 解压Elasticsearch3.2 修改Elasticsearch的配置文件3.3 修改系统配置3.4 启动Elasticsearch 4.ElasticSearch-head插件安装5.Logstash安装6.Kibana安装7.SpringCloud集成logsta