网站首页 > 技术文章 正文
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 有了很清晰的理解。
猜你喜欢
- 2025-01-18 云端卫士实战录 React + Redux 前端项目实践
- 2025-01-18 MyBatis 插件原理与实战
- 2025-01-18 字节跳动3-3大牛力荐!RabbitMQ实战指南:消息队列面试必刷手册
- 2025-01-18 阿里大数据专家用实战经验总结的一份Apache Kylin实战(PDF)
- 2025-01-18 Webpack5 入门与实战,前端开发必备技能无密今朝岁起东
- 2025-01-18 Django实战017:django+vue+redis项目
- 2025-01-18 基于Vue和Quasar的前端SPA项目实战 免费开源(一)
- 2025-01-18 成为一名合格的前端架构师,前端知识技能与项目实战教学
- 2025-01-18 腾讯大数据专家首次分享这份Spark实战指南(PDF)
- 2025-01-18 前端必学 40个精选案例实战 一课吃透HTML5+CSS3+JS(超清完结)
你 发表评论:
欢迎- 576℃几个Oracle空值处理函数 oracle处理null值的函数
- 573℃Oracle分析函数之Lag和Lead()使用
- 559℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 557℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 552℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 543℃【数据统计分析】详解Oracle分组函数之CUBE
- 531℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 527℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)