Dubbo学习系列之十四(Seata分布式事务方案AT模式)

一直说写有关最新技术的文章,但前面似乎都有点偏了,只能说算主流技术,今天这个主题,我觉得应该名副其实。分布式微服务的深水区并不是单个微服务的设计,而是服务间的数据一致性问题!解决了这个问题,才算是把分布式正式收编了!但分布式事务解决方案并没有统一的标准,只能说根据业务特点来适配,有实时的,非实时的,同步或异步的,之前已经实现了异步MQ的分布式事务方案,今天来看看Seata方案,自19年初才推出,还几易其名,目前还不算特别完善,但其光环太耀眼,作为一名IT人,还是有必要来瞧一瞧的。单说Seata,就有AT、TCC、Saga和XA模式,看来是盘大菜。

**工具:**

Idea201902/JDK11/Gradle5.6.2/Mysql8.0.11/Lombok0.26/Postman7.5.0/SpringBoot2.1.9/Nacos1.1.3/Seata0.8.1/SeataServer0.8.1/Dubbo2.7.3

**难度:**
新手--战士--老兵--大师

**目标:**

1.多模块微服务Dubbo框架整合Seata实现分布式事务的AT模式

2.使用Seata实现订单模块与其他模块的关联型事务的TCC模式
***

**步骤:**

**为了更好的遇到各种问题,同时保持时效性,我尽量使用最新的软件版本。代码地址:其中的day17,https://github.com/xiexiaobiao/dubbo-project.git**

1.先照搬来点背景材料,分布式事务典型场景如下图,一个business主事务发起多个分支事务,并需要保证一致的commit或rollback:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第1张图片

 

 

Seata框架,有三个模块,分别是

  • - TM-TransactionManager事务管理器:定义全局事务的范围,开启、提交或回滚全局事务;
  • - RM -ResourceManager资源管理器:管理分支事务的资源,注册分支事务到TC,与TC通信反馈分支事务状态,驱动分支事务提交或回滚;
  • - TC-TransactionCoordinator事务协调器:维护全局和分支事务,驱动全局提交或回滚;

Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第2张图片

 分布式事务流程:

I. TM 请求TC 发起一个全局事务,同时TC生成一个 XID作为全局事务ID.

II. XID将分发给事务调用链上的所有微服务.

III. RM响应全局事务XID向TC注册本地分支事务.

IV. TM向TC发出提交或回滚全局事务XID的请求.

V. TC响应全局事务XID,驱动所有分支事务提交或 回滚本地分支事务.

其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

再说seata的AT模式:AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

  • - 在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第3张图片

  

  • - 二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第4张图片

  

  • - 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第5张图片

 

 2.为了单一化技术点,我直接新建一个gradle项目,以官方例子为原型做抽取制作,模拟电商业务,整体架构为多模块微服务Dubbo框架,建立5个module,common为公共模块,account为用户账户处理,order为订单处理,storage为库存处理,business为业务处理,整体的处理逻辑为第一图。

 

3.在build.gradle中引入依赖,强烈建议边写代码边逐步引入,比如使用到druid才加入druid的依赖,这样才能知道每个依赖的作用和用法。

 

4.建表,项目文件中已有SQL.script,几个业务模块的对应的表,比较简单,略。重点关注下undo_log,此表为MQ存储事务执行前后的日志表,为**AT模式所必须**,用于事务提交和回滚,其中最关键字段即xid(全局事务ID)和branch_id(分支事务ID)。另外,我将各模块DB独立,是为了模拟分布式DB环境。

 

5.使用common模块的mbg快速生成各模块的Entity、Service、Impl、Mapper、Dao和Controller,可参考往期文章《》。注意每次生成时,需修改配置。

 

6.common模块:放公共的对象,如全局Enum,Exception,Dto等,还有Dubbo的接口。

 

7.storage模块:`com.biao.mall.storage.conf.SeataAutoConfig`进行Seata配置:

  • - 先通过SpringBoot自动取得DataSourceProperties,并获取JDBC的连接信息;
  • - 注入Druid连接池对象,并对DruidDataSource做属性设置,如线程池参数,超时参数等;
  • - 注入RM的DataSourceProxy代理,来代理DruidDataSource;
  • - 初始化Mybatis的SqlSessionFactory,这里使用的是DataSourceProxy实参,并将Mapper文件加入,映射Entity和Table;
  • - 分支通过GlobalTransactionScanner来扫描XID,并注册本地事务;
@Configuration
public class SeataAutoConfig {

    private DataSourceProperties dataSourceProperties;

    @Autowired
    public SeataAutoConfig(DataSourceProperties dataSourceProperties){
        this.dataSourceProperties = dataSourceProperties;
    }

    /**
     * init durid datasource
     * @Return: druidDataSource  datasource instance
     */
    @Bean
    @Primary
    public DruidDataSource druidDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());
        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setInitialSize(0);
        druidDataSource.setMaxActive(180);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMinIdle(0);
        druidDataSource.setValidationQuery("Select 1 from DUAL");
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(25200000);
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(1800);
        druidDataSource.setLogAbandoned(true);
        return druidDataSource;
    }

    /**
     * init datasource proxy
     * @Param: druidDataSource  datasource bean instance
     * @Return: DataSourceProxy  datasource proxy
     */
    @Bean
    public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
        return new DataSourceProxy(druidDataSource);
    }

    /**
     * init mybatis sqlSessionFactory
     * @Param: dataSourceProxy  datasource proxy
     * @Return: DataSourceProxy  datasource proxy
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxy);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:/mapper/*Mapper.xml"));
        factoryBean.setTransactionFactory(new JdbcTransactionFactory());
        return factoryBean.getObject();
    }

    /**
     * init global transaction scanner
     * @Return: GlobalTransactionScanner
     */
    @Bean
    public GlobalTransactionScanner globalTransactionScanner(){
        return new GlobalTransactionScanner("${spring.application.name}", "my_test_tx_group");
    }
}

 

com.biao.mall.storage.dubbo.StorageDubboServiceImpl:Dubbo微服务storage的具体实现,@Service注解为com.apache.dubbo.config.annotation.Service,将该服务注册到注册中心,本项目注册中心使用Nacos,不是ZK。

@Service(version = "1.0.0",protocol = "${dubbo.protocol.id}",
        application = "${dubbo.application.id}",registry = "${dubbo.registry.id}")
public class StorageDubboServiceImpl implements StorageDubboService {

    @Autowired
    private ProductService  productService;

    @Override
    public ObjectResponse decreaseStorage(CommodityDTO commodityDTO) {
        System.out.println("全局事务id :" + RootContext.getXID());
        return productService.decreaseStorage(commodityDTO);
    }
}

另外注意, `com.biao.mall.storage.impl.ProductServiceImpl`中,这里的本地方法,并不需要@Transactional注解。

 

8.account模块和order模块和storage模块类似,只是order模块中`com.biao.mall.order.impl.OrdersServiceImpl`多了一个通过@Reference调用account服务的注解,其他,略。

 

9.business模块:SeataAutoConfig中因无本地事务,只需一个GlobalTransactionScanner,BusinessServiceImpl中:

  • - 类注解@Service是Spring的注解;
  • - 通过@Reference从注册中心获取storage和order服务;
  • - handleBusiness方法上通过@GlobalTransactional发起全局事务,方法内就是具体使用storage和order服务;
@Service
public class BusinessServiceImpl implements BusinessService {

    @Reference(version = "1.0.0")
    private StorageDubboService storageDubboService;

    @Reference(version = "1.0.0")
    private OrderDubboService orderDubboService;

    private boolean flag;

    @Override
    @GlobalTransactional(timeoutMills = 30000,name = "dubbo-seata-at-springboot")
    public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
        System.out.println("开始全局事务,XID = " + RootContext.getXID());
        ObjectResponse objectResponse = new ObjectResponse<>();
        //1,减库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse storageResponse = storageDubboService.decreaseStorage(commodityDTO);
        //2,创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse response = orderDubboService.createOrder(orderDTO);

        //打开注释测试事务发生异常后,全局回滚功能
//        if (!flag) {
//            throw new RuntimeException("测试抛异常后,分布式事务回滚!");
//        }
        if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
            throw new DefaultException(RspStatusEnum.FAIL);
        }

        objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
        objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
        objectResponse.setData(response.getData());
        return objectResponse;
    }
}  
  
 

 

10.写个BusinessController的方法,用于测试:

    @PostMapping("/buy")
    ObjectResponse handleBusiness(@RequestBody BusinessDTO businessDTO){
        LOGGER.info("请求参数:{}",businessDTO.toString());
        return businessService.handleBusiness(businessDTO);
    }

 

11.下载安装TC ,即 Seata 的服务端,需要独立部署运行,下载地址:https://github.com/seata/seata/releases,解压,支持window和linux下直接启动运行,如下linux命令,运行参数将指定port、host和imageFile的存储方式:

sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

 

12.测试,按顺序启动:Nacos-->SeataServer-->account-->order-->storage-->business ,启动后的效果。

Nacos注册的服务信息,注意Dubbo是区分provider和consumer的,这是不同于SpringCloud的地方,所以同一服务不同身份就有两个了:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第6张图片

可以看到各RM向TC注册的信息:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第7张图片

 

Postman提交至Controller:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第8张图片


提交运行后,一阶段更新DB,二阶段只需释放锁:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第9张图片

 

数据库情况:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第10张图片

 

13.回滚测试:将`com.biao.mall.business.service.BusinessServiceImpl`中回滚测试代码注释去掉!手动抛出异常,再次Postman提交,可见:

  • - business先开启了全局事务,并传播了XID,二阶段向TC提交rollback状态;
  • - order中,可以看到分支事务是一阶段提交了,异常后,二阶段根据XID做了rollback;
  • - order有SQL信息,是application.yml中设置了logging.level为debug;

- 数据库信息不变,贴图,略;

 

14.测试undo_log表用途:
`com.biao.mall.business.service.BusinessServiceImpl`加个断点:
Dubbo学习系列之十四(Seata分布式事务方案AT模式)_第11张图片

 

其他模块正常启动,postman提交:

 

看undo_log表,这里只是个临时的数据,二阶段后会删除:

  

***
复盘记: 

1.Seata只能支持RPC模式的事务,对MQ模式的分布式事务不能实施,比较好的搭配是Dubbo+Seata。

2.启动应用向SeataServer注册,不一定能一次成功,有时要尝试多次,可见稳定性一般!

3.依赖冲突问题:报错提示:`Class path contains multiple SLF4J bindings`,因其来自于以下两个jar,
`logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class`
,`slf4j-nop-1.7.28.jar!/org/slf4j/impl/StaticLoggerBinder.class`
由于logback是主流,不排除,直接去掉`slf4j-nop`依赖,问题解决!

4.报错:`NoSuchMethodError:org.yaml.snakeyaml.nodes.ScalarNode.getScalarStyle`,**特别注意**这种情况很多时候也是依赖冲突,而不是缺少类,处理方法:

  a.先百度,需要加入snakeyaml依赖,结果还是报错,

  b.再先全局搜索,双击shift键,查找`ScalarNode`类,发现出现在两个地方,估计冲突了,

  c.在Idea中使用依赖分析命令,`order`为module名,`snakeyaml`为依赖名:

  `gradle :order:dependencyInsight --dependency snakeyaml`

发现有多方引入的情况,结果是dubbo本身也使用了snakeyaml,直接在dubbo依赖中使用exclude语法排除,问题解决!

5.报错:`NoSuchBeanDefinitionException: No qualifying bean of type 'com.biao.mall.order.dao.OrdersDao' available`:
表面上看是Mapper类无Bean实例,确定加了@Mapper和@Repository注解,还是错误!想到既然是缺少注入的Bean,可能是缺少mybatis-plus依赖导致,添加`mybatis-plus-boot-starter`,问题解决!

6.报错:`io.seata.common.exception.NotSupportYetException: not support register type: null`,需添加 registry.conf 和 file.conf。

7.seata server安装和启动方法:
https://github.com/seata/seata/wiki/Quick-Start

8.报错:`com.alibaba.nacos.api.exception.NacosException: java.lang.ClassNotFoundException`,添加Nacos相关依赖 dubbo-registry-nacos/spring-context-support/nacos-api/nacos-client。

9.dubbo的service是明显区分consumer和provider的,如果使用Nacos做注册中心,可以通过detail查看其服务角色,还有其提供的方法。

10.`com.biao.mall.storage.conf.SeataAutoConfig`中设置Mapper路径,需使用`getResources("classpath:/mapper/*Mapper.xml"))`;不可使用`getResources("${mybatis.mapper-locations}"))`配置方式,
会告警:`Property 'mapperLocations' was specified but matching resources are not found`,最后导致Mapper文件无法加载,Dao方法读取失败,应用运行会异常,我估计是Bean加载顺序问题,但没有验证,sorry。

11.本文参考文章地址:https://www.sofastack.tech/blog/seata-distributed-transaction-deep-dive/

***
推荐阅读:

 Dubbo学习系列之十三(Mycat数据库代理)

你可能感兴趣的:(Dubbo学习系列之十四(Seata分布式事务方案AT模式))