SpringCloud:seata 事务之TCC模式(3)

SpringCloud:seata 事务之TCC模式(3),第1张

SpringCloud:seata 事务之TCC模式(3)

文章目录
  • SpringCloud:seata 事务之TCC模式(3)
    • 关联文章
    • 1、TCC设计
      • 1.1、允许空回滚
      • 1.2、防悬挂控制
      • 1.3、幂等控制
    • 2、配置文件
    • 3、使用TCC
      • 3.1、客户端
      • 3.2、定义TCC接口
      • 3.3、ResultHolder类


关联文章

SpringCloud:seata 服务端启动以及介绍

SpringCloud:seata 事务之AT模式

SpringCloud:seata 事务之TCC模式

1、TCC设计

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

简单点概括,SEATA的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑而不依赖AT模式的undo_log

1.1、允许空回滚

Cancel 接口设计时需要允许空回滚。在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回滚。

正如二阶段cancel代码:

        String xid = businessActionContext.getXid();
        //空返回 防悬挂
        String id = ResultHolder.getResult(this.getClass(), xid);
        if (id == null) {
            //保证事务防悬挂
            if (xid != null) {
                ResultHolder.setResult(this.getClass(), xid, "二阶段Cancel");
            }
            return true;
        }
1.2、防悬挂控制

悬挂的意思是:Cancel 比 Try 接口先执行,出现的原因是 Try 由于网络拥堵而超时,事务管理器生成回滚,触发 Cancel 接口,而最终又收到了 Try 接口调用,但是 Cancel 比 Try 先到。按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的 Try 接口不应该执行,否则会产生数据不一致,所以我们在 Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,则不执行 Try 的业务 *** 作。

一阶段try代码:

String xid = businessActionContext.getXid();
//防悬挂
String id = ResultHolder.getResult(this.getClass(), xid);
if (id != null) {
    //保证事务防悬挂,cancel,则一阶段try不再执行直接返回。
    ResultHolder.removeResult(this.getClass(), xid);
    return new CommonResult(500, "插入失败");
}
1.3、幂等控制

幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试 *** 作,所以很可能一个业务 *** 作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我们可以用事务 xid 或业务主键判重来控制。

通过ResultHolder中的Class actionClassString xid来控制。

2、配置文件
seata:
  application-id: ${spring.application.name}
  config:
    type: nacos
    nacos:
      data-id: seata.properties
      username: "nacos"
      password: "nacos"
      group: SEATA_GROUP

同AT模式,采用nacos配置中心,可以看前面的讲解,这儿不做介绍。

3、使用TCC 3.1、客户端
    @Override
    @GlobalTransactional
    public CommonResult insert(AccountDO accountDO) throws InnerException {
        log.info("client端insert入参{}", accountDO);
        CommonResult insert = tccAccountFeign.insert(accountDO);
        if (!insert.getCode().equals(200)) {
            throw new InnerException(ResultCode.SYSTEM_ERROR, insert.getMessage());
        }
        log.info("client端insert出参:{}", JSON.toJSONString(insert));
        return insert;
    }

同AT模式,加上 @GlobalTransactional 注解,管控全局事务。

3.2、定义TCC接口

定义接口

@LocalTCC
public interface AccountService {
    @TwoPhaseBusinessAction(name = "insert", commitMethod = "insertCommit", rollbackMethod = "insertCancel")
    CommonResult insert(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "accountDO") AccountDO accountDO);

    /**
     * insert 二阶段提交方法
     *
     * @param businessActionContext
     * @return
     */
    Boolean insertCommit(BusinessActionContext businessActionContext);

    /**
     * insert 二阶段回滚方法
     *
     * @param businessActionContext
     * @return
     */
    Boolean insertCancel(BusinessActionContext businessActionContext);
}

由于我们使用的是 SpringCloud + Feign,Feign的调用基于http,因此此处我们使用@LocalTCC便可。值得注意的是,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法便可,TCC相关注解如下:

  • @LocalTCC 适用于SpringCloud+Feign模式下的TCC
  • @TwoPhaseBusinessAction 注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指向提交方法,rollbackMethod指向事务回滚方法。指定好三个方法之后,seata会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。
  • @BusinessActionContextParameter 注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法。
  • BusinessActionContext 便是指TCC事务上下文

接口实现

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Autowired
    public AccountDao accountDao;

    @Override
    @Transactional
    public CommonResult insert(BusinessActionContext businessActionContext, AccountDO accountDO) {
        String xid = businessActionContext.getXid();
        //防悬挂
        String id = ResultHolder.getResult(this.getClass(), xid);
        if (id != null) {
            //保证事务防悬挂,先执行了cancel,则一阶段try不再执行直接返回。
            ResultHolder.removeResult(this.getClass(), xid);
            return new CommonResult(500, "插入失败");
        }
        ResultHolder.setResult(this.getClass(), businessActionContext.getXid(), accountDO.getId().toString());
        int i = accountDao.insert(accountDO);
        if (i > 0) {
            return new CommonResult(200, "插入成功");
        }
        //保证幂等性
        return new CommonResult(500, "插入失败");
    }

    /**
     * 二阶段提交
     * @param businessActionContext
     * @return
     */
    @Override
    public Boolean insertCommit(BusinessActionContext businessActionContext) {
        //若已经移除了,则直接返回不需要再次执行
        if (ResultHolder.getResult(this.getClass(), businessActionContext.getXid()) == null) {
            return true;
        }
        Map<String, Object> actionContext = businessActionContext.getActionContext();
        log.info(JSON.toJSONString(actionContext));
        //移除,保持幂等
        ResultHolder.removeResult(this.getClass(), businessActionContext.getXid());
        return true;
    }

    /**
     * 二阶段回滚
     * @param businessActionContext
     * @return
     */
    @Override
    public Boolean insertCancel(BusinessActionContext businessActionContext) {
        String id = ResultHolder.getResult(this.getClass(), businessActionContext.getXid());
        if (id == null) {
            return true;
        }
        Map<String, Object> actionContext = businessActionContext.getActionContext();
        AccountDO accountDO = JSONObject.parseObject(actionContext.get("accountDO").toString(), AccountDO.class);
        log.info("实体类:{},返回主键:{}", accountDO, id);
        accountDao.deleteById(id);
        ResultHolder.removeResult(this.getClass(), businessActionContext.getXid());
        return true;
    }

}

  • 在try方法中使用@Transational可以直接通过spring事务回滚关系型数据库中的 *** 作,而非关系型数据库等中间件的回滚 *** 作可以交给rollbackMethod方法处理。

  • 使用context.getActionContext(“params”)便可以得到一阶段try中定义的参数,在二阶段对此参数进行业务回滚 *** 作。

  • 注意1: 此处亦不可以捕获异常(同理切面处理异常),否则TCC将识别该 *** 作为成功,二阶段直接执行commitMethod。

  • 注意2: TCC模式要 开发者自行 保证幂等和事务防悬挂

  • ResultHolder类用来保持幂等性,在一阶段try时存入,在二阶段成功时移除。若重复执行根据ResultHolder的值来判断是否执行成功。

3.3、ResultHolder类
public class ResultHolder {
    private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();

    public static void setResult(Class<?> actionClass, String xid, String v) {
        Map<String, String> results = map.get(actionClass);
        if (results == null) {
            synchronized (map) {
                if (results == null) {
                    results = new ConcurrentHashMap<>();
                    map.put(actionClass, results);
                }
            }
        }
        results.put(xid, v);
    }

    public static String getResult(Class<?> actionClass, String xid) {
        Map<String, String> results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

    public static void removeResult(Class<?> actionClass, String xid) {
        Map<String, String> results = map.get(actionClass);
        if (results != null) {
            results.remove(xid);
        }
    }
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/756336.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-30
下一篇 2022-04-30

发表评论

登录后才能评论

评论列表(0条)

保存