解决思路
XA方案问题分析:
1. 基于数据库的XA协议本质上就是两阶段提交,但由于性能原因在互联网高并发场景下并不适用
2. 2PC是在正常情况下的方案,回滚只是业务异常回滚。如果某个资源挂了呢?怎么保证交易的原子性A,从而保证数据一致性C呢?
3. 2PC正常情况下,怎么处理分布式事务隔离呢? 最好的方式是 串行,可行吗?
改进方案:
1. TM挨个调用 RM,如果出现异常,就对已经成功提交的进行补偿,比如:前面RM已提交了,后边的RM调用异常,那么就让前面的RM业务回滚。这种朴素的模型就是Saga
2. Saga这种方式并不能保证隔离性,于是出现了TCC
3. 还有像Ebay提出的基于消息表,即可靠消息最终一致模型,但本质上这也属于Saga模式的一种特定实现
思想有两个:
- 基于应用共享事务记录执行轨迹;
- 然后通过异步重试确保交易最终一致(这也使得这种方式不适用那些业务上不允许补偿回滚的场景)。
这类分布式事务场景并不是微服务才出现的,在SOA时代其实就有了,常见的Saga、TCC、可靠消息最终一致等模型也都是很多年前就有了,只是最近几年随着微服务兴起,这些方案又重新被人关注了起来。
TCC
TCC事务补偿是基于2PC实现的业务层事务控制方案,它是Try、Confirm和Cancel三个单词的首字母,含义如下:
1、Try 检查及预留业务资源完成提交事务前的检查,并预留好资源。
2、Confirm 确定执行业务操作
对try阶段预留的资源正式执行。
3、Cancel 取消执行业务操作
对try阶段预留的资源释放。
注意:操作必须支持幂等。原因是调用报错会多次调用。
转账场景举例:
我们需要操作的目标字段,都要添加一个相关的冻结字段,try操作是操作冻结字段,cc操作时,将冻结的数值更新到目标字段。
示例如下:
<!--try逻辑-->
<update id="increaseMoney">
UPDATE company SET frozen = frozen + #{money}
WHERE id = #{id}
</update>
<!--confirm逻辑-->
<update id="confirmIncreaseMoney">
UPDATE company SET money = money + #{money},frozen = frozen - #{money}
WHERE id = #{id}
</update>
<!--cancel逻辑-->
<update id="cancelIncreaseMoney">
UPDATE company SET frozen = frozen - #{money}
WHERE id = #{id}
</update>
Sagas
事务模型(最终一致性)
Saga的核心就是补偿,一阶段就是服务的正常顺序调用(数据库事务正常提交),如果都执行成功,则第二阶段则什么都不做;但如果其中有执行发生异常,则依次调用其补偿服务(一般多逆序调用未已执行服务的反交易)来保证整个交易的一致性。
这种模式,是使用一个反向的业务操作,来撤销之前的业务操作。
其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 理论出自 Hector & Kenneth 1987发表的论文 Sagas。
saga模式的实现,是长事务解决方案。
Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
如图:T1~T3都是正向的业务流程,都对应着一个冲正逆向操作C1~C3
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga 正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
TCC&SAGA
1. 调用机制不同
TCC 是按个 确认金额够不够。然后才是提交。
Saga 是按个 直接扣钱,不够再补偿回来。
2. 异常补偿机制不同
TCC 需要添加一个冬季金额,原金额不变,执行都没问题后在一块提交事务。
Saga 直接把钱扣了,如果没扣成功,在加回来。à 可能有假数据。
基于消息的事务
基于可靠消息最终一致,一阶段服务正常调用,同时同事务记录消息表,二阶段则进行消息的投递,消费。虽然跟TCC和Saga列在一块,都属于补偿事务机制,但对RM的管理机制是不同的。
本地消息
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
保证消息发送成功后,在决定是否提交和回滚本地事务。
TCC设计注意
- ➢业务模型分2阶段设计
- ➢并发控制
- ➢允许空回滚
- ➢防悬挂控制
- ➢幂等控制
业务模型分 2 阶段设计
用户接入 TCC ,最重要的是考虑如何将自己的业务模型拆成两阶段来实现。
以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段 Try 成功的话 二阶段 Confirm 一定能成功。
如上图所示,Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try 方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。
二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。
如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可用。
用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
允许空回滚
Cancel 接口设计时需要允许空回滚。在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回滚。
防悬挂控制
悬挂的意思是:Cancel 比 Try 接口先执行,出现的原因是 Try 由于网络拥堵而超时,事务管理器生成回滚,触发 Cancel 接口,而最终又收到了 Try 接口调用,但是 Cancel 比 Try 先到。按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的 Try 接口不应该执行,否则会产生数据不一致,所以我们在 Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,则不执行 Try 的业务操作。
幂等控制
幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我们可以用事务 xid 或业务主键判重来控制。
Saga使用场景
Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。
事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,可以使用 Saga 模式。
Saga模式的优势是:
一阶段提交本地数据库事务,无锁,高性能;
参与者可以采用事务驱动异步执行,高吞吐;
补偿服务即正向服务的“反向”,易于理解,易于实现;
缺点:Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。
与TCC实践经验相同的是,Saga 模式中,每个事务参与者的冲正、逆向操作,需要支持:
空补偿:逆向操作早于正向操作时;
防悬挂控制:空补偿后要拒绝正向操作
幂等
实现组件
Bytetcc
Bytetcc是由北京新奥集团工程师开发,是一个兼容JTA规范的基于TCC机制的分布式事务管理器。目前开发到了第五版,稳定版本为第四版。
特点:
1、支持Spring容器的声明式事务管理;
2、支持普通事务、TCC事务、saga事务等事务机制;
3、支持多数据源、跨应用、跨服务器等分布式事务场景;
4、支持长事务;
5、支持dubbo服务框架;
6、支持spring cloud;
TCC-transaction
DTX
传统分布式事务需保证ACID属性,强调一致性,要求强一致;而BASE则是与之相对立的理论,认为为了可用性(Availability)而牺牲部分一致性(Consistency)可以显著的提升系统的可伸缩性,这就是异步操作。蚂蚁金服分布式事务产品DTX分别基于两种理论实现了两种模式:基于BASE理论的TCC模式和基于ACID理论的FMT模式。
为了解决 TCC 模式的易用性问题,蚂蚁分布式事务后来又推出了框架托管模式(Framework-managed transactions,简称 FMT)。FMT是一种无侵入的分布式事务解决方案,该模式解决了分布式事务的易用性问题,在该模式下,开发者只需关注一阶段操作,框架会自动解析SQL语义,生成二阶段提交和回滚操作,使分布式事务的接入更便捷,该模式下对业务代码几乎无侵入,框架能够“自动化”地解决分布式架构下的数据一致性问题。
“DTX本身是有嵌套的,如果调了一个服务,可能它下面还调用了其它服务,也是分布式的,从而形成多级复杂嵌套。DTX是一个方法论级的保证,不管分多少级,只要层层提交成功了,最终就都能成功提交。”尹博学介绍。DTX本身带有实时监控,可以监控实时的事务信息,包括事务数、成功率、平均耗时等,也可以与链路监控相结合,根据DTX上报的实时信息,提供历史趋势图、同比/环比分析、报警等功能。
异步模式让DTX具有极强的可扩展性,交易量翻多少倍都可以支持。
蚂蚁金服的分布式事务有两个名字:对内叫XTS,ExtendedTransaction Service可扩展事务服务;对外叫DTX,Distributed Transaction-eXtended分布式事务
“首先我们假想这样一种场景:转账服务,从银行 A 某个账户转 100 元钱到银行 B 的某个账户,银行 A 和银行 B 可以认为是两个单独的系统,也就是两套单独的数据库。
我们将账户系统简化成只有账户和余额 2 个字段,并且为了适应 DTS 的两阶段设计要求,业务上又增加了一个冻结金额(冻结金额是指在一笔转账期间,在一阶段的时候使用该字段临时存储转账金额,该转账额度不能被使用,只有等这笔分布式事务全部提交成功时,才会真正的计入可用余额)。按这样的设计,用户的可用余额等于账户余额减去冻结金额。这点是理解参与者设计的关键,也是 DTS 保证最终一致的业务约束。”
在try阶段并没有对银行A和B数据库中的余额字段做操作,而是对冻结金额做的操作,对应A银行预留资源操作是对冻结金额加上100元,这时候A银行账号上可用钱为余额字段-冻结金额;对应B银行的操作是对冻结金额上减去100,这时候B银行账号上可用的钱为余额字段-冻结金额。
如果事务协调器调用银行A和银行B的try方法有一个失败了(比如银行A的账户余额不够了),则调用cancle进行回滚操作(具体是对冻结金额做反向操作)。如果调用try方法都OK了,则进入confirm阶段,confirm阶段则不做资源检查,直接做业务操作,对应银行A要在账户余额减去100,然后冻金额减去100;对应银行B要对账户余额字段加上100,然后冻结金额加上100。
最关心的,如果confirm阶段如果有一个参与者失败了,该如何处理,其实上面操作都是xts-client做的,还有一个xts-server专门做事务补偿的。
EasyTransaction
EasyTransaction(后简称ET)的目标也是构建出一个全面分布式事务解决方案,到目前为止其包含TCC,自动补偿(Seata AT),手动补偿,可靠事务消息、Saga事务等等多种形态。
Hmily
Hmily是由碧桂园工程师开发,高性能异步分布式事务TCC框架。
Hmily 是一款高性能分布式事务 tcc 开源框架。基于java语言来开发(JDK1.8),支持 Dubbo、Spring Cloud、Motan 等 RPC 框架进行分布式事务。
框架特性
支持嵌套事务(Nested transaction support)
采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别
支持SpringBoot-starter 项目启动,使用简单
RPC框架支持 : dubbo,motan,springcloud
本地事务存储支持 : redis,mongodb,zookeeper,file,mysql。
事务日志序列化支持 :java,hessian,kryo,protostuff
TX-LCN
SpringCloud系列——TX-LCN分布式事务管理 - huanzi-qch - 博客园