03-分布式事务框架Seata-TCC模式

本文深入探讨了Seata TCC(Try-Confirm-Cancel)事务模式,分析了其与AT模式的区别,并通过一个订单系统的例子详细展示了如何实现TCC事务,包括订单、库存和账户服务的TCC分支事务。在每个阶段,都强调了幂等性和事务控制的重要性。同时,文章还涵盖了并发场景下的处理和如何避免重复提交或回滚的问题。
摘要由CSDN通过智能技术生成

一、工作原理

TCC 与 Seata AT 事务一样都是两阶段事务,需要TC 事务协调器,TM 事务管理器,RM 资源管理器三大组件来支持全局事务的控制。
TCC 与 AT 事务的主要区别为

  1. TCC 对业务代码侵入严重,每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。
  2. TCC 效率更高,不必对数据加全局锁,允许多个事务同时操作数据。

在这里插入图片描述

1 第一阶段 Try

以账户服务为例,当下订单时要扣减用户账户金额:
在这里插入图片描述
假如用户购买 100 元商品,要扣减 100 元,TCC 事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元
在这里插入图片描述

2 第二阶段 Confirm

如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时, TC会控制当前分支事务进行提交,如果提交失败,TC 会反复尝试,直到提交成功为止。

当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作
在这里插入图片描述

3 第二阶段 Cancel

如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC 会控制当前分支事务回滚,如果回滚失败,TC 会反复尝试,直到回滚完成为止。
在这里插入图片描述

4 多个事务并发的情况

多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可
在这里插入图片描述

二、订单系统添加TCC事务

1 准备工作

第一步 :idea 新建empty工程,导入 分布式事务案例demo准备工作 创建的无事务版本项目

第二步:父项目seata依赖注释打开

第三步:订单模块order,账户模块account,库存模块storange下的application.yml统一添加事务组组名

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

第四步:订单模块order,账户模块account,库存模块storange 三个项目的resources目录下的配置文件 file.conf,registry.conf02-分布式事务框架Seata-AT模式 中完全相同 ,可直接拷贝过来
第五步: 幂等性控制
如果重复执行提交或回滚,就和执行一次结果一样
在tcc包中创建工具类 ResultHolder
这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。

ResultHolder可以为每一个全局事务保存一个标识:

public class ResultHolder {
    private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
    public static void setResult(Class<?> actionClass, String xid, String v) {
        Map<String, String> results = map.get(actionClass);

        if (results == null) {
            synchronized (map) {
                if (results == null) {
                    results = new ConcurrentHashMap<>();
                    map.put(actionClass, results);
                }
            }
        }
        results.put(xid, v);
    }
    public static String getResult(Class<?> actionClass, String xid) {
        Map<String, String> results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }
        return null;
    }

    public static void removeResult(Class<?> actionClass, String xid) {
        Map<String, String> results = map.get(actionClass);
        if (results != null) {
            results.remove(xid);
        }
    }
}

2 order启动全局事务,添加“保存订单”分支事务

在订单项目中执行添加订单
在这里插入图片描述
TCC 事务操作的代码:
Try - 第一阶段,冻结数据阶段,向订单表直接插入订单,订单状态设置为0(冻结状态)。
在这里插入图片描述
Confirm - 第二阶段,提交事务,将订单状态修改成1(正常状态)
在这里插入图片描述
Cancel - 第二阶段,回滚事务,删除订单
在这里插入图片描述
OrderMapper接口及OrderMapper.xml修改
根据前面的分析,订单数据操作有以下三项:创建订单,修改订单状态,删除订单

public interface OrderMapper extends BaseMapper<Order> {
    void create(Order order);//创建订单
    //修改状态
    void updateStatus(Long id,Integer status);
    //删除订单,使用继承的deleteById()
}
    <insert id="create">
        INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
        VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money},#{status});
    </insert>
    <update id="updateStatus">
        update `order` set status=#{status} where id=#{id}
    </update>
     <delete id="deleteById">
        delete from  `order` where id=#{id}
    </delete>

Seata 实现 TCC 操作需要定义一个接口,在 cn.tedu.order.tcc包下创建OrderTccAcction操作接口

@LocalTCC
public interface OrderTccAcction {
    /**
     * TwoPhaseBusinessAction 第一阶段方法里面已定义 默认第二阶段方法commit,rollback
     * 第二阶段方法名是默认值无需配置,不是默认可通过注解指定第二阶段的方法
     * 为了避开seata的bug,这里不使用封装的Order对象,而是一个一个的单独传递每一个值
     *
     * BusinessActionContext上下文对象,用来向第二阶段传递数据
     * @BusinessActionContextParameter用来把参数放入上下文对象
     */
    @TwoPhaseBusinessAction(name = "OrderTccAcction")
    boolean prepare(BusinessActionContext ctx, @BusinessActionContextParameter(paramName = "orderId") Long orderId, Long userId, Long productId, Integer count, BigDecimal money);
    //第二阶段提交
    boolean commit(BusinessActionContext ctx);
    //第二阶段回滚
    boolean rollback(BusinessActionContext ctx);
}

添加OrderTccAcction的实现类OrderTccAcctionImpl

@Component
public class OrderTccAcctionImpl implements OrderTccAcction{
    @Autowired
    private OrderMapper orderMapper;

    @Transactional//控制本地事物
    @Override
    public boolean prepare(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
        System.out.println("创建 order 第一阶段,预留资源 - "+businessActionContext.getXid());
        orderMapper.create(new Order(orderId, userId, productId, count, money, 0));
        //事务成功,保存一个标识,供第二阶段进行判断
         ResultHolder.setResult(OrderTccAcction.class,ctx.getXid(),"p");
        return true;
    }

    @Transactional//控制本地事物
    @Override
    public boolean commit(BusinessActionContext ctx) {
     // 防止幂等性,如果commit阶段重复执行则直接返回
     
    if(ResultHolder.getResult(OrderTccAcction.class,ctx.getXid())==null)return true;
        //ctx对象先变成json,在向第二阶段传递,会丢失类型信息,取出的orderId可能是Integer或Long
        orderMapper.updateStatus((long)ctx.getActionContext("orderId"), 1);
        //提交成功是删除标识
        ResultHolder.removeResult(OrderTccAcction.class,ctx.getXid());
        return true;
    }

    @Transactional//控制本地事物
    @Override
    public boolean rollback(BusinessActionContext ctx) {
     //第一阶段没有完成的情况下,不必执行回滚,因为第一阶段有本地事务,事务失败时已经进行了回滚。
        //如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
        //幂等性控制:如果重复执行回滚则直接返回
    if(ResultHolder.getResult(OrderTccAcction.class,ctx.getXid())==null)return true;
        orderMapper.deleteById((long)ctx.getActionContext("orderId"));
        ResultHolder.removeResult(OrderTccAcction.class,ctx.getXid());
        return true;
    }
}

在业务代码中调用 Try 阶段方法
业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepare(),并添加全局事务注解 @GlobalTransactional
OrderServiceImpl类修改

    @Autowired
    private OrderTccAcction orderTccAcction;

    @GlobalTransactional//启动全局事务,OrderTccAcctionImpl中已开启了本地事物
    @Override
    public void create(Order order) {
        order.setId(Long.valueOf(easyIdClient.nextId("order_business")));
        //不直接完成业务数据操作orderMapper.create(order),而是调用tcc的第一阶段方法
        /**
         * orderTccAcction实例,是动态代理对象,用AOP添加了切面代码,创建上下文对象,然后传入原始方法
         */
        orderTccAcction.prepare(null, order.getId(), order.getUserId(), order.getProductId(), order.getCount(), order.getMoney());

        //后面两个模块没有完成,无法启动,先注释掉
        // TODO: 远程调用库存storage,减少库存
        //storageClient.decrease(order.getProductId(), order.getCount());
        // TODO: 远程调用账户account,扣减账户
        //accountClient.decrease(order.getUserId(), order.getMoney());
    }

启动服务 访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试,观察控制台日志

3 storage添加减少库存分支事务

在库存项目中执行减少库存
在这里插入图片描述
我们要添加以下 TCC 事务操作的代码
Try - 第一阶,冻结数据阶段,将要减少的库存量先冻结
在这里插入图片描述
Confirm - 第二阶段,提交事务,使用冻结的库存完成业务数据处理
在这里插入图片描述
Cancel - 第二阶段,回滚事务,冻结的库存解冻,恢复以前的库存量
在这里插入图片描述
根据前面的分析,库存数据操作有以下三项:冻结库存,冻结库存量修改为已售出量,解冻库存
StorageMapper添加以下方法

    //查询商品库存,用来判断是否有足够的库存
    Storage findByProductId(Long productId);
    //可用---》冻结
    void updateResidueToFrozen(Long productId, Integer count);
    //提交时,冻结--》已售出
    void updateFrozenToUsed(Long productId, Integer count);
    //回滚时,冻结--->可用
    void updateFrozenToResidue(Long productId, Integer count);

StorageMapper.xml文件修改

    <select id="findByProductId" resultMap="BaseResultMap">
      select * from storage where product_id=#{productId}
    </select>

    <update id="updateResidueToFrozen">
        update storage set residue = residue - #{count} ,frozen=frozen + #{count} WHERE product_id = #{productId}
    </update>
    <update id="updateFrozenToUsed">
        update storage set used = used + #{count} ,frozen=frozen - #{count} WHERE product_id = #{productId}
    </update>
    <update id="updateFrozenToResidue">
        update storage set residue = residue + #{count} ,frozen=frozen - #{count} WHERE product_id = #{productId}
    </update>

在cn.tedu.storage.tcc 添加 TCC 接口StorageTccAction

@LocalTCC
public interface StorageTccAction {
    @TwoPhaseBusinessAction(name = "StorageTccActopn")
    boolean prepare(BusinessActionContext cxt, @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "count")  Integer count);
    boolean commit(BusinessActionContext cxt);
    boolean rollback(BusinessActionContext cxt);
}

实现类

@Component
@Slf4j
public class StorageTccActionImpl implements StorageTccAction{
    @Autowired
    private StorageMapper storageMapper;

    @Transactional
    @Override
    public boolean prepare(BusinessActionContext cxt, Long productId, Integer count) {
        log.info("减少商品库存,第一阶段,锁定减少的库存量,productId="+productId+", count="+count);
        Storage storage = storageMapper.findByProductId(productId);
        if(storage.getResidue()<count){
            throw new RuntimeException("库存不足");
        }
        storageMapper.updateResidueToFrozen(productId, count);
        //一阶段成功,添加成功标记:StorageTccAction.class-->事务id--->"p"
        ResultHolder.setResult(StorageTccAction.class,cxt.getXid(),"p");
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext cxt) {
        //二阶段执行之前,判断如果没有标记,则不执行二阶段操作,避免二阶段操作重复执行
        if(ResultHolder.getResult(StorageTccAction.class,cxt.getXid())==null) return true;     storageMapper.updateFrozenToUsed(Long.getLong(cxt.getActionContext("productId").toString()),Integer.parseInt(cxt.getActionContext("productId").toString()));
        //第二阶段完成时,删除标记
        ResultHolder.removeResult(StorageTccAction.class,cxt.getXid());
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext cxt) {
        //二阶段执行之前,判断如果没有标记,则不执行二阶段操作,避免二阶段操作重复执行
        if(ResultHolder.getResult(StorageTccAction.class,cxt.getXid())==null) return true;     storageMapper.updateFrozenToResidue(Long.getLong(cxt.getActionContext("productId").toString()),Integer.parseInt(cxt.getActionContext("productId").toString()));
        //第二阶段完成时,删除标记
        ResultHolder.removeResult(StorageTccAction.class,cxt.getXid());
        return true;
    }
}

同理:订单模块也需要相应加幂等性代码,这里不重复演示啦

在业务代码中调用 Try 阶段方法 StorageServiceImpl类修改

@Service
public class StorageServiceImpl implements StorageService{
    /*@Autowired
    private StorageMapper storageMapper;*/
    @Autowired
    private StorageTccAction storageTccAction;

    @Override
    public void decrease(Long productId, Integer count){
        storageTccAction.prepare(null, productId, count);
    }
}

打开订单服务的远程调用仓库服务的方法的注释

启动服务,调用保存订单地址: http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试

4 account添加扣减金额分支事务

扣减金额 TCC 事务分析本文开头说TCC工作原理就介绍了流程,详情见目录一

根据前面的分析,库存数据操作有三项:1.冻结库存 2.冻结库存量修改为已售出量 3.解冻库存

在 AccountMapper 中添加方法

  //查询账户
    Account findByUserId(Long userId);
    //可用--->冻结
    void updateResidueToFrozen(Long userId,BigDecimal money);
    //冻结-->已消费
    void updateFrozenToUsed(Long userId,BigDecimal money);
    //冻结--->可用
    void updateFrozenToResidue(Long userId,BigDecimal money);

AccountMapper .xml的sql添加

    <select id="findByUserId" resultMap="BaseResultMap">
         select * from account where user_id = #{userId};
    </select>
    <update id="updateResidueToFrozen">
        update account set residue=residue-#{money},frozen=frozen+#{money} where user_id = #{userId};
    </update>
    <update id="updateFrozenToUsed">
        update account set used=used+#{money},frozen=frozen-#{money} where user_id = #{userId};
    </update>
    <update id="updateFrozenToResidue">
        update account set residue=residue+#{money},frozen=frozen-#{money} where user_id = #{userId};
    </update>

在cn.tedu.account.tcc下建AccountTccAction接口

@LocalTCC
public interface AccountTccAction {
    @TwoPhaseBusinessAction(name = "AccountTccAction")
    boolean prepare(BusinessActionContext ctx, @BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "money") BigDecimal money);
    boolean commit(BusinessActionContext ctx);
    boolean rollback(BusinessActionContext ctx);
}

实现类

@Component
public class AccountTccActionImpl implements AccountTccAction {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public boolean prepare(BusinessActionContext ctx, Long userId, BigDecimal money) {
        Account accout = accountMapper.findByUserId(userId);
        if (accout.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("可用金额不足");
        }
        accountMapper.updateResidueToFrozen(userId, money);
        ResultHolder.setResult(AccountTccAction.class, ctx.getXid(), "p");
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext ctx) {
        //防止重复提交
        if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) return true;
        accountMapper.updateFrozenToUsed(Long.valueOf(ctx.getActionContext("userId").toString()), new BigDecimal(ctx.getActionContext("money").toString()));
        //删除标识
        ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        //防止重复回滚
        if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) return true;
        accountMapper.updateFrozenToResidue(Long.valueOf(ctx.getActionContext("userId").toString()), new BigDecimal(ctx.getActionContext("money").toString()));
        //删除标识
        ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
        return true;
    }
}

在业务代码中调用 Try 阶段方法
AccountServiceImpl 修改

    @Autowired
    private AccountTccAction accountTccAction;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountTccAction.prepare(null, userId, money);
        //accountMapper.decrease(userId, money);
    }

订单模块远程调用账户的注释打开,启动服务, 访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值