5

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

 2 years ago
source link: http://dockone.io/article/2434622
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

分布式事务实战:用 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 等,可以参考我的另一篇文章《分布式事务最经典的七种解决方案》,里面有全面的讲解。

原文链接:https://mp.weixin.qq.com/s/lCOfgGp19NdoQz1-aaAmHg

Recommend

  • 161

    Financial-level flexible distributed transaction solution https://dromara.org/ English |

  • 90

  • 10
    • wuwenliang.net 3 years ago
    • Cache

    我说分布式事务之TCC

    我说分布式事务之TCC | 朝·闻·道接触分布式相关的开发已经有一段时间了,自然绕不开分布式事务。从本文开始,我将带领大家了解常见的分布式事务的解决方案,深入原理,浅出实践,让我们在今后的开发中对分布式事务不再畏惧。 常见的分布式解决方案有:

  • 10
    • wuwenliang.net 3 years ago
    • Cache

    分布式事务之聊聊TCC

    分布式事务之聊聊TCC | 朝·闻·道分布式事务在分布式架构中是一个难以躲开的话题。常见的方案有三种,分别是 一、结合MQ消息中间件实现的可靠消息最终一致性二、TCC补偿性事务解决方案三、最大努力通知型方案 第一种方案:可靠...

  • 4

    带你用go轻松完成一个saga分布式事务 yedf · 5天之前 · 196 次点击 · 预计阅读时间 1 分钟 · 大...

  • 6

    什么是分布式事务?银行跨行转账业务是一个典型分布式事务场景,假设A需要跨行转账给B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的ACID,只能够通过分布式事务来解决。分布式事务就是指事务的发起者、资源及资源管理器和事务协...

  • 5

    什么是分布式事务?银行跨行转账业务是一个典型分布式事务场景,假设 A 需要跨行转账给 B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的 ACID,只能够通过分布式事务来解决。 分布式事务就是指事务的发起者、资源及资源管理器和事...

  • 4

    什么是 TCC,TCC 是 Try 、Confirm 、Cancel 三个词语的缩写,最早是由 Pat Helland 于 2007 年发表的一篇名为《 Life beyond Distributed Transactions:an Apostate’s Opinion 》的论文提出。 TCC 组成 TCC 分为 3 个阶段 Try 阶...

  • 4

    .Net Core with 微服务上一次我们讲解了分布式事务的 2PC、3PC 。那么这次我们来理一下 TCC 事务。本次还是讲解 TCC 的原理跟 .NET 其实没有关系。 Try 准备...

  • 5

    一、TCC 简介 在两阶段提交协议(2PC,Two Phase Commitment Protocol)中,资源管理器(RM, resource manager)需要提供“准备”、“提交”和“回滚” 3 个操作;而事务管理器(TM, transaction manager)分 2 阶段协调所有资源管理器,在第一阶段询问所有...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK