前言:
上一篇《数据库事务,spring事务 》我们讲到从数据库事务到spring事务的演变以及原理和使用,spring事务已经能够满足单连接情况下的事务要求,但是在多线程场景(不同线程使用不同的数据库连接),或者微服务场景(多个服务各自使用不同的数据库)时显得力不从心,这时候我们需要引入另外一个东西来处理这些场景-----分布式事务。
以下标准解决方案基础知识参考了两篇不错的帖子,感兴趣的朋友可以查看原贴
术语说明
在讲解每一种思路前我认为有必要给大家提一嘴常用的分布式事务中的常用角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
总结:简单来理解 ,例如在项目中使用seata来管理事务,那么Seata服务就是TC,加了事务注解的那个方法就是TM,TM内部调用的各个独立的需要保证事务的方法就是RM。
常见的分布式事务解决思路
- 2PC(两阶段提交)
- 3PC(三阶段提交)
- TCC
- 可靠消息队列
- SAGA事务
- AT事务
2PC(两阶段提交)
2PC(Two-phase commit protocol)两阶段提交就是将事务提交拆成了准备阶段
和提交阶段
这两个阶段,为了管理者两个阶段,我们引入一个新的第三方TC (Transaction Coordinator) - 事务协调者(反正所谓架构不就是再抽一层嘛,哈哈),协调者
就相当于一个总的事务管理器,它会根据事务的执行状态来判断事务最终是提交
还是回滚
。
准备阶段:协调者(TC)会给各参与者(RM)发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了,同时协调者会不断询问事务的所有参与者是否执行完sql(注意:这个时候没有真正提交事务),如果已准备好提交回复Prepared
,否则回复Non-Prepared
,这个阶段各参与者RM仍然会持有各自的数据库锁。
提交阶段,如果全部的事务参与者都返回给协调者的是Prepared,则协调者向所有的参与者发送Commit指令,所有事务参与者立即执行提交操作。 如果任意一个参与者回复了Non-Prepared消息,或任意一个参与者超时未回复,协调者向所有参与者发送 Abort 指令,参与者立即执行回滚操作。
两阶段提交存在以下缺点:
- 单点故障问题:若协调者(TC)在发送准备命令前宕机,那么相当于事务没有开启(可以接受),若TC在发送准备命令后宕机或者在第二阶段发送回滚或提交命令前,那么问题就来了,RM会一直持有数据库锁(不能接受)--可能有同学会想为什么我们不引入一个超时机制呢?这个接下来会讲,若在第二阶段发送提交或者回滚命令之后挂掉(勉强可以接受),一般来说只要不出现网络故障,锁会得到释放。
- 性能问题:要等待所有的参与者处理完毕为止,性能较差。
- 一致性问题:协调者如果因为网络问题或者宕机了,向一部分参与者发送了commit,另一部分还没来得及发就宕机了,就会造成数据不一致。
3PC(三阶段提交)
3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
3PC 包含了三个阶段,分别是准备阶段(CanCommit)、预提交阶段(PreCommit)和提交阶段(DoCommit)
准备阶段:准备阶段的变更不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。作用就是先确保所有的参与者(RM)都可以执行再上锁。避免某个别参与者不能执行成功导致其他所有参与者都上锁。
预提交阶段与提交阶段与2PC对应的两阶段是一致的。
引入超时机制:避免了TC宕机,RM一直持有锁不放的问题。
总结:
性能上,如果在事务能够正常提交的场景中,因为3PC多了一个询问阶段,所以性能反而会更差。如果事务需要回滚的场景中,三阶段的性能通常要好一些。
一致性问题并没有得到解决,在事务需要回滚的场景下,如果因为网络问题参与者一直没有收到协调者的的Abort回滚指令,这些参与者将会错误的提交事务,仍有数据不一致的问题。
TCC(Try -Confirm -Cancel )
通过前面的讲解可知,2PC和3PC底层还是得依赖RM的数据库事务,如果部分RM不支持数据库事务(没有事务,提交阶段提交个狗屁)那岂不是凉凉?此时TCC上场了。
TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法如下。
Try : 尝试执行阶段,并预留好事务需要用到的所有资源(比如冻结可用库存)。
Confirm : 确认执行阶段,无需进行业务检查,直接使用Try阶段准备的资源。该阶段可能会重复执行,因此需要满足幂等性。
Cancel : 取消执行阶段,释放Try阶段预留的业务资源。该阶段也可能会重复执行,因此也需要满足幂等性。
如果将2PC的提交阶段的提交和回滚拆开,你会发现和TCC非常类似。但是TCC是位于用户代码层面,而不是基础设施层面,我们就需要投入更高的开发成本。
TCC需要开发者在每个参与者(RM)中自己定义try,confirm,Cancel三个动作,具体调用流程如下如图
优点:
隔离性强:在业务执行的时候,只操作预留资源,几乎不会涉及到资源的争用,所以性能较高
缺点:
业务侵入性高,开发成本高,技术不可控。如果你对接的有第三方服务(比如银行),别人不愿意按照TCC的方式来修改代码,那就推动不下去了。
针对第二种方式,我们可以考虑采用下面的柔性事务方案:SAGA事务。
SAGA事务
SAGA 事务把大事务拆分成若干个子事务,每个子事务都可以被看作原子行为。我们需要对每个子事务设计对应的补偿动作。如果各个子事务都能提交成功,那么事务就可以顺利完成,否则我们就要采取以下两种恢复策略之一:
正向恢复:如果某个子事务提交失败,则一直对该事务进行重试,直至成功为止,这种适用于事务最终都要成功的场景。
反向恢复:如果某个子事务提交失败,则对该子事务及其之前所有的子事务进行补偿操作。
与TCC相比,SAGA不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往要比冻结操作容易实现的多。
缺点:
SAGA必须保证所有子事务都能提交或者补偿,但是SAGA系统本身也可能会崩溃,所以它必须设计成与数据库类似的日志机制。
需要保证正向、反向恢复过程能严谨执行。
SAGA是基于数据补偿来代替回滚的思路,也可以应用在其他事务方案上。比如阿里的 Seata 框架的 AT事务模式就是这样的一种应用。
AT事务
AT事务也是参照两阶段提交协议来实现的,针对两阶段提交涉及到的资源必须都等到最慢的事务完成后才能统一释放的问题,AT事务也设计了对应的解决方案。
它在业务数据提交时,自动拦截所有的SQL,分别保存SQL对数据修改前后的快照,这就相当于记录了重做和回滚日志
如果分布式事务成功了,那我们后续只需清理每个数据源中对应的日志数据即可。如果分布式事务需要回滚,就要根据日志数据自动产生用于补偿的逆向SQL。
基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放资源,相比两阶段提交,极大的提升了系统的吞吐量。但是它和我们使用的事务消息一样,牺牲了隔离性。
消息事务
RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
可以看到消息事务实现的也是最终一致性。
最大努力通知
其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。
上面讲了各种思路,具体采用哪总方案要根据实际的业务场景来决定。下面我们就一起来操练一下seata的使用吧。
分布式事务框架Seata
seata官方文档已经写的很详细,具体了解可以直接查看如下链接
seata中文官方文档http://seata.io/zh-cn/docs/overview/what-is-seata.html
AT模式
这我复述一下AT模式运行原理:
假设有A,B两个事务,我么先考虑两个事务修改的是同一张表同一个字段这种极端情况
A事务执操作字段 m = 1000 - 100 = 900,B事务操作字段 m = 900 - 100 = 800;
第一阶段:A事务先获取本地锁(数据库锁),获取后执行操作,将m改成900,然后获取全局锁(seata提供的锁),都成功后执行本地commit,并释放本地锁(注意此时没有释放全局锁)——此时数据库中m的状态是实实在在的900(给自己留个疑问-如果这时候被当前全局事务范围外其他线程获取到本地锁并进行修改会怎样呢?),然后执行B事务逻辑,因为A事务已经释放本地锁,因此B事务可以顺利拿到本地锁正常执行,B事务将m变成800 此时尝试获取全局锁,若A事务外层的业务逻辑还未执行完毕,没有释放全局锁,此时B事务等待并重试获取全局锁。
第二阶段: A事务释放全局锁前,存在两种情况。
第一种A事务执行成功了,释放全局锁B事务获取到全局锁,提交本地事务,释放本地锁,释放全局锁。整个事务完成。
第二种A事务执行失败了,不释放全局锁,且尝试获取本地锁。但此时本地锁一直被B事务持有 ,且B事务在等待获取全局锁。双方会僵持在这种状态,直到B事务发现获取全局锁等待超时,此时B事务会回滚本地事务,并释放本地锁,这是A事务就会顺利获取到本地锁,通过undo—log表中的记录回滚。回滚后释放全局锁。
读取隔离级别:
通过以上流程可以发现,在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,在整个过程中AT模式对于A事务对应的数据的读取隔离级别为读未提交(全局事务还未结束,但是A事务已释放本地锁,其他线程可读),对于B事务对应的数据而言是读已提交(全局事务未结束时,不可读)。
当然如果你的业务一定要实现读已提交,seata也提供了对应的处理方法,在查询的时候使用select for update ,SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
写入隔离级别:
因为全局锁一直被A事务持有,因此后面的事务都不能提交本地事务,因此不存在脏写问题。(此处留一个疑问,如果是被当前全局事务范围外的线程修改会则导致脏写吗?)
考察事务,隔离级别一定是我们最关心的,因为隔离级别决定着业务极端情况下会不会出错,我们拿到一个业务时候,选择某个分布式事务框架,或者某个框架的某个模式时候,首先要摸清该模式的隔离等级是否满足我们的业务要求。
此处我只做一些简单的验证和使用示例
- 首先安装seata服务和mysql服务,具体安装步骤不再赘述,安装完成如下。
因为AT模式需要使用数据库存放前后镜像,因此我们去github上找到sql脚本,如下图
执行mysql.sql,在我们创建的数据库中创建需要用到表格。
接着创建我们的测试项目,重点是在项目中引入seata客户端依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>与你项目springCloud相对应的seata版本号</version>
</dependency>
在项目中我们创建两个RM(接下来的代码只为了演示使用seata的AT模式,因此不要太纠结项目分层以及代码风格问题)
package com.moky.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.moky.dao.StockDao;
import com.moky.entity.StockEntity;
import org.springframework.stereotype.Service;
@Service
public class StockService extends ServiceImpl<StockDao, StockEntity> {
public void createStock(){
StockEntity stockEntity = new StockEntity();
stockEntity.setGoodsId(123456L);
stockEntity.setNum(100);
save(stockEntity);
}
}
package com.moky.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.moky.dao.OrdersDao;
import com.moky.entity.OrdersEntiey;
import org.springframework.stereotype.Service;
@Service
public class OrderService extends ServiceImpl<OrdersDao,OrdersEntiey> {
public void createOrder(){
int i = 0;
i = 1/0;
/* OrdersEntiey ordersEntiey = new OrdersEntiey();
ordersEntiey.setCreateby("张三");
save(ordersEntiey);*/
}
}
注意第二个RM会抛出异常,这样我们可以模拟需要回滚时的场景。
然后我们再创建一个TM
package com.moky.service;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TMService {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@GlobalTransactional
public void testAt(){
stockService.createStock();
orderService.createOrder();
}
}
根据seataAT模式原理,作如下推断:
我们在orderService.createOrder()方法前打一个断点,我们来看看数据库中是否能够查到刚刚新增的数据,注意,此时全局事务还未全部执行完毕。
此时我们放开断点让代码执行完毕,再次查看数据库
此时,数据库已恢复,undo-log也在恢复后自动删除。
这里我回答一下之前留的两个疑问,其实两个疑问是同一个问题,如果第一个事务执行结果是正常执行,那么其实不用处理,数据是没有问题的。但是如果是要回滚,这时就会导致回滚时就会导致后镜像与数据库的数据对应不上,此时就需要我们手动配置策略。
前后镜像不一致需要手动配置回滚策略,这里一直没有找到文档。找到后再来补充,此处给自己留一个疑问。
TCC模式:
AT可以很好的无侵入的处理事务,底层依然依赖数据库的事务。若我们一组全局事务中,某一些操作不是基于数据库的,那么AT模式就显得无能为力了。此时我们可以考虑使用TCC模式。
TCC模式的优势是灵活性,不依赖于数据库的事务特性来实现两阶段提交,而是采用代码来实现。对于无法完全依赖于数据库事务特性的分布式事务,就可以考虑使用TCC模式。、
当然TCC模式的灵活性,也就带来了复杂性,因为不能依赖于数据库事务的提交和回滚机制,就需要在代码中,用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。
TCC 三个方法描述:
- Try:资源的检测和预留;
- Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
- Cancel:预留资源释放;
用户接入 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 模式高很多。
下面我们上Demo
首先创建业务类接口,在此接口中定义TCC事务
package com.moky.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import java.util.Map;
@LocalTCC
public interface TccService {
/**
* try方法(用户预留,冻结资源)
* @param params
*/
@TwoPhaseBusinessAction(name = "tryDoSomeThing",commitMethod = "commitStock",rollbackMethod = "cancel")
void tryDoSomeThing(@BusinessActionContextParameter(paramName = "params") Map<String, String> params);
/**
* 二阶段提交方法
* @param context 上下文
*/
void commitStock(BusinessActionContext context);
/**
* 二阶段取消方法
* @param context 上下文
*/
void cancel(BusinessActionContext context);
}
然后再定义具体的实现类
package com.moky.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class TccServicImpl implements TccService{
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Override
public void tryDoSomeThing(Map<String, String> params) {
System.out.println("我是 try,我执行了");
stockService.frezzStock(10);
}
@Override
public void commitStock(BusinessActionContext context) {
System.out.println("我是 commit,我执行了");
stockService.commitStock(1);
}
@Override
public void cancel(BusinessActionContext context) {
System.out.println("我是 cancel,我执行了");
stockService.cancel(1);
}
}
定义TM来定义事务的范围
package com.moky.service;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class TMService {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@Autowired
private TccService tccService;
@GlobalTransactional
public void testAt(){
stockService.createStock();
orderService.createOrder();
}
@GlobalTransactional
public void testTcc(){
Map<String, String> params = new HashMap<>();
params.put("param1","我是测试参数");
tccService.tryDoSomeThing(params);
}
}
接下来我们来调试一下看看在commit方法上打断点。
可以看见,调用try方法后会,若没有抛异常,seata框架会自动调用commit方法。
此时我们查看一下数据库,可以看到库存已被冻结。
我们放开断点,执行commit方法,可以看到freeze_num 被释放
rollback我这里就不贴图了,流程与commit类似,当Try执行异常,seata框架就会自动调用我们配置的cancel方法。
这里,我给大家解释一下用到的注解
@LocalTCC:作用于服务接口上,表示实现该接口的实现类被 seata 来管理,seata 根据事务的状态,自动调用我们定义的方法,如果没问题则调用 Commit 方法,否则调用 Rollback 方法。
@TwoPhaseBusinessAction:该注解用在接口的 Try 方法上,name 为 tcc 方法的 bean 名称,需要全局唯一,一般写方法名即可;commitMethod指定事务成功后的commit方法;rollbackMethod 指定事务失败后的rollback方法。
@BusinessActionContextParameter: 该注解用来修饰 Try 方法的入参,被修饰的入参可以在 Commit 方法和 Rollback 方法中通过 BusinessActionContext 获取。
到此TCC模式的基本使用就算是完成了,使用我们在使用TCC模式有几个需要我们重点需要注意的点
- 允许空回滚
- 防悬挂控制
- 幂等控制
1.为什么一定要允许空回滚:
Cancel 接口设计时需要允许空回滚。在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回滚。
2.防悬挂控制
所谓悬挂问题,就是二阶段模式中,cancel比try先执行
这是怎么导致的呢?
就拿我们上述的案例来假设,在订单服务中调用商品服务的扣减库存方法reduceInventory时,因为是通过RPC(feign)的方式来调用的,那么如果调用时刚好网络堵塞,或者商品服务出现问题,导致调用失败,出现报错,TM会通知TC出现错误,TC会通知所有的RM进行本地事务回滚,也就是执行cancel方法。
当cancel方法执行完成后,try方法偏偏连通了,又执行了,那么就出现了问题,订单会被更新为未提交,但因为事务已经被cancel过了,就不会再执行confirm,也就没有谁再来将资源状态从预处理更新为已处理了。资源就会导致浪费。
这时进行的回滚操作其实并没有真实回滚业务,这个现象我们称之为空回滚。
所有我们需要针对悬挂问题进行防悬挂处理,方案呢就是限制如果二阶段执行完成,一阶段就不能再执行。
比如执行cancel方法时会判断是否是空回滚,出现空回滚注册事务ID,try方法执行前先检查事务ID是否存在,如果存在则不允许执行
当然这些处理呢,seata已经帮我们实现了,这也是使用现成的分布式事务框架的好处。省心!但是我们自己要知道这些问题和原理。
seata中的解决方案是增加一个事务记录表,在cancel阶段最后往事务记录表中插入一条记录(xid-status)标记cancel阶段已经执行过。此时try阶段进入时发现已经执行过回滚操作,则放弃try阶段的执行
3.幂等性问题
所谓幂等就是操作一次和操作多次的执行效果是一样的。
想象一下,我们的库存扣除操作,如果因为某一步操作报错,导致需要回滚重试,结果每次重试都会重复扣减库存,那这样肯定是不对的。
所以为了保证我们在confirm,cancel中进行的重试机制不会使得我们的资源发生重复消耗,那么需要我们对方法做好幂等性处理:
比如说通过添加状态字段来判断是否执行过
总结: TCC模式能处理不能依赖数据库事务的业务,但是用起来很麻烦,而且需要注意的点较多。如果遇到第三方系统,对方不一定会配合(不愿意将以前的事务改造成TCC--人家本来一条sql就完成了,改成tcc需要将一阶段改成先冻结再提交,很容易影响本来的正常业务),此时TCC就没有办法了,那此时我们应该怎么办呢?
SAGA事务
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性(一阶段已提交)
seata的文档比较烂,关于状态机的视频也去看了,录制的不是很好。找到一篇不错的帖子,Demo我就不敲了。等空了补上。
你这 Saga 事务保“隔离性”吗?__江南一点雨的博客-CSDN博客_saga 隔离性
XA模式
XA模式在使用上与AT模式基本没有区别,主要是修改数据源类型
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
XA模式seata官网文档写的挺好,这里不再复述。
总结:XA和AT模式都是建立在数据库事务的基础上,如果要严格保证隔离性选XA模式,否则选AT,AT模式效率高于XA模式。而且两者都对业务代码没有侵入性,优先选择这两种,至于没有数据库可依赖的业务,首选TCC,若事务较长,三方改TCC对业务更改较多,TR与TR之间关系密切,则考虑Saga。