seata分布式事务~TCC模式 ~ langhai

林书豪
2023-01-10 14:35:41
seata分布式事务~TCC模式 浪海值:1397度
文章标签:seata分布式事务
文章摘要:待更新
使用新的显示器:新的显示器 如果遇到图片单击即可放大/缩小。

seata分布式事务~TCC模式

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

Try:资源的检测和预留。

Confirm:完成资源操作业务,要求Try成功Confirm一定要成功。

Cancel:预留资源释放,可以理解为try的反向操作。


优点:相比AT模型,无需生成快照,无需使用全局锁,性能最强。

不依赖数据库事务,而是依赖补偿操作,可以用于非事务性数据库。

缺点:有代码侵入,需要人为编写代码。

需要考虑人工编写的Confirm Cancel失败的情况。要做好幂等处理。


TCC模式的一些问题:

空回滚:某分支事务的try阶段阻塞,可能导致全局事务超时而触发第二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚。

业务悬挂:对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel。


实现TCC模式:

要解决空回滚以及业务悬挂的问题,就需要记录事务的状态。

可以创建一个表来记录当前事务的状态。

DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
  `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

需要声明一个TCC接口。

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;

/**
 * TCC事务的接口
 *
 * @author langhai.cc
 * @date 2023-01-10 15:00
 */
@LocalTCC
public interface AccountTCCService {

    /**
     * try方法声明 余额减少
     *
     * @param userId
     * @param money
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * confirm方法声明
     *
     * @param ctx
     * @return
     */
    boolean confirm(BusinessActionContext ctx);

    /**
     * cancel方法声明
     *
     * @param ctx
     * @return
     */
    boolean cancel(BusinessActionContext ctx);
}

TCC接口实现类

/**
 * TCC事务的接口 实现类
 *
 * @author langhai.cc
 * @date 2023-01-10 15:10
 */
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;

    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deduct(String userId, int money) {
        // 0.获取事务id
        String xid = RootContext.getXID();
        // 业务悬挂处理
        AccountFreeze oldFreeze = freezeMapper.selectById(xid);
        if(oldFreeze != null){
            return;
        }
        // 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);
        // 获取参数
        String userId = ctx.getActionContext("userId").toString();
        // 空回滚处理 判断表中是否有事务记录
        if(freeze == null){
            // 说明没有事务记录 需要处理空回滚
            freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }
        // 幂等处理
        if(freeze.getState() == AccountFreeze.State.CANCEL){
            // 已经处理过一次CANCEL,无需重复处理。
            return true;
        }

        // 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;
    }
}

当出现异常情况的时候,成功进行回滚,并在mysql中生成记录。

image.png

控制台输出:

// TCC模式
branchType=TCC

// 成功进行回滚
Branch Rollbacked result: PhaseTwo_Rollbacked

// 回滚SQL执行 余额增加回来
Preparing: update account_tbl set money = money + 200 where user_id = ? 
// 事务回滚记录
Preparing: UPDATE account_freeze_tbl SET user_id=?, freeze_money=?, state=? WHERE xid=? 


有任何问题联系 QQ 676558206 email [email protected] [email protected]


提交评论