一、Seata核心组件
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
举例:
A服务调用B服务,B服务调用C服务。
此时TM定义全局事务管理的服务是A服务,B服务和C服务是RM。TC就是Seata客户端了。
二、XA模式
1、简介
定义:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
XA模式是一个强一致性的分阶段事务模式,当各个分支事务完成之后,不提交。等各个分支事务将自己的事务执行状态报告给TC,由TC决定是分支事务的提交还是回滚。
这种处理方式保证了数据的一致性,但是每个事务的执行需要等待别的事物执行结果,性能不好。
2、两段式提交
两段式提交:
第一个阶段,所有的分支事务全部执行,但不提交,同时将分支事务的执行情况汇总交给一个协调者。
第二个阶段就是协调者根据各个分支事务的执行情况来给各个分支事务发指令,是一起commit提交还是回滚。
3、XA模式原理
XA执行流程:
第一步:
1.1 事务发起者TM在Seata的客户端TC创建一个全局事务,此时会获取得到一个全局事务Id。
1.2 调用各个分支事务,同时将这个全局事务Id传递过去,这样每个分支事务都获得这个全局事务Id了。
1.3 各个分支事务注册到根据这个全局事务Id注册到seata的客户端TC,这样各个分支事务都注册到了TC的全局事务Id里面了。
1.4 各个分支事务开始执行,执行完成之后不提交。
1.5 各个分支事务将执行结果报告给TC
第二步:
2.1 TM事务发起方 通知TC端开始执行事务。
2.2 TC端根据各个分支事务执行情况,通知各个事务提交/回滚。
2.3 各个分支 事务开始执行 提交/回滚。
1)RM一阶段的工作: 注册分支事务到TC 执行分支业务sql但不提交 报告执行状态到TC。
2)TC二阶段的工作: TC检测各分支事务执行状态 如果都成功,通知所有RM提交事务 如果有失败,通知所有RM回滚事务。
3)RM二阶段的工作: 接收TC指令,提交或回滚事务。
XA模式的优点:
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入。
XA模式的缺点:
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差。
依赖关系型数据库实现事务。不支持Redis等非关系型数据库。
三、AT模式
1、简介
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
AT模式也是默认的seata事务模式。
2、AT模式执行逻辑
AT模式和XA模式最大的不同就是,XA模式执行完成之后不会提交,最后会统一提交。但是AT模式为了提升性能,就需要执行完之后就立即提交,极大提升了性能,当然也不可避免的出现脏数据问题。
第一步:
1.1 事务发起者TM在Seata的客户端TC创建一个全局事务,此时会获取得到一个全局事务Id。
1.2 调用各个分支事务,同时将这个全局事务Id传递过去,这样每个分支事务都获得这个全局事务Id了。
1.3 各个分支事务注册到根据这个全局事务Id注册到seata的客户端TC,这样各个分支事务都注册到了TC的全局事务Id里面了。
1.4 各个分支事务数据库存储执行前的快照数据。存储完成之后开始执行SQL语句,执行完成之后直接提交即可。
1.5 各个分支事务向TC报告各自的执行情况。
第二步:
2.1 TM事务发起方 通知TC端开始执行事务。
2.2 TC端根据各个分支事务执行情况,通知各个事务提交/回滚。
2.3 如果需要事务提交,则删除我们记录的执行前的快照即可。
2.4 如果需要事务回滚,则根据快照恢复执行前的数据,删除存储快照即可。
阶段一RM的工作: 注册分支事务 记录undo-log(数据快照) 执行业务sql并提交 报告事务状态。
阶段二提交时RM的工作: 删除undo-log即可。
阶段二回滚时RM的工作: 根据undo-log恢复数据到更新前。
从上述讲解我们理解,关键点就是这个快照,通过快照进行回滚数据。
举例:
例如,一个分支业务的SQL是这样的:update tb_account set money = money - 10 where id = 1 。
执行完成之后,这个数据是
存储的快照是
执行流程:
简述AT模式与XA模式最大的区别?
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
XA模式强一致;AT模式最终一致
3、AT模式的脏数据问题
1、数据脏写情况
解释说明:
之所以是行锁是因为where 后面的条件id是索引,如果不是索引就是表锁了。
第一阶段:
1.1 事务1获取得到数据库的行锁,保存当前数据未修改之前的数据。
1.2 执行SQL语句,将id为1的money 从100修改为90。
(此时有新的线程过来如果要处理这行数据是不允许的,因为已经被行锁锁住了)
1.3 提交事务,释放数据库行锁。
第二阶段:
2.1 此时数据库DB行锁被释放,事务2开始修改这行数据。
2.2 事务1获取得到数据库的行锁,保存当前数据未修改之前的数据。
2.3 执行SQL语句,将id为1的money 从90修改为80。
2.4 提交事务,释放数据库行锁。
第三阶段:
3.1 分布式事务抛异常,使用数据未修改之前的数据快照进行数据恢复。
3.2 事务1 找到保存的 数据快照 id为1 的money是100,直接恢复,此时将id为1的数据从80 改为100。本来是希望 90 -> 100,由于在事务提交和回滚的空隙时间有别的事务进行了数据修改,出现了这种脏数据问题。
针对上述的问题,Seata也给我们提供了解决方案,就是Seata自己内部维护了一个全局锁,来保证Seata内部事务和事务之间的调用顺序。
注意是Seata自己内部维护的全局锁,自然也只能保证自己内部的事务了。如果是非Seata 的事务就无法保证了。
解决Seata的
全局锁:由TC记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。
第一阶段:
1.1 事务1获取得到数据库的行锁,保存当前数据未修改之前的数据。
1.2 执行SQL语句,将id为1的money 从100修改为90。
1.3 获取Seata内部的全局锁,此时拥有的是 数据库内部的行锁和Seata内部的全局锁。
(此时有新的线程过来如果要处理这行数据是不允许的,因为已经被行锁锁住了)
1.3 提交事务,释放数据库行锁。
第二阶段:
2.1 此时数据库DB行锁被释放,事务2开始修改这行数据。
2.2 事务2获取得到数据库的行锁,保存当前数据未修改之前的数据。
(如果修改的不是这行数据,则不会被全局锁影响,全局锁也是个行锁)
2.3 修改的时候发现还有一个Seata内部的全局锁,于是开始等待全局锁的索释放。
2.4 全局锁一直未被释放,此时事务2执行超时,释放DB行锁
(在事务2 等待全局锁释放且自身未释放行锁期间,事务1的回滚也是不可以的,因为数据库行锁被事务2占用了)
第三阶段:
3.1 分布式事务抛异常,使用数据未修改之前的数据快照进行数据恢复。
3.2 事务1 找到保存的 数据快照 id为1 的money是100,直接恢复即可。
上述的解决方案看似解决了脏数据问题,但有一个核心前提就是这个事务必须都是Seata的内部事务,如果是非Seata的事务,不受全局锁影响,仍会存在脏数据问题,这个问题无可避免,但是由于出现情况极少,可以才用出现之后,将这个消息发送到消息中间件的告警队列,消费者消费告警队列发送邮件通知用户进行手动处理。
核心点:事务执行前会记录 事务执行前的快照和事务执行后的快照数据。根据事务执行前的快照回滚数据,根据事务执行后的快照数据判断是否存在脏数据问题。
在上述处理过程中,进行数据回滚之前会将当前数据库的数据和事务执行后的快照数据进行比较,不相等的话说明出现了脏数据,将这个消息发送到消息中间件的告警队列,消费者消费告警队列发送邮件通知用户进行手动处理。相等的话就证明没有脏数据,进行数据恢复即可。恢复完成之后删除快照数据即可。
AT模式的优点:
1、一阶段完成直接提交事务,释放数据库资源,性能比较好。
2、利用全局锁实现读写隔离。
3、没有代码侵入,框架自动完成回滚和提交 。
AT模式的缺点:
1、两阶段之间属于软状态,属于最终一致。
2、框架的快照功能会影响性能,但比XA模式要好很多。
软状态指的就是 分支事务提交之后和整体事务完成之后这段空隙时间存在数据不一致问题。,但最终随着整体事务的提交/回滚,会打到数据的一致性,所以叫做最终一致性。
四、TCC模式
1、简介
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。
实现方式,3个方法:
1、Try:资源的检测和预留;
2、Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
3、 Cancel:预留资源释放,可以理解为try的反向操作。
TCC这个模式的不同之处就在于有代码侵入。
TCC的工作模型图
核心:使用这个模式关键是 创建一张表 --- 事务处理状态表(A表)
第一阶段:
1.1 事务发起者TM在Seata的客户端TC创建一个全局事务,此时会获取得到一个全局事务Id。
1.2 调用各个分支事务,同时将这个全局事务Id传递过去,这样每个分支事务都获得这个全局事务Id了。
1.3 各个分支事务注册到根据这个全局事务Id注册到seata的客户端TC,这样各个分支事务都注册到了TC的全局事务Id里面了。
1.4 各个分支事务首先预留资源。预留资源的具体操作就是记录冻结金额和事务状态到A表。
1.5 各个分支事务向TC报告各自的执行情况。
第二阶段:
2.1 TM事务发起方 通知TC端开始执行事务。
2.2 TC端根据各个分支事务执行情况,通知各个事务提交/回滚。
第三阶段:
3.1 事务执行成功,开始confirm操作,根据xid删除A表的冻结记录。
3.2 事务执行失败,开始Canel操作,修改A表,冻结金额为0,state为2,根据A表的冻结金额恢复数据。
以上的操作步骤是我们理想状态下的,但是我们往往在业务逻辑里面会遇到各种场景问提,下面是几个场景问题和解决方案。
问题一:调用分支事务1的时候,执行成功。调用分支事务2的时候阻塞了。此时回滚的时候,需要如何回滚呢?
在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。对于已经空回滚的业务,如果以后继续执行try,就永远不可能confirm或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂。
空回滚情况: 上方调用分支按照TCC流程正常执行,此时下方调用分支因为某种原因而阻塞了,由于长时间没有执行,这个分支发生了超时错误,由TM经过2.1步骤发送超时错误,回滚全局事务的指令给TC,TC检查分支状态2.2,发现确实有一只分支超时,发送2.3回滚指令到各分支的RM,由RM执行2.4cancel操作。 此时对于第一个分支而言,执行cancel没有问题,因为流程正常。但对于第二个分支而言,他并没有执行第一步的try,所以此时第二个分支不能真正的执行cancel,需要执行空回滚,也就是说返回一个正常状态,且不报错。需要在cancel之前查看是否有前置的try,如果没有执行try则需要空回滚。
业务悬挂情况: 假设在上方的基础上,下方分支的阻塞畅通了,此时他执行1.4去锁定资源(try),但整个事务都已经回滚结束了,所以他不会执行第二阶段,但冻结了资源,这种情况应该进行避免。需要在try操作之前查看当前分支是否已经回滚过,如果已经回滚过则不能在执行try命令。
我们拿下订单扣减库存的案例来说,在订单服务中调用商品服务的扣减库存方法reduceInventory时,通常通过RPC(feign)的方式来调用,那么如果调用时刚好网络堵塞,或者商品服务出现问题,导致调用失败,出现报错,TM会通知TC出现错误,TC会通知所有的RM进行本地事务回滚,也就是执行补偿事务。
当补偿事务方法执行完成后,正向事务方法偏偏连通了,又执行了,那么就出现了问题,这个正向服务之前的补偿事务都执行了,但又执行了一个多余的正向服务。
所有我们需要针对悬挂问题进行防悬挂处理,方案呢就是限制如果二阶段执行完成,一阶段就不能再执行。
seata中的解决方案是增加一个事务记录表,在补偿服务执行后往事务记录表中插入一条记录(xid-status)标记补偿服务已经执行过。此时正向服务进入时发现已经执行过回滚操作,则放弃正向服务的执行。
关键注解:
@LocalTCC
Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
@TwoPhaseBusinessAction(name = "prepare",
commitMethod = "confirm",
rollbackMethod = "cancel")
2、 TCC伪代码
package com.hihonor.tcc;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface Tccservice {
// try方法
// Try逻辑,@TwoPhaseBusinessAction中的name属性要与当前方法名一致,用于指定Try逻辑对应的方法
@TwoPhaseBusinessAction(name = "preparre",commitMethod = "confirm",rollbackMethod = "cancel")
void preparre();
// confirm 提交方法
//@param context 上下文,可以传递try方法的参数
void confirm (BusinessActionContext context);
// cancel 回滚方法
void cancel (BusinessActionContext context);
}
package com.hihonor.tcc.impl;
import com.hihonor.bean.AccountFreeze;
import com.hihonor.mapper.AccountFreezeMapper;
import com.hihonor.mapper.UserMapper;
import com.hihonor.service.RakeFeginService;
import com.hihonor.tcc.Tccservice;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class TccserviceImpl implements Tccservice {
@Autowired
private AccountFreezeMapper accountFreezeMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private RakeFeginService rakeFeginService;
/*
* 锁定资源
* 当我们执行方法之后,将订单创建成功
* 保存订单的创建id到我们的 TCC 状态表里面,事务状态设置为0
*
* 业务悬挂:try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
* */
@Override
public void preparre() {
String xid = RootContext.getXID();
addOrder();
// 业务悬挂
AccountFreeze accountFreezeTwo = accountFreezeMapper.selectXid(xid);
//如果已经存在则证明Cancel已经执行,拒绝执行try业务
if (accountFreezeTwo != null) {
return;
}
addFreeze(xid);
}
// 添加锁定资源信息
private void addFreeze(String xid) {
AccountFreeze accountFreeze = AccountFreeze.builder()
.xid(xid)
.userId("108")
.freezaMoney(100)
.state(0)
.build();
// 添加锁定资源信息
accountFreezeMapper.insertFreeze(accountFreeze);
}
/*
* 提交资源
* 将Tcc状态表里面的数据删除即可---根据xid
*xid --- seata 的事务id
* */
@Override
public void confirm(BusinessActionContext context) {
try {
accountFreezeMapper.deleteAccountFreeze(context.getXid());
} catch (Exception e) {
log.error("资源提交,删除冻结资源失败");
}
}
/*
* 如果发生回滚
* 将这个xid的状态改为回滚,
* 同时将里面的订单id值取出来,找到对应订单删除
* 发生空回滚:如果发现没有这个xid,则开始 try->cancle
*
*
* */
@Override
public void cancel(BusinessActionContext context) {
AccountFreeze accountFreeze = accountFreezeMapper.selectXid(context.getXid());
// 如果为空,证明是空回滚,此时需要处理
if (accountFreeze == null) {
System.out.println("**********空回滚");
// 创建订单
addOrder();
//锁定资源
addFreeze(RootContext.getXID());
return;
}
// 判断幂等性 此时回滚超时,seata 会进行行二次调用
if (accountFreeze.getState() == 2) {
return;
}
accountFreeze.setState(2);
accountFreezeMapper.insertFreeze(accountFreeze);
userMapper.updateOrderOne(Long.parseLong(accountFreeze.getUserId()));
}
// 直送数据库写操作--实际业务
private void addOrder() {
System.out.println("调用成功*************");
userMapper.updateOrder(108L);
// 调用返利服务
rakeFeginService.rake1("刘晓辉001", 588);
}
}
五、Saga模式
1、简介
saga的定义是“长时间活动的事务”,是普林斯顿大学教授Hector & Kenneth发表的论文《sagas》中提出的概念。它的思想是允许分布式事务在全部提交前提前释放占用的某些资源。
其实我看到saga这个名称的第一印象,是想到了圣斗士星矢里的沙迦,沙迦以强悍的实力著称。而SAGA模式也专用于解决长事务资源占用难题。
2、长事务
所谓长事务,就是需要长时间执行的事务,这类事务往往需要访问大量的数据对象,其执行周期甚至能达到几周或几月。但传统的事务执行时需要锁定占用资源,如果在这样的一个场景下,资源被长期锁定,带来的性能消耗可想而知。因此我们引入了SAGA模式来解决长事务。
SAGA的工作原理呢,就比较好理解了:
SAGA模式由一串本地事务组成,每个本地事务都有自己回滚数据的补偿事务。
事务之间串型执行,当正向执行的某一个事务出现报错,那么将执行这个事务的补偿事务,并且逆行执行之前事务的补偿事务。
SAGA也是有两阶段的,一阶段是正向事务,二阶段是补偿事务。
saga模式依然要求我们自己实现正向服务和补偿服务。但是它于TCC模式的区别之处在于:
saga的模式设计使得它天然适合于长流程的业务。TCC要实现同样的长流程的话,需要多写一个confirm操作,并且要考虑如何将业务拆分为两部分。
saga模式在正向服务中时就已经提交了本地事务了,而补偿事务也比较好实现,将正向服务的结合逆向补偿即可。
比如正常服务是update product set price=20 where price=30 and id=1;
那么补偿服务就是update product set price=30 where price=20 and id=1;比起TCC模式,saga模式更适用于一些老服务、第三方服务或者其他无法改造的服务,要接入到我们的分布式事务中时,就可以将其作为一个正向服务存在,而直接实现他的补偿服务即可。而TCC因为要对业务进行拆分为try-confirm-cancel,所以它不适用于不可改造的服务
同时,saga模式同样不需要全局锁,只需要结合本地事务加本地锁即可,所以性能依旧有保证。
3、SAGA模式的三种事务类型
3.1 可补偿性事务
所谓可补偿性事务,也就是可以使用、需要使用补偿事务来回滚数据的事务。
比如说下订单,就需要删除订单的补偿事务,因此下订单就是一个可补偿性事务。
3.2 关键性事务
关键性事务是saga执行的关键点,如果关键性事务运行成功,则saga将一直运行到完成。关键性事务不一定是个可补偿性事务或者可重复性事务,但是他可以是最后一个可补偿的事务或第一个可重复的事务。
———《微服务架构设计模式》
通过书中的描述,我们知道关键性事务的定义从结构上理解,是处于可补偿性事务和可重复性事务的中间。
具体把哪个事务定义为关键性事务,还要根据具体的业务情况而定,我们可以通过以下标准来判断。
- 从结构上是否处于可补偿事务和可重复事务之间
- 从业务上该事务是否能表示整个业务执行成功的转折点
3.3 可重复性事务
关键性事务之后的事务就是可重复性事务,不需要回滚,并且保证能够执行完成。所以我们会通过一些机制来保证这类事务一定能执行成功,比如重试机制。
4、SAGA模式服务调用机制
我们上述说了,saga模式是通过正向服务、补偿服务之间的正向串性和逆向串性来实现的。这些服务之间的调用链很长,我们通过什么方式来实现调用呢?
4.1 每个服务给后续服务发送消息
我们让每个服务来通知后续的服务进行操作,这是我们串行服务最常想到的做法。但这样有个很明显的问题,那就是具体做起来的困难性。
想象一下,如果我们要对事件进行调用,我们不但要考虑正向的服务调用,还要考虑逆向的补偿调用,特别是要再考虑逆向的补偿调用,一些简单的串型业务设计起来很简单,但是某些交错复杂的业务会非常麻烦。同时服务间的耦合性又增强了。
所以我们做了一个增强版,那就是前一个服务执行完成后发送消息,后一个服务通过订阅消息的模式来实现服务的协调。我们引入了MQ的概念来解决耦合性问题。
引入MQ完成消息之间的顺序调用
优点:这种模式的实现相对来说逻辑清晰,当然这里简单只能说针对于部分业务而言,某些比较复杂的场景的话,这样的设计反而很难实现,相互之间的订阅交错复杂。
缺点:服务之间通过订阅消息来触发调用,处理不当,容易造成相互订阅的情况,从而出现循环依赖或消息死循环问题。
面对复杂业务的局限性:上述也说了,当业务调用交错复杂时,我们通过订阅消息的形式来获取调用事务的时机,但是也决定了,每个事务都要订阅会影响它的事件消息,复杂场景时会导致我们需要考虑的东西很多,就需要非常强大的逻辑能力来支撑了,存在局限性。
难上手:想象一下,你接手上一个同事设计的稍微复杂一点的协同模式时,你需要花多久理清楚这里面的消息订阅的逻辑线。它的呈现并不直观,较难理解。
5、事件驱动器来协调
如果接触过过工作流设计的同学可能对这个东西比较熟悉,简单来说就是一个第三方组件,通过它可以进行拖拽化的流程设计,如下图所示。关于这个驱动器就不再多说了,感兴趣的可以去官方了解。
seata官方也提供了在线的模块设计工具:saga 事件驱动在线设计
好处
不会产生死循环:调用是单向的,驱动器会调用事务,但是事务不会调用驱动器,因此其调用关系完全交给驱动器去管理,也就没有了依赖循环的问题
理解简单:虽然设计器的使用学习有成本,但是我们针对其设计上的理解来说,更加容易理解上手。
业务逻辑更加简单清晰:事务协调完全交给了驱动器,业务代码无需关心,可以专注于业务需求上,降低了代码难度。
坏处
学习成本:存在一定的设计器的以及相关API的学习成本
6、SAGA模式如何保证事务隔离性
事务独立执行,不受其他并发操作影响就是事务的隔离性。
但如上述,SAGA中各个本地事务执行完就提交,所以是相对独立的,SAGA事务中的某一个事务的执行结果,是可以被其他业务操作或影响的。但一旦中间数据被其他业务修改了,再要回退时就会出现脏写而无法回滚。
因此SAGA模式下的分布式事务就没有隔离性了吗?那还能叫事务吗?出现脏写怎么办?
SAGA模式也提供了一种最终实现隔离性的思路,seata官方文档中的介绍是“宁可长款,不可短款”的原则。
官方解释如下
业务流程设计时遵循“宁可长款, 不可短款”的原则, 长款意思是客户少了钱机构多了钱, 以机构信誉可以给客户退款, 反之则是短款, 少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
有些业务场景可以允许让业务最终成功, 在回滚不了的情况下可以继续重试完成后面的流程, 所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。
通俗来讲,就是不需要按照原路返回,只需要通过一定的措施让数据恢复之前的状态即可,也就是保证最终一致性即可。
7、锁定方案
7.1语义锁
说的直白点语义锁就是一个标记状态,该标记表示该记录未提交且可能发生改变,正向事务会在其操作的每一条记录中添加这个状态。
比如扣除库存时,给对应商品添加一个状态,当有其他事务访问这个商品时发现状态为锁定中,就不会再访问这个商品,会等待状态更新完成后再操作。其实就是一个手动加锁的过程。
执行成功的话,最后通过一个可重复事务或者定时任务将状态更新为已解锁,执行失败就通过补偿事务将状态更新为已解锁
这里的可重复事务,指的是不管执行成功还是失败都可以执行的事务,其执行不影响原本的数据记录。一般可重复性事务会放到最后执行。
7.2 交换式更新
把更新操作设计成可以按任何顺序执行,也就是说操作是可以交换的。
这样说明其实是很抽象的,我们举个例子来说明,比如下订单后,商品要扣减库存,正向事务是库存-10,
update product set inventory=inventory-10 where id=1;
那么将其补偿事务设计为库存+10。update product set inventory=inventory+10 where id=1;
这样哪怕有其他事务操作污染了库存数据,但是因为更新的内容是纯数字的,不受其他事务的影响,且定位信息为id=1这一点无法篡改。当然我们要求这里没有删除商品的操作。
那么我们就称现在的正向事务和补偿事务是可以交换的。是不是稍微理解一点了,更新的交换性存在着很大的局限性,并不是所有的操作都可以设计为可交换的。
常见的交换性操作也就是数值、枚举值上的更新。因此不同的操作也需要我们结合不同的方案来设计隔离性。
7.3悲观视图
悲观视图实际上并不是一个100%的方案,他的本意是说以最悲观的情况考虑事务被其他事务更改的可能性,然后重新排序saga事务的步骤,以此最大限度的较低脏写风险
所以这也就决定了,悲观视图是一个不那么完善的方案,并不能完全保证隔离性。所以能够应用到的业务也有限。
7.4重读值
重读值的意思是在更新之前重新读取记录,可以通过维护一个计数器,比如版本号,来验证它是否发生改变,如果已经改变了那么事务中止或者重新启动。如果未改变那么继续执行。其实了解乐观锁的同学应该闻到味儿了,没错,这玩意儿就是乐观锁的一种。
7.5版本文件
版本文件对策之所以如此命名,是因为它记录了对数据执行的操作,以便可以对它们进行重新排序。这是将不可交换操作转换为可交换操作的一种方法
针对这个方案的理解,我们直接通过一个案例来解释:
我们有一个下订单的业务。我们现在还有一个取消这个订单的业务
当因为网络阻塞或者其他原因导致取消订单的业务先执行了,当下订单的业务后执行时就会创建一个订单,导致用户发起的取消操作变得无效了。从而产生了数据的不一致性
通过版本文件,我们将在取消订单的时候会记录这个取消操作数据,后续收到下订单的操作时会比较版本文件,来跳过订单的创建操作,其效果也就相当于将两个操作顺序调换了一下。以此保证了最后的数据依旧是订单被取消。
就是低风险的不容易出现脏读或者出现脏读也不影响业务的选择saga,高风险的对脏读敏感的选择其他分布式模式。
8、SAGA模式的补偿措施
8.1幂等性问题
所谓幂等就是操作一次和操作多次的执行效果是一样的。
想象一下,我们的库存扣除操作,如果因为某一步操作报错,导致需要回滚重试,结果每次重试都会重复扣减库存,那这样肯定是不对的。
所以为了保证我们在confirm,cancel中进行的重试机制不会使得我们的资源发生重复消耗,那么需要我们对方法做好幂等性处理:
比如说通过添加状态字段来判断是否执行过。当然这一点在seata等分布式框架中不用我们再手动实现,框架已经帮我们实现了。
8.2 悬挂问题
所谓悬挂问题,就是二阶段模式中,二阶段比一阶段先执行。
我们拿下订单扣减库存的案例来说,在订单服务中调用商品服务的扣减库存方法reduceInventory时,通常通过RPC(feign)的方式来调用,那么如果调用时刚好网络堵塞,或者商品服务出现问题,导致调用失败,出现报错,TM会通知TC出现错误,TC会通知所有的RM进行本地事务回滚,也就是执行补偿事务。
当补偿事务方法执行完成后,正向事务方法偏偏连通了,又执行了,那么就出现了问题,这个正向服务之前的补偿事务都执行了,但又执行了一个多余的正向服务。
所有我们需要针对悬挂问题进行防悬挂处理,方案呢就是限制如果二阶段执行完成,一阶段就不能再执行。
seata中的解决方案是增加一个事务记录表,在补偿服务执行后往事务记录表中插入一条记录(xid-status)标记补偿服务已经执行过。此时正向服务进入时发现已经执行过回滚操作,则放弃正向服务的执行。
SAGA模式应用场景
- 适用于长事务业务场景
- 适用于需要接入老服务、第三方服务或者其他无法改造的服务的业务场景
- 需要操作更细分散在多个服务、系统中的数据的业务场景
参考文章:springcloud进阶:四种分布式事务模式之SAGA模式(三)_wu@55555的博客-CSDN博客
Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:
一阶段:直接提交本地事务
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
Saga模式
优点:
事务参与者可以基于事件驱动实现异步调用,吞吐高
一阶段直接提交事务,无锁,性能好
不用编写TCC中的三个阶段,实现简单
缺点:
软状态持续时间不确定,时效性差
没有锁,没有事务隔离,会有脏写