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

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


关联文章

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);
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot + Spring Cloud Alibaba Seata 中配置 TCC 模式,需要进行以下步骤: 1. 引入 Seata TCC 的依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.0.3.RELEASE</version> </dependency> ``` 2. 配置 Seata TCC 的相关参数: ```yaml spring: application: name: demo-service # 应用名称 seata: service: group: my_test_tx_group # 事务分组名称 vgroup-mapping.my_test_tx_group: default # 分组所在的虚拟组 enable-degrade: false # 是否开启降级模式,默认为 false use-jdk-proxy: false # 是否使用 JDK 代理,默认为 false config: type: nacos # 配置中心类型,可以是 file、nacos、apollo、zk nacos: namespace: seata # 命名空间 server-addr: localhost:8848 # Nacos 服务地址 group: SEATA_GROUP # 配置组 username: nacos # 用户名 password: nacos # 密码 file: name: file.conf # 配置文件名 registry: type: nacos # 注册中心类型,可以是 file、nacos、eureka、consul、zk nacos: server-addr: localhost:8848 # Nacos 服务地址 namespace: seata # 命名空间 group: SEATA_GROUP # 注册组 username: nacos # 用户名 password: nacos # 密码 tx-service-group: my_test_tx_group # 事务分组名称 ``` 3. 在需要使用 TCC 模式的方法上使用 @Tcc 注解: ```java @Service public class DemoServiceImpl implements DemoService { @Autowired private AccountService accountService; @Autowired private StorageService storageService; /** * TCC 模式下的分布式事务实现 */ @Override @GlobalTransactional(timeoutMills = 300000, name = "demo-service-tx") public void tccTransaction(String userId, String commodityCode, Integer count) { // 第一步:减少库存 storageService.decrease(commodityCode, count); // 第二步:扣除账户余额 accountService.decrease(userId, count); // 第三步:确认(提交) // do nothing // 第四步:取消(回滚) // do nothing } } ``` 4. 在 Seata 控制台中配置相应的 TCC 事务分组。 以上就是在 Spring Boot + Spring Cloud Alibaba Seata 中配置 TCC 模式的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值