用Go轻松完成一个SAGA分布式事务,保姆级教程

ljsqisgu -
用Go轻松完成一个SAGA分布式事务,保姆级教程

银行跨行转账业务是一个典型分布式业务场景,假定A需求跨行转账给B,那么就触及两个银行的数据,无法经过一个数据库的本地业务确保转账的ACID,只能够经过分布式业务来处理。

分布式业务

分布式业务在分布式环境下,为了满意可用性、性能与降级服务的需求,降低一致性与隔离性的要求,一方面遵从 BASE 理论:

根本业务可用性(Basic Availability)
柔性状态(Soft state)
最终一致性(Eventual consistency)
另一方面,分布式业务也部分遵从 ACID 标准:

原子性:严厉遵从
一致性:业务完结后的一致性严厉遵从;业务中的一致性可适当放宽
隔离性:并行业务间不可影响;业务中心成果可见性答应安全放宽
持久性:严厉遵从
SAGA
Saga是这一篇数据库论文SAGAS提到的一个分布式业务方案。其中心思想是将长业务拆分为多个本地短业务,由Saga业务和谐器和谐,假设各个本地业务成功完结那就正常完结,假设某个步骤失利,则依据相反次序一次调用补偿操作。

目前可用于SAGA的开源结构,主要为Java言语,其中以seata为代表。咱们的比如选用go言语,运用的分布式业务结构为https://github.com/yedf/dtm,它对分布式业务的支撑十分优雅。下面来详细解说SAGA的组成:

DTM业务结构里,有3个角色,与经典的XA分布式业务一样:

AP/应用程序,建议大局业务,界说大局业务包含哪些业务分支
RM/资源管理器,担任分支业务各项资源的管理
TM/业务管理器,担任和谐大局业务的正确履行,包含SAGA正向/逆向操作的履行
下面看一个成功完结的SAGA时序图,就很简略了解SAGA分布式业务:

image.png

SAGA实践
对于咱们要进行的银行转账的比如,咱们将在正向操作中,进行转入转出,在补偿操作中,做相反的调整。

首要咱们创立账户余额表:

CREATE TABLE dtm_busi.user_account (
id int(11) AUTO_INCREMENT PRIMARY KEY,
user_id int(11) not NULL UNIQUE ,
balance decimal(10,2) NOT NULL DEFAULT '0.00',
create_time datetime DEFAULT now(),
update_time datetime DEFAULT now()
);
咱们先编写中心业务代码,调整用户的账户余额

func qsAdjustBalance(uid int, amount int) (interface{}, error) {

_, err := dtmcli.SdbExec(sdbGet(), "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid)
return dtmcli.ResultSuccess, err

}
下面咱们来编写具体的正向操作/补偿操作的处理函数

app.POST(qsBusiAPI+"/TransIn", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
    return qsAdjustBalance(2, 30)
}))
app.POST(qsBusiAPI+"/TransInCompensate", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
    return qsAdjustBalance(2, -30)
}))
app.POST(qsBusiAPI+"/TransOut", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
    return qsAdjustBalance(1, -30)
}))
app.POST(qsBusiAPI+"/TransOutCompensate", common.WrapHandler(func(c *gin.Context) (interface{}, error) {
    return qsAdjustBalance(1, 30)
}))

到此各个子业务的处理函数现已OK了,然后是开启SAGA业务,进行分支调用

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()

至此,一个完好的SAGA分布式业务编写完结。

假设您想要完好运转一个成功的示例,那么依照yedf/dtm项目的说明搭建好环境之后,经过下面指令运转saga的比如即可:

go run app/main.go quick_start
处理网络反常
假定提交给dtm的业务中,调用转入操作时,呈现时间短的毛病怎么办?依照SAGA业务的协议,dtm会重试未完结的操作,这时咱们要如何处理?毛病有可能是转入操作完结后出网络毛病,也有可能是转入操作完结中呈现机器宕机。如何处理才能够确保账户余额的调整是正确无问题的?

DTM提供了子业务屏障功用,确保屡次重试,只会有一次成功提交。(子业务屏障不只确保幂等,还能够处理空补偿等问题,概况参阅分布式业务最经典的七种处理方案的子业务屏障环节)

咱们把处理函数调整为:

func sagaBarrierAdjustBalance(sdb *sql.Tx, uid int, amount int) (interface{}, error) {

_, err := dtmcli.StxExec(sdb, "update dtm_busi.user_account set balance = balance + ? where user_id = ?", amount, uid)
return dtmcli.ResultSuccess, err

}

func sagaBarrierTransIn(c *gin.Context) (interface{}, error) {

return dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) {
    return sagaBarrierAdjustBalance(sdb, 1, reqFrom(c).Amount)
})

}

func sagaBarrierTransInCompensate(c *gin.Context) (interface{}, error) {

return dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) {
    return sagaBarrierAdjustBalance(sdb, 1, -reqFrom(c).Amount)
})

}
这儿的dtmcli.TroughBarrierCall调用会运用子业务屏障技能,确保第三个参数里的回调函数仅被处理一次

您能够测验屡次调用这个TransIn服务,仅有一次余额调整。您能够运转以下指令,运转新的处理方式:

go run app/main.go saga_barrier
处理回滚
假设银行将金额准备转入用户2时,发现用户2的账户反常,回来失利,会怎么样?咱们调整处理函数,让转入操作回来失利

func sagaBarrierTransIn(c *gin.Context) (interface{}, error) {

return dtmcli.ResultFailure, nil

}
咱们给出业务失利交互的时序图

image.png

这儿有一点,TransIn的正向操作什么都没有做,就回来了失利,此时调用TransIn的补偿操作,会不会导致反向调整出错了呢?

不必担心,前面的子业务屏障技能,能够确保TransIn的错误假设发生在提交之前,则补偿为空操作;TransIn的错误假设发生在提交之后,则补偿操作会将数据提交一次。

您能够将回来错误的TransIn改成:

func sagaBarrierTransIn(c *gin.Context) (interface{}, error) {

dtmcli.ThroughBarrierCall(sdbGet(), MustGetTrans(c), func(sdb *sql.Tx) (interface{}, error) {
    return sagaBarrierAdjustBalance(sdb, 1, 30)
})
return dtmcli.ResultFailure, nil

}
最终的成果余额仍旧会是对的,原理能够参阅:分布式业务最经典的七种处理方案的子业务屏障环节

小结
在这篇文章里,咱们介绍了SAGA的理论知识,也经过一个比如,完好给出了编写一个SAGA业务的过程,涵盖了正常成功完结,反常状况,以及成功回滚的状况。信任读者经过这边文章,对SAGA现已有了深化的了解。

文中运用的dtm是新开源的Golang分布式业务管理结构,功用强大,支撑TCC、SAGA、XA、业务消息等业务模式,支撑Go、python、PHP、node、csharp等言语的。同时提供了十分简略易用的接口。

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

git

扩展阅读

加个好友,技术交流

1628738909466805.jpg