原文链接
如果是小白,可以先看TCC步骤,核心思想,然后使用Seata,阅读Seata官方提供的示例代码,验证自己的猜想,再看遍TCC。
分布式事务是跨过多个数据库或者系统的事务,在电商、金融领域应用十分广泛。TCC方案非常适用于对实时性以及可靠性要求都很高的场景。Seata是阿里开源的分布式事务中间件,包含XA、TCC、SAGA、AT四种模式。
Head first TCC
try {
callA()
} catch (e) {
rollbackA()
exit()
}
try {
callB()
} catch (e) {
rollbackB()
rollbackA()
exit()
}
上面的代码无法保证A、B同时成功,或者失败,没有任何地方能够确信目前执行到哪。具体点,就是执行上面代码的机器发生了宕机,怎么知道代码执行到哪一步骤了,是否执行成功了。因为不能确切知道A、B是否执行成功或者回滚成功,所以无法进行补偿重试或者回滚操作。但是,如果有一个平台能够知道当前系统执行状态,比如能确定当前A执行成功,B执行失败,那么就可以调用A、B的回滚操作。通过事务补偿保证A、B要么同时成功,要么同时失败。
通过补偿实现分布式事务保证了最终一致性,是弱一致性。
事务补偿型的分布式事务解决方案有多种,这里只谈TCC。TCC增加协调者记录目前各个子事务(A、B的执行)的状态,由框架统一管理。
相较于传统事务,TCC还增加了try阶段,提前检查所包含的服务能否执行,如检查用户是否有100元下单支付。分布式事务内所包含的所有服务try成功才可以进行confirm,否则进入cancel阶段。
总结下TCC,核心思想就两条:1.事务补偿 2.增加try阶段,try阶段后文会逐步加深印象。
TCC分三个部分
1.尝试(Try):开始执行分布式事务,并锁定相关资源。
解释下锁定资源。比如银行转账,A向B转钱,service A并不会在这个阶段扣A账户上的钱,只会在A账户上增添一个冻结资产的字段,修改冻结资产。所以使用TCC方案的数据库必须提前设计好,增加锁定资源的字段。Try阶段究竟怎么设计取决于业务。
2. 确认(Confirm):如果在执行分布式事务过程中没有发生错误,则提交事务并释放锁定的资源也就是回滚try阶段的操作。
3. 取消(Cancel):如果在执行分布式事务过程中发生了错误,则回滚事务并释放锁定的资源,即回滚try阶段操作。
根据TCC协议,Confirm和Cancel是只返回成功,不会返回失败。如果service机器宕机或者网络等问题导致confirm失败,那么TCC的框架内部会进行重试,最终成功。也就是confirm阶段必须成功,如果失败就不停重试。从这个角度看,TCC属于补偿性事务框架。如果重试很多次仍然失败,比如用户账号突然被冻结,完全不可操作,公司内部会有报警或者定时任务然后查询相关数据库发现这种问题,由人工处理。
问题:为什么不直接向协调者confirm,对于转账业务也就是直接扣A 10块钱,B到账10块钱。如果confirm失败,协调者再向已经confirm过的service发起回滚操作?协调者知道各个service的调用情况,也能保证它们要么都执行成功或者都执行失败。
解释:有类似做法的分布式事务解决方案,比如SAGA。但是直接confirm,之后再回滚,会对用户产生影响,用户可能看到自己的钱先变多了后来又减少变回来了。因此TCC先去try,提前询问各个service是否有足够的资源提交,如果没有,则不进行confirm,如果所有service try都能执行成功,那至少说明每个service业务上是可以confirm的。因此try阶段怎么设计很重要。
示例
TCC具体实现中,业务开发人员需要做什么。银行转账,A->B 10元,并且A、B两个账户的操作由不同微服务操作。
不包含任何分布式事务框架
而在TCC,数据库增加了字段freezed_amount
伪代码
service A
try:
if amount > 10, then freezed_amount -= 10
else return
confirm:
amount -= 10
cancel:
freezed_amount += 10
service B
try:
freezed_amount += 10
confirm:
amount += 10
cancel:
freezed_amount -= 10
仅为示例,复制Seata官方提供的代码,有很多问题。具体怎么设计try预留资源取决于具体业务。
调用service A、B的服务,分布式事务发起者的伪代码见下文。
TCC接口实现难点
需要先了解seata的流程
http://seata.io/zh-cn/docs/overview/what-is-seata.html
TM:发起分布式事务的service,比如调用A、B的service
RM:执行分支事务的service,实现TCC三个接口的service,比如A、B
TC:seata服务,也就是TCC协调者
TM伪代码:
A.prepare()
do something
B.prepare()
do something
TM发起了分布式事务,调用了RM的prepare(try)接口,这样就向TC注册了分支事务。当所有prepare都执行返回成功后,TC调用RM的commit。
RM的接口需要注意:
幂等:TCC协调者可能会重试confirm操作,因此confirm需要幂等
空回滚:分布式事务包含两个分支事务A、B,A.try成功,B.try一直未返回给TC,因此TC发起cancel,向A、B发起cancel指令。A正常cancel,而B之前可能try并未成功,此时cancel B就是空回滚。因此cancel前需要判断RM是否执行了try。
事务悬挂:还是上面的例子。分布式事务已经回滚了B,对TC而言整个分布式事务已经结束。但是这时之前阻塞的TM已经恢复,然后继续执行B的try函数。由于分布式事务已经结束,B的try也就不会被回滚。因此try前需要判断是否执行了cancel。
目前seata已经在框架内部解决了空回滚和事务悬挂问题,查看下使用方法即可。
Seata使用
请参考官方文档。简单来说首先得启动TCC,部署TCC框架。然后调用的TCC客户端分为分布式事务的发起者与被调用方。
不爱看文档的话,喜欢先看代码的话,官方文档提供的可以避免踩坑的信息有,http://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html中的“步骤四:初始化GlobalTransactionScanner”
如果使用本地测试,不想使用Dubbo等,一定要在包含TCC三个接口的interface类上添加@LocalTCC注解,否则会导致businessActionContext为null, @LocalTCC相关坑请百度搜索更多信息。
这里提供一个可以本地测试代码,修改了TCC官方sample(https://github.com/seata/seata-samples/tree/master/tcc/transfer-tcc-sample/src/main/java/io/seata/samples/tcc/transfer)
没有修改官方sample核心代码,只是从Dubbo改成了本地测试local调用,客户端数据库改为了MySQL,需要安装MySQL 8版本
1. https://github.com/ShengyuFang/seata
2. https://github.com/ShengyuFang/seata-local-tcc-sample
使用方法:
1.为TCC服务端,先执行里面包含的SQL脚本,生成相关table,运行ServerApplication.main()
2.先运行TransferProviderStarter.main()创建table,然后运行TransferApplication.main()
重要的事情说三遍! 生产环境下千万不要直接使用sample代码!生产环境下千万不要直接使用sample代码!生产环境下千万不要直接使用sample代码!因为代码有bug,线程不安全, 数据库先读内存再更新有“超卖问题”,用Mysql update ... where ...很容易解决。“超卖”属于其他问题,超出本文范围。