• Spring Cloud 系列:Seata 中TCC模式具体实现


    概述

    https://seata.io/zh-cn/docs/dev/mode/tcc-mode

    https://seata.io/zh-cn/docs/user/mode/tcc

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

    • Try:资源的检测和预留;
    • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
    • Cancel:预留资源释放,可以理解为try的反向操作。

    Seata的TCC模型

    Seata中的TCC模型依然延续之前的事务架构,如图:

    image

    优缺点

    TCC模式的每个阶段是做什么的?

    • Try:资源检查和预留
    • Confirm:业务执行和提交
    • Cancel:预留资源的释放

    TCC的优点是什么?

    • 一阶段完成直接提交事务,释放数据库资源,性能好
    • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
    • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

    TCC的缺点是什么?

    • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
    • 软状态,事务是最终一致
    • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

    事务悬挂和空回滚

    1)空回滚

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

    如图:

    image

    执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

    2)业务悬挂

    对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂

    执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

    实现TCC模式

    决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?

    1)思路分析

    这里我们定义一张表:

    CREATE TABLE `account_freeze_tbl` (
      `xid` varchar(128NOT NULL,
      `user_id` varchar(255DEFAULT NULL COMMENT '用户id',
      `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
      `state` int(1DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
      PRIMARY KEY (`xid`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
    
    

    其中:

    • xid:是全局事务id
    • freeze_money:用来记录用户冻结金额
    • state:用来记录事务状态

    那此时,我们的业务开怎么做呢?

    • Try业务:
      • 记录冻结金额和事务状态到account_freeze表
      • 扣减account表可用金额
    • Confirm业务
      • 根据xid删除account_freeze表的冻结记录
    • Cancel业务
      • 修改account_freeze表,冻结金额为0,state为2
      • 修改account表,恢复可用金额
    • 如何判断是否空回滚?
      • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
    • 如何避免业务悬挂?
      • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

    接下来,我们改造account-service,利用TCC实现余额扣减功能。

    2)声明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);
    }
    

    3)编写实现类

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

    四种模式对比

    我们从以下几个方面来对比四种实现:

    • 一致性:能否保证事务的一致性?强一致还是最终一致?
    • 隔离性:事务之间的隔离性如何?
    • 代码侵入:是否需要对业务代码改造?
    • 性能:有无性能损耗?
    • 场景:常见的业务场景

    如图:

    image

  • 相关阅读:
    1.java环境搭建与eclipse安装和配置
    wordpress插件-免费的wordpress全套插件
    SpringIOC源码解析
    JavaEE平台技术——Spring和Spring Boot
    MySQL产生死锁原因
    同位语【语法笔记】
    时序预测 | Matlab灰色-马尔科夫预测
    数据库连接池与Druid【后端 16】
    手机怎么把照片转JPG格式?这三种手机小技巧需要知道
    Object.defineProperty()方法详解,了解vue2的数据代理
  • 原文地址:https://www.cnblogs.com/vic-tory/p/17980860