银行跨行转账业务是一个典型的分布式事务场景。假设A需要跨行转账给B,那么涉及到两家银行的数据,通过一个数据库的本地事务无法保证转账的ACID。只能通过分布式事务来解决。分布式事务Distributedtransactions在分布式环境中,为了满足可用性、性能和降级服务的需求,降低一致性和隔离性的要求,一方面遵循BASE理论:基本可用性(BasicAvailability)和软态(Softstate)。)最终一致性另一方面,分布式事务也部分遵循ACID规范:原子性:严格遵循一致性:事务完成后严格遵循一致性;事务中的一致性可以适当放宽Isolation:ParallelInter-transactions不受影响;事务中间结果的可见性允许安全放松持久化:严格遵守SAGASAga是本数据库论文SAGAS中提到的一种分布式事务方案。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调。如果每个本地事务都成功完成,则将正常完成。如果某个步骤失败,将以相反的顺序调用一次补偿操作。SAGA目前可用的开源框架主要是Java语言,其中以seata为代表。我们的例子使用的是go语言,使用的分布式事务框架是https://github.com/yedf/dtm,非常优雅的支持分布式事务。下面详细解释一下SAGA的组成:在DTM事务框架中,有3个角色,和经典的XA分布式事务一样:AP/应用程序,发起全局事务,定义全局事务中包含哪些事务分支RM/resourcemanager,负责管理分支事务TM的各种资源/transactionmanager负责协调全局事务的正确执行,包括SAGA正向/反向操作的执行。下面看一张成功完成的SAGA时序图,轻松理解SAGA分布式事务:SAGA实践以我们要进行的银行转账为例,我们会在正向操作中进行转入和转出,在正向操作中进行相反的调整补偿操作。首先我们创建账户余额表:CREATETABLEdtm_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());我们先写调整用户账户余额的核心业务代码defsaga_adjust_balance(cursor,uid,amount):affected=utils.sqlexec(cursor,"updatedtm_busi.user_accountsetbalance=balance+%dwhereuser_id=%dandbalance>=-%d"%(amount,uid,amount))ifaffected==0:raiseException("updateerror,balancenotenough")下面写一个具体的前向操作/补偿操作处理函数@app.post("/api/TransOutSaga")deftrans_out_saga():saga_adjust_balance(c,out_uid,-30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransOutCompensate")deftrans_out_compensate():saga_adjust_balance(c,out_uid,30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransInSaga")deftrans_in_saga():saga_adjust_balance(c,in_uid,30)return{"dtm_result":"SUCCESS"}@app.post("/api/TransInCompensate")deftrans_in_compensate():saga_adjust_balance(c,in_uid,-30)return{"dtm_result":"SUCCESS"}至此各个子交易的处理函数都OK了,接下来启动SAGAtransactionandbranchCall#这是dtm服务地址dtm="http://localhost:8080/api/dtmsvr"#这是业务微服务地址svc="http://localhost:5000/api"req={"数量":30}s=saga.Saga(dtm,utils.gen_gid(dtm))s.add(req,svc+"/TransOutSaga",svc+"/TransOutCompensate")s.add(req,svc+"/TransInSaga",svc+"/TransInCompensate")s.submit()至此,一个完整的SAGA分布式事务就写完了,如果想运行成功的例子,那就参考这个例子yedf/dtmcli-py-sample,它非常容易运行简单#部署并启动dtm#需要docker版本18或以上gitclonehttps://github.com/yedf/dtmcddtmdocker-composeup#创建另一个命令行gitclonehttps://github.com/yedf/dtmcli-py-samplecddtmcli-py-samplepip3installflaskdtmclirequestsflaskrun#再启动一个命令行curllocalhost:5000/api/fireSaga处理网络异常假设在提交给dtm的事务中,调用转账操作时出现短时失败怎么办?根据SAGA事务协议,dtm将重试未完成的操作。我们应该如何处理?故障可能是转入操作完成后出现网络故障,也可能是转入操作过程中机器停机。如何处理才能保证账户余额的调整正确无误?正确处理这类网络异常是分布式事务中的一大难题。异常分为三种:重复请求、空补、暂停,都需要正确处理。DTM提供了子事务屏障功能,保证上述异常情况下的业务在逻辑上只会按照正确的顺序提交一次成功。(关于子事务屏障的详细介绍,可以参考分布式事务最经典的七大解决方案的子事务屏障链接)我们调整处理函数为:@app.post("/api/TransOutSaga")deftrans_out_saga():withbarrier.AutoCursor(conn_new())作为游标:defbusi_callback(c):saga_adjust_balance(c,out_uid,-30)barrier_from_req(request).call(cursor,busi_callback)return{"dtm_result":"SUCCESS"}其中barrier_from_req(request).call(cursor,busi_callback)调用会使用子事务屏障技术保证busi_callback回调函数只提交一次可以尝试多次调用这个TransIn服务,而且只有一个平衡调整。处理回滚如果银行在给用户2转账的时候发现用户2的账户异常,退回失败怎么办?我们调整处理函数,让转入操作返回失败@app.post("/api/TransInSaga")deftrans_in_saga():return{"dtm_result":"FAILURE"}我们给出一个交易失败交互的时序图这里有一点,TransIn的正向操作没有做任何事情,返回失败。这时候调用TransIn的补偿操作会导致反向调整出错?不用担心,之前的子事务屏障技术可以保证,如果在提交前发生TransIn错误,补偿会是一个no-op;如果提交后出现TransIn错误,补偿操作会提交一次数据。您可以将返回错误的TransIn更改为:@app.post("/api/TransInSaga")deftrans_in_saga():withbarrier.AutoCursor(conn_new())ascursor:defbusi_callback(c):saga_adjust_balance(c,in_uid,30)barrier_from_req(request).call(cursor,busi_callback)return{"dtm_result":"FAILURE"}最后结果的余额还是会正确的,原理可以参考:最经典的七小节分布式事务的解决方案TransactionBarrier总结在这篇文章中,我们介绍了SAGA的理论知识,并通过一个例子,给出了一个完整的编写SAGA事务的过程,涵盖了正常成功完成、异常情况和回滚成功的情况。相信读者通过这篇文章已经对SAGA有了更深入的了解。本文使用的dtm是一个全新开源的Golang分布式事务管理框架,功能强大,支持TCC、SAGA、XA、事务消息等事务模式,支持Go、python、PHP、node等语言,和csharp。同时,它提供了一个非常简单易用的界面。看完这篇干货,欢迎大家访问项目https://github.com/yedf/dtm,给个star支持!
