什么是分布式事务?银行跨行转账业务是典型的分布式事务场景。假设A需要跨行给B转账,涉及到两家银行的数据。转账的ACID不能通过数据库的本地事务来保证,只能通过分布式事务来解决。分布式事务是指事务发起者、资源和资源管理者、事务协调者分别位于分布式系统的不同节点上。在上述转账业务中,用户A-100的操作和用户B+100的操作不在同一个节点上。本质上,分布式事务是为了保证分布式场景下数据操作的正确执行。什么是TCC分布式事务,TCC是Try、Confirm、Cancel这三个词的缩写,最早是由PatHelland在2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出的。TCC的组成TCC分为3个阶段Try阶段:try执行,完成所有业务检查(一致性),预留必要的业务资源(准隔离)Confirm阶段:如果Try的所有分支都成功,则进入Confirm阶段。Confirm实际执行业务,不做任何业务检查,只使用Try阶段预留的业务资源。Cancel阶段:如果所有分支中有一个Try失败,则进入Cancel阶段。Cancel释放Try阶段预留的业务资源。在TCC分布式事务中,有3个角色,与经典的XA分布式事务相同:AP/应用程序,发起全局事务,定义全局事务中包含哪些事务分支RM/资源管理器,负责管理分支事务的各种资源TM/transactionmanager负责协调全局事务的正确执行,包括Confirm和Cancel的执行,以及处理网络异常。如果我们要进行类似银行跨行转账的业务,分别转出(TransOut)和转入(TransIn),在不同的微服务中,成功完成TCC交易的典型时序图如下:跨行转账操作,最简单的方法是在Try阶段调整余额,在Cancel阶段反转余额调整,确认阶段然后no-op。这样造成的问题是,如果A扣钱成功,但是转账给B失败,就会回滚,A的余额会调整到初始值。在这个过程中,如果A发现自己的余额被扣了,而收款人B却迟迟没有收到余额,就会给A带来麻烦。更好的做法是在Try阶段冻结A转账的金额,确认实际扣款,Cancel解冻资金,让用户在任何阶段看到的数据都清晰明了。接下来,我们将对TCC事务进行具体的开发。TCC目前可用的开源框架主要是Java语言,其中seata是代表。我们的例子使用的是Python语言,使用的分布式事务框架是https://github.com/yedf/dtm,非常优雅的支持分布式事务。下面详细解释一下TCC的组成。我们先创建两张表,一张是用户余额表,一张是冻结资金表。建表语句如下:CREATETABEdtm_busi.`user_account`(`id`int(11)AUTO_INCREMENTPRIMARYKEY,`user_id`int(11)notNULLUNIQUE,`balance`decimal(10,2)NOTNULLDEFAULT'0.00',`create_time`datetimeDEFAULTnow(),`update_time`datetimeDEFAULTnow());CREATETABEdtm_busi.`user_account_trading`(`id`int(11)AUTO_INCREMENTPRIMARYKEY,`user_id`int(11)notNULLUNIQUE,`trading_balance`decimal(10,2)NOTNULLDEFAULT'0.00',`create_time`datetimeDEFAULTnow(),`update_time`datetimeDEFAULTnow());在交易表中,trading_balance记录了正在交易的金额。我们先写核心代码,冻结/解冻资金操作,检查约束balance+trading_balance>=0,如果约束不成立则执行失败trading_balance+%dwhereuser_id=%dandtrading_balance+%d+(selectbalancefromdtm_busi.user_accountwhereid=%d)>=0"%(amount,uid,amount,uid))ifaffected==0:raiseException("updateerror,maybebalancenotenough")然后调整余额deftcc_adjust_balance(cursor,uid,amount):utils.sqlexec(cursor,"updatedtm_busi.user_account_tradingsettrading_balance=trading_balance+%dwhereuser_id=%d"%(-amount,uid))utils.sqlexec(cursor,"updatedtm_busi.user_accountsetbalance=balance+%dwhereuser_id=%d"%(amount,uid))下面写具体的Try/确认/取消处理函数@app.post("/api/TransOutTry")deftrans_out_try():#交易和异常处理tcc_adjust_trading(c,out_uid,-30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransOutConfirm")deftrans_out_confirm():#交易和异常处理tcc_adjust_balance(c,out_uid,-30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransOutCancel")deftrans_out_cancel():#交易和异常处理tcc_adjust_trading(c,out_uid,30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransInTry")deftrans_in_try():#交易和异常处理tcc_adjust_trading(c,in_uid,30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransInConfirm")deftrans_in_confirm():#transaction和异常处理tcc_adjust_balance(c,in_uid,30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransInCancel")deftrans_in_cancel():#交易及异常处理tcc_adjust_trading(c,in_uid,-30)return{"dtm_result":"SUCCESS"}至此,各个子交易的处理函数就OK了,接下来启动TCC事务,进行分支调用@app.get("/api/fireTcc")deffire_tcc():#Initiateatcctransactiongid=tcc.tcc_global_transaction(dtm,utils.gen_gid(dtm),tcc_trans)return{"gid":gid}#具体过程tcc交易的ssingdeftcc_trans(t):req={"amount":30}#业务请求加载#调用转账服务的Try|Confirm|Cancelt.call_branch(req,svc+"/TransOutTry",svc+"/TransOutConfirm",svc+"/TransOutCancel")#调用Try|Confirm|Cancelt.call_branch(req,svc+"/TransInTry",svc+"/TransInConfirm",svc+"/TransInCancel")至此,一个完整的TCC分布式事务就写完了。如果您想运行一个成功的示例,请按照dtmcli-py-sample项目的说明进行操作。如果银行在给用户2转账的时候发现用户2的账户异常,退回失败怎么办?我们修改代码来模拟这种情况:@app.post("/api/TransInTry")deftrans_in_try():#Transactionandexceptionhandlingtcc_adjust_trading(c,in_uid,30)return{"dtm_result":"FAILURE"}这是事务失败交互的时序图与成功的TCC不同的是,当一个子事务失败时,随后回滚全局事务,调用每个子事务的Cancel操作,保证全局事务全部回滚。TCC网络异常TCC在整个全局交易过程中可能会出现各种网络异常,典型的有空回滚、幂等、挂起等。由于TCC的异常,类似于SAGA、可靠消息等事务模式,所以我们把所有的异常解决方案都放在了本文最经典的七大分布式事务解决方案的异常处理章节中。在这篇文章中,我们介绍了TCC的理论知识,并通过一个例子,给出了一个完整的TCC事务编写流程,涵盖正常成功完成和成功回滚。相信读者通过本文对TCC有了更深入的了解。
