分布式事务方案XA/JTA/2PC/3PC/TCC/SAGA
1. 基础概念
CAP理论
- 一致性(Consistency) :在分布式系统中所有的数据备份,在同一时刻都保持一致状态,如无法保证状态一致,直接返回错误;
- 可用性(Availability):在集群中一部分节点故障,也能保证客户端访问系统并得到正确响应,允许一定时间内数据状态不一致;
- 分区容错性(Partition tolerance):分布式系统在遇到任何网络分区故障时,仍然能保证对外提供满足一致性和可用性的服务,除非整个网络环境都发生故障;
本地事务四大特性(ACID)
- 原子性(atomicity):一个事务中的所有操作,不可分割,要么全部成功,要么全部失败;
- 一致性(consistency):一个事务执行前与执行后数据的完整性必须保持一致;
- 隔离性(isolation):一个事务的执行,不能被其他事务干扰,多并发时事务之间要相互隔离;
- 持久性(durability):一个事务一旦被提交,它对数据库中数据的改变是永久性的。
BASE理论
基本可用(Basically Available):分布式系统在出现故障时,保证核心可用,允许损失部分可用性。(
响应时间上的损失、功能上的损失
)
软状态(Soft State):系统中的数据允许存在中间状态,中间状态不影响系统的整体可用性。(支付中
、处理中
等)
最终一致性(Eventually Consistent):系统中的数据不可一直处于软状态,必须在有时间期限,在期限过后应当保证数据的一致性。(支付中
变为支付成功
)
- 相比于本地事务的ADIC强一致性模型,BASE理论提出通过牺牲一定的强一致性来获得可用性;
- 不同业务单元和业务组件对数据一致性的要求不一样,因此分布式系统中BASE理论和ACID特性会结合使用。
幂等性设计
幂等(Idempotent)
是一个数学与计算机学中的概念。f(n) = 1^n // 无论n等于多少,f(n)永远值等于1
;- 在程序中,使用相同参数执行同一个方法,每一次执行结果都是相同的,即具有
幂等性
;- 以订单状态处理为例的幂等性设计(简单示例),不论执行多少次
orderProcess()
方法,都只会扣减一次库存,并且返回true
。/** 订单处理 */ public boolean orderProcess(OrderEntity orderEntity){ //查询原订单信息 OrderEntity oldOrderEntity = getOrderInfo(orderEntity) //判断订单状态,如订单状态为 ok 则直接返回 true if("ok".equals(oldOrderEntity.getOrderState())){ return true; } //更新订单状态 boolean upBoolean = updateOrderInfo(orderEntity); if(!upBoolean){ return false; } if("ok".equals(orderEntity.getOrderState())){ //扣减库存 boolean reduceBoolean = reduceInventories(orderEntity); if(!reduceBoolean){ return false; } } return true; }
2. 事务的演进过程
单体架构
对于一些小型项目,使用单体架构可以快速的开发,并且很容易控制我们的业务逻辑和事务处理。通过本地事务的ACID特性,保证我们数据的一致性。
单体架构
分库
实际项目中,我们可能会遇到一种情况就是,并发量不大,但常年累积下来数据量很大,这时候我们考虑到了分库策略。由于垮库情况下本地事务已经无法保证多库之间的数据一致性,这时我们就需要考虑分布式事务了。
分库
分布式架构
也有可能所使用的数据库性能比较好,但我们的单应用的性能无法满足业务需求,这时候我们可以选择对服务进行拆分的策略。此时虽然还是使用同一数据库,但我们多个服务之间互相调用来完成原有单体架构下的业务逻辑,这种情况下原有的本地事务也无法保证数据的一致性,这时我们也需要考虑分布式事务。
服务拆分
微服务架构
服务的拆分可以提高应用性能,让应用更专注于处理自己所负责的事情。数据库的拆分,一定程度上提升IO性能、数据库连接数、单机硬件资源的瓶颈。这时,服务间互相调用,每个服务都存在一个自己特定的业务数据库,所以我们需要考虑分布式事务。
服务拆分、数据库拆分
2. 分布式事务
XA规范(协议)
X/Open组织(现在的Open Group)定义了一套DTP(Distributed Transaction Processing)分布式事务处理模型,主要包含以下四部分:
- AP(应用程序)
- TM(事务管理器):交易中间件
- RM(资源管理器):数据库
- CRM(通信资源管理器):消息中间件
**XA规范**
则是DTP模型定义TM和RM之间通讯的接口规范。XA接口函数由数据库厂商提供。TM用它来通知数据库事务的开始、结束、提交、回滚。基于XA规范
衍生出下面的二阶段提交(2PC)、三阶段提交(3PC)。
XA规范
包括两套函数,以xa_
开头的及以ax_
开头的。
以下的函数使事务管理器可以对资源管理器进行的操作:
- xa_open,xa_close:建立和关闭与资源管理器的连接。
- xa_start,xa_end:开始和结束一个本地事务。
- xa_prepare,xa_commit,xa_rollback:预提交、提交、回滚一个本地事务。
- xa_recover:回滚一个已进行预提交的事务。
- ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
- ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。
XA的一些问题:
- 性能(阻塞、响应时间增加、死锁);
- 依赖于独立的J2EE中间件,
Weblogic
、Jboss
,后期轻量级的Atomikos
、Narayana
、Bitronix
;- 不是所有资源(RM)都支持XA协议;
JTA(Java Transaction API)
即Java的事务API,基于XA实现,也就是RM需要支持XA,所以也有JTA(XA)
的说法,JTA仅定义了接口。主要包括javax.sql.XADataResource
、javax.sql.XAConnection
、javax.sql.XAException
、javax.transaction.xa.XAResource
、javax.transaction.Xid
。
目下JTA的实现有几种形式:
- J2EE容器提供的JTA实现(Weblogic、JBoss );
- JOTM(Java Open Transaction Manager)、Atomikos,可独立于J2EE容器的环境下实现JTA;
二阶段提交(2PC)
2PC就是分布式事务中将事务分为两步进行提交。基于数据库的XA协议完成事务本质上就是二阶段提交
(XA、JTA/JTS)。
- 协调者(Coordinater):事务管理器(TM)
- 参与者(participants):资源管理器(RM)
- 准备阶段:
协调者
向参与者
发送prepare
信息,以询问参与者是否能够提交事务;
参与者
在收到prepare
信息后,进行本地事务的预处理,但不提交。并根据处理结果返回,失败not commit
or 成功ready
;- 提交阶段:
如协调者
收到参与者
的失败消息,则向每个参与者发送rollback
消息进行回滚;
所有参与者
都返回ready
,则向每个参与者发送提交commit
消息,通知参与者进行事务提交;
二阶段提交的一些问题:
- 同步阻塞,事务执行过程中所有参与者都是阻塞型的,第三方参与者访问参与者占有的资源时会被阻塞;
- 单点故障,协调者一旦发生故障,参与者会被阻塞。尤其在提交阶段,所有参与者都处于锁定资源状态中,无法完成事务操作;(可以选择新的协调者,但无法解决参与者被阻塞的问题);
- 数据不一致,提交阶段协调者向参与者发送
commit
信息,发生局部网络故障,会导致存在参与者未收到commit
信息无法提交事务情况,导致出现数据不一致现象;
三阶段提交(3PC)
相比于2PC,3PC把2PC的准备阶段再次进行拆分,并且3PC引入了参与者超时机制。
- canCommit:
协调者
询问参与者
,是否具备执行事务的条件,参与者进行自身事务必要条件的检查;- preCommit:
协调者
通知参与者
进行事务的预提交;- doCommit:
协调者
根据preCommit
阶段参与者
的反馈结果通知参与者是否进行事务提交或是进行事务回滚;
事务补偿方案TCC
TCC的核心思想就是校验、资源锁定、补偿,对每个操作(Try)
都提供确认(Confirm)
和取消(cancel)
的操作,这样根据操作的结果,来确认是进行Confirm还是Cancel。
可以看出XA的两阶段提交是基于资源层面
的,而TCC也是一种两阶段提交,但它是基于应用层面
的。
- Try:主要负责对业务进行数据检查和资源预留,例如:对资金进行
冻结
;对状态更改为处理中
;- Confirm:确认执行业务的操作,例如:进行实际
资金扣除
;更改状态为最终结果
;- Cancel:取消执行业务的操作,例如:
解冻资金
;更改状态为未处理
;
TCC存在的一些问题:
- 业务操作的是不同服务的Try来进行资源预留,每个Try都是独立完成本地事务,因此不会对资源一直加锁。
- 业务服务需要提供try、confirm、cancel,业务侵入性强,如不适用三方框架要做到对各阶段状态的感知,比较麻烦。
- 常用TCC框架:
tcc-transaction
、ByteTCC
、spring-cloud-rest-tcc
、Himly
- Confirm/Cancel要做幂等性设计。
常见的微服务系统大部分接口调用是同步的,这时候使用TCC来保证一致性是比较合适的。
Saga(补偿)
Saga的核心是补偿,与TCC不同的是Saga不需要Try,而是直接进行confirm
、cancel
操作。
- Confirm:依次按顺序依次执行资源操作,各个资源直接处理本地事务,如无问题,二阶段什么都不用做;
- Cancel:异常情况下需要调用的补偿事务(逆操作)来保证数据的一致性。
可以看出,Saga和TCC有些类似,都是补偿型事务
优势:
- 一阶段提交本地事务,无锁,高性能;
- 事件驱动模式,参与者可异步执行,高吞吐;
- 应用成本低,补偿服务易于实现;
劣势:
- 无法保证隔离性(脏写)
可靠消息最终一致性(RocketMQ)
有一些情况,服务间调用时异步的,服务A将消息发送到MQ,服务B进行消息的消费。这时我们就需要用到可靠消息最终一致性
来解决分布式事务问题。首先字面理解,
- 可靠消息:即这个消息一定是可靠的,并且最终一定需要被消费的。
- 最终一致性:过程中数据存在一定时间内的不一致,但超过限定时间后,需要最终会保持一致。
确保以上两点情况下,通过消息中间件(RocketMQ)来完成分布式事务处理,因为RocketMQ支持事务消息,可以方便的让我们进行分布式事务控制。
因此首先需要了解一下,RocketMQ的事务消息的原理。
摘自网络
half message:半消息,此时消息不能被consumer所发现和消费,需
producer
进行二次消息确认。
producer
发送half message
给MQ Server
;
producer
根据MQ Server
应答结果判断half message
是否发送成功;
producer
处理本地事务;
producer
发送最终确认消息commit / rollback
;
commit
:consumer
对消息可见并进行消费;
rollback
:discard
抛弃消息,consumer
无法进行消息消费;
- 如遇异常情况下
step4
最终确认消息为达到MQ Server
,MQ Server
会定期查询当前处于半消息状态下的消息,主动进行消息回查来询问producer
该消息的最终状态;
producer
检查本地事务执行的最终结果;
producer
根据检查到的结果,再次提交确认消息,MQ Server
仍然按照step4
进行后续操作。
事务消息发送对应步骤1、2、3、4,事务消息回查对应步骤5、6、7。
由以上步骤可以看出通过事务性消息的两步操作,避免了消息直接投递所产生一些问题。最终投递到MQ Server的消息,是真实可靠且必须被消费的。
Seata
阿里开源的Seata 是一款分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 事务模式。
Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚(支持多种注册中心形式以及本地文件形式);
- 通过全局锁实现了写隔离与读隔离。
结尾
在实际业务开发中,考虑设计一套好分布式事务框架,需要根据具体业务情况结合上述一些理论,进行权衡取舍。
需要考虑的特性
- 实现复杂度:事务模式与当前业务结合,实施成本是否过高;
- 业务侵入性:基于注解、XML、补偿逻辑;
- TC/TM部署:独立部署、与应用部署;
- 性能:回滚概率、回滚所付出的代价、响应时间、吞吐量;
- 高可用:数据库、注册中心、配置中心
- 持久化:文件、数据库;
- 同步/异步:分布式事务执行过程中是否阻塞,还是非阻塞;