spring boot + seata + nacos

一、需求背景

目前项目整体架构都是基于微服务的基础上开发,同时逐渐把第三方的功能回迁,此次采用其中一个功能(报销流程)来举例,整个大功能后端大致上可以分成三个模块,财务模块,流程模块,发票模块,文章后序均已简称描述,A模块(财务模块),B模块(流程模块),C模块(发票模块),调用的逻辑链路是 :A -> B -> A -> C -> A,三个模块有功能上相互依赖的关系。

在这里插入图片描述

报销的流程业务描述:

  1. 在页面发起报销流程,请求进入到A
  2. 调用B,判断发票是否已被使用(业务上限制一张发票只能被使用一次)
  3. 在A保存基础的表单信息(报销的金额,报销人等基础信息)
  4. 调用B保存发票的相关信息
  5. 调用C发起报销的流程,流程成功发起后返回相关的流程信息(如流程唯一标识等)
  6. 在A保存C返回的信息
  7. 调用B,让B保存C返回的流程唯一标识

在公司人员在飞速增长下,内部系统访问量渐增,分布式事务问题日渐凸显,在一次巧合下了解到了Seata,同时架构师说公司用户运营中台有一个Seata的Committer,文章后续简称‘伟少’,在初期自己集成的Seata的时候遇到各种各样的问题,在网络上流传的文章基本都是千篇一律,不是你C我的就是我V你的,都是一些标准的CV工程师,几乎没有自己想要的答案,后面实在不行了就去找伟少了,结局肯定都是完美解决。

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

二、整合过程

服务端

Seata Server: 1.1.0

客户端

Spring Cloud Alibaba 1.5.1 + Nacos 1.1(初期使用Eureka) + Seata 1.1.0

相信有不少人在整合中间件的过程中,都遇到了不少问题,本着记录和分享的精神,将过程中遇到的每一个问题,以便帮助遇到同样问题的朋友。

先来看看以下表格,可以很清晰的看到,SCA 1.5.1依赖的Seata版本为0.7.1,但是当时Seata最新版本已经到了1.1.0,经过内部讨论后,依然决定使用低版本的SCA去整合高版本的Seata

Spring Cloud Alibaba VersionSentinel VersionNacos VersionRocketMQ VersionDubbo VersionSeata Version
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE1.8.01.3.34.4.02.7.81.3.0
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE1.7.11.2.14.4.02.7.61.2.0
2.2.0.RELEASE1.7.11.1.44.4.02.7.4.11.0.0
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE1.7.01.1.44.4.02.7.30.9.0
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE1.6.31.1.14.4.02.7.30.7.1

SCA版本说明:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

相信使用过Seata的朋友,已经知道怎么去启动Seata Server,此处就不详细展开讲解,只作简单描述

修改registry.conf

registry {
  type = "nacos"
  nacos {
  	serverAddr = "127.0.0.1:8848"
  	namespace = ""
  }
}

修改file.conf为db模式(Seata 1.1支持的模式可选有file和db)

store {
  mode = "db"
  db {
    url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
    user = "mysql"
    password = "mysql"
  }
}

运行官方提供的sql脚本

CREATE TABLE IF NOT EXISTS `global_table`
...
CREATE TABLE IF NOT EXISTS `branch_table`
...
CREATE TABLE IF NOT EXISTS `lock_table`
...

脚本地址:https://github.com/seata/seata/tree/1.1.0/script/server/db

启动成功并检查nacos

在这里插入图片描述

接下来是客户端

先粗略讲解一下seata AT模式的大概过程:通过代理数据库的数据源去接管本地事务,然后在请求链路的入口处,生成一个全局的xid,然后把这个xid带到下游服务里面去,服务端就通过这个xid确定这次请求的范围一共涉及了多少个事务。

既然要试用低版本的SCA去整合高版本的Seata,以下操作是必不可少的,然后重新依赖Seata的最新版本1.1.0

<spring-cloud-alibaba-seata.version>1.5.1.RELEASE</spring-cloud-alibaba-seata.version>
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-alibaba-seata</artifactId>
   <version>${spring-cloud-alibaba-seata.version}</version>
   <exclusions>
      <exclusion>
         <artifactId>seata-all</artifactId>
         <groupId>io.seata</groupId>
      </exclusion>
   </exclusions>
</dependency>

使用官方提供的脚本新建undo_log表

CREATE TABLE IF NOT EXISTS `undo_log`
...

脚本地址:https://github.com/seata/seata/tree/1.1.0/script/client/at/db

客户端配置

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: oa-process-service
seata:
  registry:
    type: nacos
    nacos:
      namespace: nacos空间名
      server-addr: nacos的地址
  service:
    vgroup-mapping:
      oa-process-service: default
  client:
    undo:
      logSerialization: kryo

logSerialization 这个配置可根据实际情况去配置,这个配置是用于客户端保存undolog记录的时候序列化和反序列化数据,项目上有使用了flowable框架,flowable上时间的字段大部分都是精确到毫秒的,seata默认使用的是jackson,经调研发现kyro和protobuf在对时间精度上有更加好的支持,最终选定kyro。

三、问题初现

当使用Spring Cloud Alibaba 1.5.1集成高版本Seata,有问题也是在预料之中,当集成完毕后,启动会出现如下的报错

在这里插入图片描述

由于Spring Cloud Alibaba 1.5.1和对应的Spring Cloud Edgware官方已经停止维护,所以不建议使用此版本

经过调试和请教后发现,是初始化GlobalTransactionScanner时出现了问题,由于在Spring Cloud Alibaba 1.5.1存在的GlobalTransactionAutoConfiguration和Seata 1.1存在的SeataAutoConfiguration,他们的目的都是用于初始化GlobalTransactionScanner,导致了重复初始化引发的错误。

GlobalTransactionScanner是用于为使用了@GlobalTransactional和@GlobalLock生成代理类,使得该方法被调用时能够进入Seata的拦截器

继续尝试启动,会出现第二个问题如下图

在这里插入图片描述

找不到Netty相关的方法,因为Spring Cloud Edgware所依赖的eureka版本使用的Netty较旧,而Seata所使用的Netty较新,而导致问题,只需要简单把老版本Netty依赖排除掉即可

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec-http</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-transport-native-epoll</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-buffer</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec-http</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-common</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-handler</artifactId>
        </exclusion>

    </exclusions>
</dependency>

最后一个问题也是上面所提到过的logSerialization变更为kyro的主要原因之一,由于Spring boot 1.5.x 会出现 jackson 相关 NoClassDefFoundException

Caused by: java.lang.NoClassDefFoundError: Could not initialize class com.fasterxml.jackson.databind.ObjectMapper

原因是发现在 Spring Boot 1.5.x 版本中原始引入的 jackson 版本过低,会导致 Seata 依赖 jackson 的新特性找不到,Seata 要求 jackson 版本2.9.9+,但是使用 jackson 2.9.9+ 版本会导致Spring Boot中使用的jackson API找不到,也就是jackson本身的向前兼容性存在问题

四、全面升级

听说Seata从1.1开始,到1.4.0,每一个版本都有非常大的变动,每个版本合并pr高达过百个,可见社区活跃度非常不错。经过部门内部讨论,也考虑到该中间件的性能和完善程度。决定邀请伟少前来协助,对内部系统所有微服务进行全面的升级

五、问题再现

当微服务版本依赖变更完成后尝试启动项目,出现了比较奇怪的错误,错误原因出现在Seata绑定XID到上下文时,提示的XID不能为空。

当带有@GlobalTransactional注解的方法被调用时,如果Seata上下文不存在XID,则会请求TC开启一个新的全局事务并生成XID返回到客户端,客户端会将XID绑定到上下文中并传递到下游的链路中。

但可疑的是,这是应用启动的时候报错,意味着并没有开启全局事务。理论上XID作为全局事务ID,我写了@GlobalTransactional注解之后才会生成并且传递,可是我就一个查询接口我也没写@GlobalTransactional居然也报错了,后来折腾了几天感觉要入土了就只能去请教伟少的帮忙了,结果伟少看了一眼说了句1.4是会有这个问题,前几天刚发布了1.4.1修复了这个问题,你升级一下版本就好,幸福来得这么突然,果然升级了版本之后就好。

在这里插入图片描述

后续运行期间,在TC会有较低的概率出现branch_table和lock_table出现残留数据,但是global_table缺没记录,导致后续其他全局事务提交失败,需要手动清理branch_table和lock_table的数据。

由于分支事务一阶段提交时,需要往TC注册分支事务,注册分支时候需要获取全局锁,由于lock_table已经存在残留数据,所以会导致获取锁失败,从而导致分支事务注册失败。

这个问题在1.1版本就开始出现,但是一直没有找到原因。又经过了几天的排查,在伟少的帮助下,终于发现了这个问题的真相,原因是TC端连的数据库做了主从,当全局事务决议进行二阶段的时候,去找分支事务信息和锁信息删除的时候,查询请求分发到了从库,此时分支数据还没完全同步到从库上,只能获取到部分的分支事务,导致二阶段提交时,只提交了一部分分支事务,而另外一部分继续残留在数据库。最后关闭掉数据库主从,该问题得以解决。

六、总结

Seata AT模式简单的执行过程

  1. 开启全局事务(通过@GlobalTransactional,生成xid)
  2. 执行分布式分支的方法,在提交本地事务的时候,数据更新之前,先对需要修改的数据生成一个前置镜像,然后执行sql,然后再对修改后的数据生成一个后置的镜像
  3. 向TC发起分支注册,并且保存本地的undolog,最后提交本地事务
  4. TC会给每个微服务发起提交请求,如果出现异常,分支会根据xid和branchId(每一个分支都会对应一个自己的branchId)去查询undolog,会将之前生成的后置镜像和数据库当前的数据对比,看是否有脏写,如果没有发生脏写,则会根据前置镜像去回滚数据

在集成和使用seata的过程中虽然遇到不少的问题,但分析这些问题并且解决之后可以提升自身的能力,对自身能力有很大的锻炼,总的来说seata是一个很棒棒的开源框架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值