微服务seata 1.4.2 分布式事务TCC模式示例
seata 1.4.2 分布式事务TCC模式示例,seata TCC模式和AT模式的基础环境是一样的,只是在实现方式上有所区别,而且TCC模式还可以和AT模式混合使用。
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)