微服务seata 1.4.2 分布式事务TCC模式示例

seata TCC模式和AT模式的基础环境是一样的,只是在实现方式上有所区别,而且TCC模式还可以和AT模式混合使用。

关于AT模式示例,可以参考seata 1.4.2 分布式事务AT模式示例。

TCC模式的优势是灵活性,不依赖于数据库的事务特性来实现两阶段提交,而是采用代码来实现。对于无法完全依赖于数据库事务特性的分布式事务,就可以考虑使用TCC模式。、

当然TCC模式的灵活性,也就带来了复杂性,因为不能依赖于数据库事务的提交和回滚机制,就需要在代码中,针对每个需要实现分布式事务特性的业务操作,添加提交和回滚处理。

1. TCC模式代码实现

1.1 TCC接口定义

这里以一个业务为例说明,其它业务参考该模式实现即可。

package com.platform.account.tcc.service;

import com.platform.account.domain.Account;
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;

@LocalTCC
public interface AccountTccService {

    /**
     * Prepare boolean.
     *
     * @param actionContext the action context
     * @param account             the account
     * @param price             the price
     * @return the boolean
     */
    @TwoPhaseBusinessAction(name = "AccountTccService", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = "account") Account account,
                           @BusinessActionContextParameter(paramName = "price") Double price);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    public boolean rollback(BusinessActionContext actionContext);
}

接口需要增加一个注解@LocalTCC,接口定义了三个方法,其中prepare是第一阶段提交,commit和rollback是第二阶段提交。

1.2 TCC接口实现

package com.platform.account.tcc.service.impl;


import com.platform.account.domain.Account;
import com.platform.account.service.AccountService;
import com.platform.account.tcc.service.AccountTccService;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;


@Service
public class AccountTccServiceImpl implements AccountTccService {

    @Autowired
    private AccountService accountService;

    @Override
    public boolean prepare(BusinessActionContext actionContext, Account account, Double price) {

        String xid = actionContext.getXid();
        System.out.println("TccActionOne prepare, xid:" + xid +  ", account:" + account);
        
        int res = accountService.reduceBalance(account.getId(), price);

        if(res > 0)
        {
            return true;
        }
        else {
            return  false;
        }


    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        String xid = actionContext.getXid();
        System.out.println("TccActionOne commit, xid:" + xid + ", account:" + actionContext.getActionContext("account"));

        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {

        String xid = actionContext.getXid();
        System.out.println("TccActionOne rollback, xid:" + xid + ", account:" + actionContext.getActionContext("account"));

        // ResultHolder.setActionOneResult(xid, "R");

        return true;
    }
}

1.3 TCC接口的调用

package com.platform.account.controller;

import com.platform.account.domain.Account;
import com.platform.account.handler.AjaxResult;
import com.platform.account.service.AccountService;
import com.platform.account.tcc.service.AccountTccService;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class AccountController {


    @Autowired
    private AccountTccService accountTccService;


    /**
     * 扣减库存接口
     * @param userId
     * @param price
     */
    @PostMapping("/reduceBalance")
    AjaxResult reduceBalance(@RequestParam("userId") Long userId, @RequestParam("price")Double price)
    {

        BusinessActionContext actionContext = new BusinessActionContext();

        Account account = new Account();
        account.setId(userId);

        System.out.println("price = " + price);        

        boolean res = accountTccService.prepare(actionContext, account, price);

        return AjaxResult.success(1);
    }

}

2. TCC模式的异常处理

因为TCC模式需要在代码中处理各种异常,所以需要将各种情况考虑全面,因为在分布式环境中,出现网络超时、重发,机器宕机等一系列的异常,一旦这些异常情况没有处理,或者处理不合理,就可能导致业务数据错误。最常见的主要是这三种异常,分别是空回滚、幂等、悬挂。

2.1 空回滚

首先是空回滚。什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

3.2 幂等

接下来是幂等。幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

3.3 悬挂

最后是防悬挂。按照惯例,咱们来先讲讲什么是悬挂。悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

3. 异常控制实现

在分析完空回滚、幂等、悬挂等异常 Case 的成因以及解决方案以后,下面我们就综合起来考虑,一个 TCC 接口如何完整的解决这三个问题。

首先是 Try 方法。结合前面讲到空回滚和悬挂异常,Try 方法主要需要考虑两个问题,一个是 Try 方法需要能够告诉二阶段接口,已经预留业务资源成功。第二个是需要检查第二阶段是否已经执行完成,如果已完成,则不再执行。因此,Try 方法的逻辑可以如图所示:

先插入事务控制表记录,如果插入成功,说明第二阶段还没有执行,可以继续执行第一阶段。如果插入失败,则说明第二阶段已经执行或正在执行,则抛出异常,终止即可。

接下来是 Confirm 方法。因为 Confirm 方法不允许空回滚,也就是说,Confirm 方法一定要在 Try 方法之后执行。因此,Confirm 方法只需要关注重复提交的问题。可以先锁定事务记录,如果事务记录为空,则说明是一个空提交,不允许,终止执行。如果事务记录不为空,则继续检查状态是否为初始化,如果是,则说明一阶段正确执行,那二阶段正常执行即可。如果状态是已提交,则认为是重复提交,直接返回成功即可;如果状态是已回滚,也是一个异常,一个已回滚的事务,不能重新提交,需要能够拦截到这种异常情况,并报警。

最后是 Cancel 方法。因为 Cancel 方法允许空回滚,并且要在先执行的情况下,让 Try 方法感知到 Cancel 已经执行,所以和 Confirm 方法略有不同。首先依然是锁定事务记录。如果事务记录为空,则认为 Try 方法还没执行,即是空回滚。空回滚的情况下,应该先插入一条事务记录,确保后续的 Try 方法不会再执行。如果插入成功,则说明 Try 方法还没有执行,空回滚继续执行。如果插入失败,则认为 Try 方法正再执行,等待 TC 的重试即可。如果一开始读取事务记录不为空,则说明 Try 方法已经执行完毕,再检查状态是否为初始化,如果是,则还没有执行过其他二阶段方法,正常执行 Cancel 逻辑。如果状态为已回滚,则说明这是重复调用,允许幂等,直接返回成功即可。如果状态为已提交,则同样是一个异常,一个已提交的事务,不能再次回滚。

4. 总结

通过上述的说明,可以看出TCC模式还是比较复杂的,该模式需要对业务规则有清晰和明确的定义,只有在业务规则清楚的情况下,针对各种异常情况进行全面的处理,才能确保TCC模式下的分布式事务的正确性和完整性。

示例中只是演示了TCC模式的接口实现,并没有对各种异常信息进行处理,主要是异常处理与业务特点和规则是密切关联的。有了原则性的方向,再结合业务特点,使用TCC模式实现分布式业务处理,应该说,还是具有较高可行性的。

参考代码下载

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值