借助Nacos高效配置与实践Seata事务的TCC模式

2024年 2月 1日 51.2k 0

实现

TCC 模式

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作。

流程分析

图片图片

阶段一(Try):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30

图片图片

图片图片

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变,事务直接提交无需等待其它事务。

阶段二(Confirm) :假如要提交,则冻结金额扣减30

图片图片

确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了,此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70

阶段二(Cancel):如果要回滚,则冻结金额扣减30,可用余额增加30

图片图片

需要回滚,那么就要释放冻结金额,恢复可用金额

Seata的TCC模型

图片图片

代码样例

配置和依赖参考之前《利用Nacos实现Seata事务模式(XA与AT)的快速配置与灵活切换》即可

bank3:

声明TCC接口

@LocalTCC
public interface AccountInTcc {

    @TwoPhaseBusinessAction(name = "prepareDeductMoney", commitMethod = "commitDeductMoney", rollbackMethod = "rollbackDeductMoney")
    boolean prepareDeductMoney(BusinessActionContext businessActionContext,
                               @BusinessActionContextParameter(paramName = "accountNo")String accountNo,
                               @BusinessActionContextParameter(paramName = "amount")Double amount);

    /**
     * 提交扣款
     * 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致
     */
    boolean commitDeductMoney(BusinessActionContext businessActionContext);
    /**
     * 回滚扣款
     * 二阶段回滚方法,要保证与rollbackMethod一致
     */
    boolean rollbackDeductMoney(BusinessActionContext businessActionContext);
}

具体实现:

@Component
public class AccountInTccImpl implements AccountInTcc {

    @Autowired
    private AccountInfoMapper accountInfoMapper;

    @Transactional
    @Override
    public boolean prepareDeductMoney(BusinessActionContext businessActionContext, String accountNo, Double amount) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasPrepareResult(xid)) {
            return true;
        }

        // 避免空悬挂,已经执行过回滚了就不能再预留资源
        if (TccActionResultWrap.hasRollbackResult(xid) || TccActionResultWrap.hasCommitResult(xid)) {
            return false;
        }
        // 预留资源
        boolean result = accountInfoMapper.prepareDeductMoney(accountNo,amount) > 0;

        // 记录执行结果,以便回滚时判断是否是空回滚
        TccActionResultWrap.prepareSuccess(xid);
        System.out.println("============prepare==============");
        return result;
    }

    // 保证提交逻辑的原子性
    @Transactional
    @Override
    public boolean commitDeductMoney(BusinessActionContext businessActionContext) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasCommitResult(xid)) {
            return true;
        }
        Map actionContext = businessActionContext.getActionContext();
        String accountNo = (String) actionContext.get("accountNo");
        BigDecimal amount = (BigDecimal) actionContext.get("amount");
        // 执行提交操作,扣除预留款
        boolean result = accountInfoMapper.commitDeductMoney(accountNo,amount.doubleValue()) > 0;
        // 清除预留结果
        TccActionResultWrap.removePrepareResult(xid);
        // 设置提交结果
        TccActionResultWrap.commitSuccess(xid);
        System.out.println("============commit==============");
        return result;
    }

    @Transactional
    @Override
    public boolean rollbackDeductMoney(BusinessActionContext businessActionContext) {
        String xid = businessActionContext.getXid();
        // 幂等性判断
        if (TccActionResultWrap.hasRollbackResult(xid)) {
            return true;
        }
        // 没有预留资源结果,回滚不做任何处理;
        if (!TccActionResultWrap.hasPrepareResult(xid)) {
            // 设置回滚结果,防止空回滚
            TccActionResultWrap.rollbackSuccess(xid);
            return true;
        }
        // 执行回滚
        Map actionContext = businessActionContext.getActionContext();
        String accountNo = (String) actionContext.get("accountNo");
        BigDecimal amount = (BigDecimal) actionContext.get("amount");
        boolean result = accountInfoMapper.rollbackDeductMoney(accountNo,amount.doubleValue()) > 0;
        // 清除预留结果
        TccActionResultWrap.removePrepareResult(xid);
        // 设置回滚结果
        TccActionResultWrap.rollbackSuccess(xid);
        System.out.println("============rollback==============");
        return result;
    }
}

业务层:

@Autowired
  private AccountInTcc accountInTcc;

  @Override
  public Boolean deductMoney(String accountNo, Double amount) {
      return accountInTcc.prepareDeductMoney(null,accountNo,amount);
  }

参数中的BusinessActionContext不需要开发人员自己传递,直接给null即可,Seata会自动处理。

mapper:

@Update("update account_info set account_balance = account_balance - #{amount}, frozen_money = frozen_money + #{amount} where account_no = #{accountNo} and account_balance >= #{amount}")
    int prepareDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);
    @Update("update account_info set frozen_money = frozen_money - #{amount} where account_no = #{accountNo}")
    int commitDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);
    @Update("update account_info set account_balance = account_balance + #{amount}, frozen_money = frozen_money - #{amount} where account_no = #{accountNo}")
    int rollbackDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);

bank4服务调用:

@GlobalTransactional
    @Override
    public Boolean addMoney(String accountNo, Double amount) {

        String result = bank3Client.deduct(amount);
        if("true".equalsIgnoreCase(result)){
            Boolean flag = baseMapper.addMoney(accountNo,amount) > 0;
            if(amount != 30 ) throw new RuntimeException("bank4 make exception amount != 30");
            return flag;
        }
        return false;
    }

TCC的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点:

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理
  • 空回滚:当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚
  • 业务悬挂:对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。

图片图片

执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论