Java小强个人技术博客站点    手机版
当前位置: 首页 >> 软件 >> Seata之TCC 模式的使用

Seata之TCC 模式的使用

18050 软件 | 2022-2-8

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。


Windows上安装Seata服务http://www.javacui.com/tool/622.html 

Windows安装Nacoshttp://www.javacui.com/opensource/603.html 

Seata之XA 模式的使用http://www.javacui.com/java/623.html 

Seata之AT 模式的使用http://www.javacui.com/java/624.html 

官方说明http://seata.io/zh-cn/docs/dev/mode/tcc-mode.html 


一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

一阶段 prepare 行为

二阶段 commit 或 rollback 行为

seata_tcc-1.png

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.

AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:

一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。

二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。

二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。

二阶段 commit 行为:调用 自定义 的 commit 逻辑。

二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中


所以,使用TCC模式时,需要自定义编码,这时seata将对代码产生侵入性,另外不是所有业务都适合TCC模式,一般是需要进行扣减或者新增数量时使用。

基于之前的XA和AT模式,需要再新建一张表

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;


基本流程为,先执行扣减操作,然后记录一个日志,该日志以事物ID为区分,如果整体事物成功,则执行confirm,如果失败则执行cancel。

这时还需要注意的问题是,可能这边还没有执行业务,但是seata认定整体失败,要开始调动cancel了,这时要处理空回滚幂等性问题。

也可能调用了cancel,此时堵塞的程序又开始执行正常业务逻辑,这时需要注意不能重复执行


编写接口,注意相关注解的使用

package cn.itcast.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);
}


实现类

package cn.itcast.account.service.impl;

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.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;

@Slf4j
@Service
public class AccountTccServiceImpl implements AccountTccService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    public void deduct(String userId, int money) {
        String xid = RootContext.getXID();
        // 查询冻结记录,如果有,就是cancel执行过,不能继续执行
        AccountFreeze oldfreeze = freezeMapper.selectById(xid);
        if (oldfreeze != null){
            return;
        }
        // 扣除
        accountMapper.deduct(userId, money);
        // 记录
        AccountFreeze freeze = new AccountFreeze();
        freeze.setXid(xid);
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        String xid = ctx.getXid();
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        String xid = ctx.getXid();
        // 查询冻结记录
        AccountFreeze freeze = freezeMapper.selectById(xid);
        if(null == freeze){
            // try没有执行,需要空回滚
            freeze = new AccountFreeze();
            freeze.setXid(xid);
            freeze.setUserId(ctx.getActionContext("userId").toString());
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freezeMapper.insert(freeze);
            return true;
        }

        // 幂等判断
        if(freeze.getState() == AccountFreeze.State.CANCEL){
            return true;
        }

        // 恢复金额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}


如果你调用一个失败的请求,这时在account_freeze_tbl表会产生一条记录。另外,各个模块中,AT和TCC模式是可以混用。


下载示例代码

seata-demo.zip

END

推荐您阅读更多有关于“ Seata Feign SpringCloud AT TCC ”的文章

上一篇:SpringBoot启动时自动执行方法的方式 下一篇:Seata之AT 模式的使用

猜你喜欢

发表评论: