https://seata.io/zh-cn/docs/dev/mode/tcc-mode
https://seata.io/zh-cn/docs/user/mode/tcc
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Seata中的TCC模型依然延续之前的事务架构,如图:
TCC模式的每个阶段是做什么的?
TCC的优点是什么?
TCC的缺点是什么?
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
如图:
执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?
这里我们定义一张表:
CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) NOT NULL, `user_id` varchar(255) DEFAULT NULL COMMENT '用户id', `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额', `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel', PRIMARY KEY (`xid`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
其中:
那此时,我们的业务开怎么做呢?
接下来,我们改造account-service,利用TCC实现余额扣减功能。
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,
我们在account-service项目中的com.mcode.account.service
包中新建一个接口,声明TCC三个接口:
package com.mcode.account.service; 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 { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money")int money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx); }
在account-service服务中的com.mcode.account.service.impl
包下新建一个类,实现TCC业务:
package com.mcode.account.service.impl; import com.mcode.account.entity.AccountFreeze; import com.mcode.account.mapper.AccountFreezeMapper; import com.mcode.account.mapper.AccountMapper; import com.mcode.account.service.AccountTCCService; import io.seata.core.context.RootContext; import io.seata.rm.tcc.api.BusinessActionContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j public class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { // 0.获取事务id String xid = RootContext.getXID(); // 1.扣减可用余额 accountMapper.deduct(userId, money); // 2.记录冻结金额,事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { // 1.获取事务id String xid = ctx.getXid(); // 2.根据id删除冻结记录 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { // 0.查询冻结记录 String xid = ctx.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); // 1.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 2.将冻结金额清零,状态改为CANCEL freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; } }
我们从以下几个方面来对比四种实现:
如图: