SpringCloud Alibaba Seata TCC 模式讲解与使用

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud Alibaba Seata TCC 模式讲解与使用相关的知识,希望对你有一定的参考价值。

一、Seata TCC 模式

好长时间没有更新文章了,最近由于换工作,工作交接实在没有腾出时间来写文章,正好今天有时间我们把前面没有讲到的Seata TCC 模式给演示下如何使用,在看本篇文章前最好已经了解了seataAT模式,因为个人感觉TCC的出现一大部分弥足了AT模式的不足,但同时也把事物的提交和回滚的控制权都交给了开发者处理,相比于AT模式,我们要写提交及回滚的逻辑,这样显而易见,对代码的侵入性就比较强了。那既然如此为什么还要用TCC模式呢?

下面我就要说下AT模式的一大不足之处了,看过我前面演示的AT模式的,仔细观察下应该可以发现一个问题,就是A 调用 B ,B执行结束,A未结束的时候,B 写入数据库的本地事物其实已经提交了,只是将数据也记了一份在 undo_log 表中,后期回滚也是通过 undo_log表中的数据定位写入的数据再进行恢复,那假如期间有其他事物对B 新写入的数据进行修改或做其他操作,都有脏读的可能,虽然seata官网也提供了一种解决方案,就是在查询的SQL中加上for update,但这样在有些情况下效率无疑降低了许多。比如B 是做的修改操作,而下面有查询操作在进行,那此时的查询会因为行锁一直在等待,直到另一个事物结束。

那现在就要看TCC模式是怎么一个过程了,其实TCC和AT在模式上都是基于2pc 两阶段提交协议的,只不过 TCC,不会帮我们自动提交或回滚数据,而是通过事件的方式进行通知,开发者在相应的通知中,根据自己的逻辑实现提交或回滚操作,就如下图的过程:

既然提交和回滚都给我们处理了,那我们就可以定制化一些场景的操作了,比如在做数据新增的场景,我们可以在数据库中添加一个标识,比如 -1 标识未提交的数据,1标识已提交的数据,这样我们在写入数据库中该标识置为-1 ,其他查询的也过滤到标识为 -1 的数据,这样就可以避免其他脏读的出现。

那对于修改数据的场景呢,其实这种情况就要看自己的业务逻辑了,如果是强一致的情况,那可以使用乐观锁,采用重试的方式等待获取到最新的数据。如果不用强一致,类似于mysql每次读取的都是已经提交过的数据,那我们也可以仿照mysql mvcc模型,搞个多版本机制,搞一个镜像表或者存在某个可以获取到的地方,在更新时,将主表的状态标识置为更新中,将更新后的数据写入镜像表,此时如果有读的操作,也是读的主表的事物前的数据,待事物提交后,将主表中的数据进行修改。

上面说了那么多,下面来实践下TCC模式的使用吧,还是接着上篇 seata 文章的场景,现在换成使用TCC模式,下面是上篇文章的地址:

https://blog.csdn.net/qq_43692950/article/details/122155924

对于环境了配置,本篇文章就不再演示了,看上面这篇文章就足够了。

在开始前先回顾下上篇的场景:

有两个服务分别为order 订单 和 stock 库存服务,在下单时调用order 订单服务,order 订单服务首先调用 stock 库存服务扣取库存,然后再生成订单,整个过程要么全成功,要么全失败,一个简易的分布式事物场景。

二、TCC 实践

对于上面的场景,我们可以简单设计为再生成订单的表中添加一个状态 status ,值为-1 则表示未提交的数据,值为 1 则表示已提交的数据。对于扣除库存,可以先扣除,再回滚的时候再把扣除的加上来,主要还是演示TCC的功能。

对于seata 中的TCC的实现,可以和AT模式进行对比。再AT模式,我们只需加一个@GlobalTransactional 注解即可,在 TCC 中我们也要加上@GlobalTransactional ,只不过对于数据库的操作我们需要声明到另一个接口实现中,并且需要在接口上加上@LocalTCC 表示走TCC模式,其中我们需要声明一个操作业务逻辑的方法,并在该方法上加上@TwoPhaseBusinessAction表示业务处理的方法,其中 name 参数表示全局事务注解名称。commitMethodrollbackMethod 就很好理解了,分别表示提交和回滚的通知方法。

其中Seata 的 TCC还为我们提供了一个BusinessActionContext 类,该类中有一个 Map类型的actionContext 对象,我们可以向其中存储一些业务数据,比如订单ID,在回滚和提交方法中也通过该类获取到 ID 然后再继续自定义的逻辑。在这里为了方便我们想Map中存储一些自定义的参数,还为我们提供了@BusinessActionContextParameter注解,被标注的可以直接放在actionContext中。

下面开始实践一下:

订单服务修改:

首先修改下order-info表,加上status字段,其中 -1 表示事物未提交的数据,1 表示已提交的数据:

在订单服务中我们需要将订单写入数据库的操作抽出来一个接口实现,并在接口中添加上TCC 的注解:

@LocalTCC
public interface GenOrderManager 

    @TwoPhaseBusinessAction(name = "order", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "id") Long id, String name, Integer bugName);

    boolean commit(BusinessActionContext context);

    boolean rollback(BusinessActionContext context);

在实现中编写业务逻辑和回滚操作:

@Slf4j
@Component
public class GenOrderManagerImpl implements GenOrderManager 
    @Autowired
    OrderDao orderDao;

    @Override
    public boolean prepare(BusinessActionContext actionContext, Long id, String name, Integer bugName) 
        OrderEntity entity = new OrderEntity();
        entity.setOrderId(id);
        entity.setOrderName(name);
        entity.setBuyCount(bugName);
        entity.setStatus(-1);
        int insert = orderDao.insert(entity);
        return insert > 0;
    

    @Override
    public boolean commit(BusinessActionContext context) 
        Long id = Long.parseLong(String.valueOf(context.getActionContext("id")));
        log.info("TCC 事物提交!id = ", id);
        OrderEntity entity = new OrderEntity();
        entity.setStatus(1);
        int update = orderDao.update(entity, new LambdaQueryWrapper<OrderEntity>()
                .eq(OrderEntity::getOrderId, id));
        log.info("TCC 事物提交修改状态:", update);
        return true;
    

    @Override
    public boolean rollback(BusinessActionContext context) 
        Long id = Long.parseLong(String.valueOf(context.getActionContext("id")));
        log.info("TCC 事物回滚!id = ", id);
        int delete = orderDao.delete(new LambdaQueryWrapper<OrderEntity>()
                .eq(OrderEntity::getOrderId, id));
        log.info("TCC 事物回滚!,删除数据: ", delete);
        return true;
    

主的 Service 业务端的代码为:

@Service
public class TccOrderServiceImpl implements TccOrderService 
    @Autowired
    OrderDao orderDao;

    @Autowired
    TccStockClient tccStockClient;

    @Autowired
    GenOrderManager genOrderManager;

    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public boolean order(Long id, String name, Integer bugName) 
        ResponseTemplate responseTemplate = tccStockClient.subStock(id, bugName);
        boolean result = false;
        if (responseTemplate.getCode() == 200) 
            result = genOrderManager.prepare(null, id, name, bugName);
            int a = 1 / 0;
        
        return result;
    

其中 tccStockClient 就是库存服务,其中代码可参考上篇的seata文章。这里接口进来会执行order方法,在该方法中首先调用库存服务扣除库存,然后再生成定在存在数据库中,这里故意写了个 1/0 的错误,注意这里需要加上@GlobalTransactional注解。

测试请求接口为:

@RestController
@RequestMapping("/tcc")
public class TccOrderController 
    @Autowired
    TccOrderService orderService;

    @GetMapping("/order")
    public ResponseTemplate order() 
        return orderService.order(1L, "商品A", 2) ?
                ResSuccessTemplate.builder().build() :
                ResFailTemplate.builder().build();
    

这里主要演示功能,直接写死了给 ID 为 1 的商品下订单,并扣除库存 2 个。

库存服务修改:

下面修改库存服务,同理需要抽象出一个接口实现取扣除库存,同样需要加上TCC 的注解:

@LocalTCC
public interface SubStockManager 

    @TwoPhaseBusinessAction(name = "SubStock", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "id") Long id, @BusinessActionContextParameter(paramName = "subCount") Integer subCount);

    boolean commit(BusinessActionContext context);

    boolean rollback(BusinessActionContext context);


业务实现类:

@Slf4j
@Component
public class SubStockManagerImpl implements SubStockManager 
    @Autowired
    StockDao stockDao;

    @Override
    public boolean prepare(BusinessActionContext actionContext, Long id, Integer subCount) 
        return stockDao.subStock(id, subCount) > 0;
    

    @Override
    public boolean commit(BusinessActionContext context) 
        log.info("TCC 事物提交!");
        return true;
    

    @Override
    public boolean rollback(BusinessActionContext context) 
        log.info("TCC 事物回滚!");
        Long id = Long.parseLong(String.valueOf(context.getActionContext("id")));
        Integer subCount = (Integer) context.getActionContext("subCount");
        Integer integer = stockDao.addStock(id, subCount);
        log.info("TCC 事物回滚,修改数据:", integer);
        return true;
    

StockDao:

@Mapper
@Repository
public interface StockDao extends BaseMapper<StockEntity> 

    @Update("update stock_info set count=(count-#subCount) where id = #id and (count-#subCount)>=0")
    Integer subStock(@Param("id") Long id, @Param("subCount") Integer subCount);

    @Update("update stock_info set count=(count+#subCount) where id = #id ")
    Integer addStock(@Param("id") Long id, @Param("subCount") Integer subCount);

在这里扣除库存的操作,直接在业务逻辑中直接扣除了,如果后面出发回滚操作,再进行将库存加上来,以简单的业务主要来演示TCC的实践,下面在主 Service 中,同样和上面订单服务一样,在@GlobalTransactional注解的方法上再调用TCC 的接口:

@Service
public class TccStockServiceImpl implements TccStockService 

    @Autowired
    SubStockManager subStockManager;

    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public boolean subStock(Long id, Integer subCount) 
        return subStockManager.prepare(null, id, subCount);
    


测试接口为:

@RestController
@RequestMapping("/tcc")
public class TccStockController 
    @Autowired
    TccStockService tccStockService;

    @Autowired
    StockDao stockDao;

    @PutMapping("/stock")
    public ResponseTemplate subStock(@RequestParam("id") Long id, @RequestParam("subCount") Integer subCount) 
        return tccStockService.subStock(id, subCount) ?
                ResSuccessTemplate.builder().build() :
                ResFailTemplate.builder().build();
    

这里主要是接受订单服务传递过来的参数,然后扣除库存,返回状态。

测试:

下面启动订单和库存服务,首先我们在库存表stock_info中,添加一个 ID 为 1 的商品,设库存量为 100 :

调用 /tcc/order 接口,在制定完库存服务的地方打个断点:

库存服务返回成功,查看数据库是否已经扣除库存:

已经扣除了两个库存,下面再走到写入订单的逻辑:

这里可以看到已经生成了一个订单,查看order-info 数据库中的数据:

此时我们定义的status字段就起作用了,表示这是一条未提交事物的数据,再向下执行,触发 1/0 的运行时异常,从打印的日志,可以看出回滚情况:


下面看库存服务:

然后再查看数据库中的数据,首先看库存表中的库存:

已经恢复了扣除的 2 个,在看订单表:

刚才预写入的数据也被移除了,到此seata 的 TCC 模式就已经实践完成了。


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

以上是关于SpringCloud Alibaba Seata TCC 模式讲解与使用的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud Alibaba 使用Seata解决分布式事物

SpringCloud Alibaba Seata处理分布式事务及示例Demo

SpringCloud Alibaba Docker 安装 Seata Server集群

SpringCloud - Spring Cloud Alibaba 之 Seata分布式事务服务;TCC事务模式机制(二十三)

SpringCloud Alibaba Seata TCC 模式讲解与使用

SpringCloud - Spring Cloud Alibaba 之 Seata分布式事务服务;集成Nacos配置中心(十九)