vlambda博客
学习文章列表

分布式事务实战,用Go轻松完成一个TCC

TCC分布式事务来源于 2007 年Pat Helland发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文,TCC分别是Try、Confirm、Cancel的手写字母。

组成

TCC有三个分支

  • Try 分支:预留锁定业务相关资源,如果资源不够,则返回失败

  • Confirm 分支:如果前面的Try全部成功,则进入Confirm,进行数据变更,这个阶段不会返回失败

  • Cancel 分支:如果前面的Try没有全部成功,有返回失败的,则进入Cancel。Cancel解冻Try锁定的资源,也类似Confirm是不会返回失败的。

假设有一个银行跨行转账的业务,因为不同银行,数据不在同一个数据库,而更可能在不同微服务下的数据库里。这是一个典型的分布式事务场景,我们看看一个成功的TCC时序图:

实践

A转账给B的跨行转账操作,如果转账不成功,我们不想让用户看到自己账上的余额变动过,因此我们在Try阶段冻结相关的余额,Confirm阶段进行转账,Cancel阶段进行余额解冻。这样可以避免A看到自己的存款减少了,但是最后转账又失败的情况。

下面是具体的开发详情

我们采用go语言,使用https://github.com/yedf/dtm这个功能强大又简单易用的分布式事务框架

创建两张表,一个用户余额表,另一个是冻结资金表,语句如下:

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

CREATE TABLE dtm_busi.`user_account_trading` (
`id` int(11) AUTO_INCREMENT PRIMARY KEY,
`user_id` int(11) not NULL UNIQUE ,
`trading_balance` decimal(10,2) NOT NULL DEFAULT '0.00',
`create_time` datetime DEFAULT now(),
`update_time` datetime DEFAULT now()
);

trading表中trading_balance记录的是交易中的金额。

最重要的业务代码包括冻结/解冻资金和调整余额,代码如下

func adjustTrading(uid int, amount int) (interface{}, error) { 幂等、悬挂处理 dbr := sdb.Exec("update dtm_busi.user_account_trading t join dtm_busi.user_account a on t.user_id=a.user_id and t.user_id=? set t.trading_balance=t.trading_balance + ? where a.balance + t.trading_balance + ? >= 0", uid, amount, amount) if dbr.Error == nil && dbr.RowsAffected == 0 { // 如果余额不足,返回错误 return nil, fmt.Errorf("update error, balance not enough") } 其他情况检查及处理}
func adjustBalance(uid int, amount int) (ret interface{}, rerr error) { 幂等、悬挂处理 这里略去进行相关的事务处理,包括开启事务,以及在defer中处理提交或回滚 // 将原先冻结的资金记录解冻 dbr := db.Exec("update dtm_busi.user_account_trading t join dtm_busi.user_account a on t.user_id=a.user_id and t.user_id=? set t.trading_balance=t.trading_balance + ?", uid, -amount) if dbr.Error == nil && dbr.RowsAffected == 1 { // 解冻成功 // 调整金额 dbr = db.Exec("update dtm_busi.user_account set balance=balance+? where user_id=?", amount, uid) } 其他情况检查及处理}

业务有个重要约束balance+trading_balance >= 0,表示用户最终的余额不能为负。如果约束不成立,返回失败

然后是Try/Confirm/Cancel的处理函数,他们比较简单

RegisterPost(app, "/api/TransInTry", func (c *gin.Context) (interface{}, error) {
return adjustTrading(1, reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransInConfirm", func TransInConfirm(c *gin.Context) (interface{}, error) {
return adjustBalance(1, reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransInCancel", func TransInCancel(c *gin.Context) (interface{}, error) {
return adjustTrading(1, -reqFrom(c).Amount)
})

RegisterPost(app, "/api/TransOutTry", func TransOutTry(c *gin.Context) (interface{}, error) {
return adjustTrading(2, -reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransOutConfirm", func TransInConfirm(c *gin.Context) (interface{}, error) {
return adjustBalance(2, -reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransOutCancel", func TransInCancel(c *gin.Context) (interface{}, error) {
return adjustTrading(2, reqFrom(c).Amount)
})

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

err := dtmcli.TccGlobalTransaction(DtmServer, gid, func(tcc *dtmcli.Tcc) (*resty.Response, error) { resp, err := tcc.CallBranch(&TransReq{Amount: 30}, Busi+"/TccBTransOutTry", Busi+"/TccBTransOutConfirm", Busi+"/TccBTransOutCancel") if err != nil { return resp, err } return tcc.CallBranch(&TransReq{Amount: 30}, Busi+"/TccBTransInTry", Busi+"/TccBTransInConfirm", Busi+"/TccBTransInCancel")  })

至此,一个TCC分布式事务全部完成。


yedf/dtm项目中有完整的示例,您可以访问该项目,通过下面命令运行上述的示例


go run app/main.go tcc_barrier

回滚

跨行转账有可能出现失败,例如A转账给B,但是B的账户由于各类原因异常,返回无法转入,这种情况会怎么样?我们可以修改代码,让我们的示例处理这种情况:

RegisterPost(app, "/api/TransInTry", func (c *gin.Context) (interface{}, error) {
return gin.H{"dtm_result":"FAILURE"}, nil
})

因为B账户的异常,会导致整个全局事务的回滚,时序图如下:


这个时序图与成功的时序图非常相近,主要差别在于TransIn返回了失败,后续的操作由Confirm变成了Cancel。

小结

这篇文章完整的介绍了TCC事务的全过程,包括TCC事务的业务设计要点、一个成功完成的例子、一个成功回滚的例子。相信读者到这里,已经对TCC有了很清晰的理解。

全局事务进行过程中,可能出现各类网络异常,例如收到重复的Cancel或者未收到Try却收到Cancel等。这类难题的处理技巧,以及其他分布式事务模式如SAGA、XA等,可以参考我的另一篇文章《分布式事务最经典的七种解决方案》,里面有全面的讲解

文中使用的例子节选自github.com/yedf/dtm,该框架功能强大又简单易用。

  • 支持多种事务模式:TCC、SAGA、XA、事务消息; 

  • 跨语言支持,已支持 golang、python、PHP、nodejs等语言的客户端。

  • 提供子事务屏障功能,优雅解决幂等、悬挂、空补偿等问题。

阅读完此篇干货,欢迎大家访问https://github.com/yedf/dtm项目,给颗星星支持!

您可以点击左下角的“阅读原文”,直接访问项目