分布式事务解决方案,Seata的基本配置和使用

        

1. 分布式事务介绍

事务分为两种

  • 本地事务
  • 分布式事务

①:本地事务

        大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。
在这里插入图片描述

        本地事务要求只有一个数据库连接Connection,通过这个连接Connection对数据库事务进行开启begin、提交commit来保证整个代码的事务!伪代码如下:

   Connection conn = getConnection(); //获取数据库连接
   conn.setAutoCommit(false); //开启事务
   try {
       //...执行增删改查sql
       conn.commit(); //提交事务
   } catch (Exception e) {
       conn.rollback();//事务回滚
   } finally {
       conn.close();//关闭链接
   }

        Spring通过AOP的方式对数据库事务进行了整合,使我们平时在解决本地事务时,只需要加上@Transactional注解即可很方便控制本地事务!

        

②:分布式事务

        对于微服务架构,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务。分布式事务需要保证在多个数据库连接下,代码要么么全部成功,要么全部失败。

        本质上来说,分布式事务就是操作多个数据库连接时,也能保证数据要么一起修改成功,要么一起失败!为了保证不同资源服务器的数据一致性。

分布式事务的出现场景:

  • 跨库事务:一个应用某个功能需要操作多个库
    在这里插入图片描述

  • 分库分表:一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql:insert into user(id,name) values (1,"张三"),(2,"李四")。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此就面临着分布式事务的问题
    在这里插入图片描述

  • 微服务:微服务自然不多说,一个接口会调用很多服务,每个服务都有独立的数据库连接!
    在这里插入图片描述

③:常见的分布式事务解决方案

在这里插入图片描述
1. 2pc

        2PCXA/JTASeataAT模式等等,主要表现为:有全局锁,保证强一致性,更适合金融领域

        TCC也可以看做是两阶段提交,不过不需要全局锁,保证最终一致性。比XA/JTASeataAT模式效率高,但需要手动实现try、confirm、cancel接口,实现起来比较难!在微服务架构下实现TCC时,很有可能出现网络超时、重发,机器宕机等一系列的异常,出现空回滚、幂等、悬挂的问题。

常用的TCC开源框架有:Tcc-Transaction、 Hmily、 ByteTCC、 EasyTransaction、 Seata TCC等

        
2. TCC设计规范

  • ①:允许空回滚

    • 空回滚出现的原因: Try超时(丢包),分布式事务回滚触发Cancel,出现未收到Try,收到Cancel的情况,Cancel中对数据进行新增或者减少,造成数据不一致!因此Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。
    • 解决方案:由一个 事务ID 贯穿全局,并保存当前 事务ID 下的try、confirm、cancel记录到数据库,在执行cancel时先检查数据库中是否有try的记录,如果没有操作过trycancel就不执行或者返回null!
      在这里插入图片描述
  • ②:防悬挂控制

    • 悬挂出现的原因Try超时(拥堵),分布式事务回滚触发Cancel,执行了空回滚。之后拥堵的Try到达,又对数据进行了修改,造成数据不一致!CancelTry先执行!要运行空回滚,但要拒绝空回滚之后的Try操作
    • 解决方案:由一个 事务ID 贯穿全局,并保存当前 事务ID 下的try、confirm、cancel记录到数据库。在执行try操作时,先判断当前事务id的cancel、confirm是否执行过,只要执行过,就不再执行下面的try操作(try操作就是正常业务操作:扣库存、扣余额等等),防悬挂
      在这里插入图片描述
  • ③:幂等控制
    TryConfirmCancel都需要保证幂等性。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,可以使用业务id进行判重操作!
    在这里插入图片描述

3. 可靠消息(最终一致性)

        强一致性的XA、AT可能更适合金融领域,但对于一些高并发场景时,比如电商,其实更多的是采用补偿的措施去解决分布式问题,比如mq发消息等,虽然可能存在消息丢失,但可以人工补偿,保证最终一致性即可,避免使用全局锁拖慢整个系统性能!实现方式有以下几种

  • 本地消息表,发消息时把消息存储在本地,通过定时任务轮询该消息的发送结果,如果一定时间没有接收到ACK响应,则认为发送失败,然后重复发送,再失败一定次数后,转人工处理
  • RocketMQ的事务消息,其实就是把本地消息轮询放在Broker端,如果Broker没有正确接收到消息,则采用事务回查机制,根据事务回查结果来决定是否给消费端投递消息。

        

4. 最大努力通知(最终一致性)

        最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,是分布式事务中对一致性要求最低的一种,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。

最大努力通知型的实现方案,一般符合以下特点:

  • 不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
  • 定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。

所以最大努力通知方案需要实现如下功能:

  1. 消息重复通知机制。
  2. 消息查询校对功能。

以发短信业务为例,除了要回调通知发送端外,还要允许发送端查询发送状态。保证最终一致
在这里插入图片描述

最大努力通知与可靠消息最终一致性有什么区别?

  • 思想不同
    • 可靠消息最终一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
    • 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
  • 两者的业务应用场景不同
    • 可靠消息最终一致性关注的是交易过程的事务一致,以异步的方式完成交易。
    • 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
  • 技术解决方向不同
    • 可靠消息最终一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
    • 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)
              

3. 2PC与3PC

2PC:

        2PC两阶段提交协议(Two Phase Commit)从字面意思来理解, Two Phase Commit,就是将提交(commit)过程划分为2个阶段(Phase):如下图
在这里插入图片描述
其中2PC的实现有为XATCC

  • XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。

  • TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
    在这里插入图片描述

  • XA是资源(数据库)的分布式事务,强一致性,在整个过程中,数据一直锁住状态;即从preparecommitrollback的整个过程中,事务管理器TM一直把持着数据库的锁,如果有其他人要修改数据库的该条数据,就必须等待锁的释放。(简单一句话就是长事务风险)。另外在开发过程中,开发人员无感知XA的代码入侵

  • TCC是业务的分布式事务,最终一致性,不会出现长事务的锁风险。为什么TCC不会出现长事务的锁风险呢?因为try是本地事务,第一阶段tyy后就commit提交事务,confirmcancel也是本地事务,可以直接提交事务;故多个短事务不会出现长事务的风险。在开发过程中,开发人员感受到两阶段提交的存在;即开发人员必须实现tryconfirmcancel方法。第一阶段事务正常提交,不过会有一个字段记录着本次提交的数据变量(比如减10,则会把数据10记录下来),如果业务正常执行confirm,清除掉记录的值10,如果业务异常,则根据记录下来的10对原数据反向补偿(执行加10)操作!

2PC存在的问题:

  • 同步阻塞问题:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率及其低下。
  • 单点故障:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,那么就会影响整个数据库集群的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,整个数据库集群将无法提供服务。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

3PC

        三阶段提交(3PC),是二阶段提交(2PC)的改进版本。为弱化2PC存在的问题,三阶段提交有两个改动点:

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制,防止断网无法接受通知而阻塞!
  2. 新增一个提交准备CanCommit阶段。作用是在访问TM之前先检验一下网络是否通畅。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommitPreCommitDoCommit三个阶段。

        

①:2PC与3PC的区别

        相对于2PC3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

        所以,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。3PC虽然解决了部分问题,做了部分优化,但是也增加了一次RMTM的交互,并没有想象的那么强大,所以用3PC的也不多,因为这些优化,其实也可以自己在2pc中去做!

        

3. Seata介绍

        Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 ATTCCSAGAXA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)

分布式事务模式介绍技术栈
AT 模式无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本(sql都由框架托管统一执行,会存在脏写问题)seata、shardingsphere
TCC 模式对业务有侵入性,需要实现三个接口。适用于核心系统等对性能有很高要求的场景(第一阶段会产生行锁,事务执行太久会锁行很久)seata、service-comb
Saga 模式长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统(第一阶段就操作DB,会存在脏读问题)seata、shardingsphere、service-comb
XA模式分布式强一致性的解决方案,但性能低而使用较少。seata、shardingsphere

        相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模 式高很多。但需要改 造成tryconfirmcanel 3个接口,开发成本高

官网:https://seata.io/zh-cn/index.html
源码: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples

        

①:Seata的三种角色

在 Seata 的架构中,一共有三个角色:

  • TC (Transaction Coordinator) - 事务协调者
    维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TMRM 为嵌入到应用中的 Client 客户端。
        
Seata 中,一个分布式事务的生命周期如下
在这里插入图片描述

  1. 事务管理器TM 请求 事务协调者TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。TM是一个事务的发起者,可以是事务方法的入口
  2. 客户端的资源管理器RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
  3. TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
  4. TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
            

②:Seata的设计思路

SeataAT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如下:

  • 第一阶段RM端提交本地事务,生成undo日志记录,释放本地锁和连接资源。并向TC注册分支事务,通过全局事务的 XID 进行关联。
  • 第二阶段:完全异步
    • 分布式事务操作成功,则TC通知RM异步删除undolog
    • 分布式事务操作失败,则TMTC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XIDBranch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
              

③:设计亮点以及存在的问题

设计亮点:

  • 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
  • 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;避免单点故障
  • 通过全局锁实现了写隔离与读隔离。

存在的问题:

  • 性能损耗:由于Seata在解决分布式事务时,需要多次与TC通讯,每次都需要一次远程通讯RPC,而且是同步的。还要写undoLog日志,每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
  • 性价比:如果仅有极少的请求会失败,需要触发回滚。在使用了Seata解决分布式事务后,为了极少的交易回滚,需要将大部分的成功交易的响应时间增加5倍,这样的代价有待考量。
  • 死锁问题Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
            

4. Seata的DB模式配置

        Seata分TC、TMRM三个角色,TC(Server端)为单独服务端部署,TMRM(Client端)由业务系统集成。由于Seata的事务协调者TC是单独配置的,所以在使用Seata时需要先配置TC,然后再配置客户端RMTM

  1. TC环境配置
  2. 客户端RMTM配置

TC(Server端)存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb)

  • file模式:无需改动,直接启动即可
  • db模式:高可用模式,全局事务会话信息通过db共享,相应性能差些
  • redis模式:Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.

下面来看一下Seata的高可用DB模式在不同项目中的配置:
        
        

① :Spring boot单体项目集成Seata

事务协调者TC环境配置

  • 步骤一:下载seata服务器和源码的安装包     地址:https://github.com/seata/seata/releases
    在这里插入图片描述
    需要写 服务器文件解释 和 源码文件解释

  • 步骤二:建表(仅DB模式下需要建三张表)
    建表的sql,可以从seata的源码中获取
    在这里插入图片描述
    建成后如下所示:
    在这里插入图片描述

  • 步骤三:修改registry.conf 的配置
    registry.conf 配置文件在seata-server启动时会被加载,通过registry.conf 找到seata-server对应的配置信息
    在这里插入图片描述
    registry.conf配置如下:

registry {
  # seata Server端启动时会先加载type,根据type 去找对应的配置
  #file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  # 会根据 type = file 找到这里,然后再去读取"file.conf"
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

  • 步骤四:修改file.config中的配置

如果使用DB模式,file.config需要修改两点

  • 修改存储模式:store.mode = "db"
  • 修改数据库连接

file.conf配置如下:

## transaction log store, only used in seata-server
store {
  ## seata-server的各种存储模式,当前选择db模式
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }
  
  ## 根据上面选择的db模式,配置数据库连接信息 
  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 100
    # 数据库中刚才我们建立的三张表
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    ## redis mode: single、sentinel
    mode = "single"
    ## single mode property
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    ## sentinel mode property
    sentinel {
      masterName = ""
      ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
      sentinelHosts = ""
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

  • 步骤五:启动Seata Server
    • windows启动:点击 seata-server.bat
    • linux启动:点击seata-server.sh
    • 源码启动: 执行server模块下io.seata.server.Server.javamain方法
    • 命令启动: bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test

启动成功可以看到如下所示:
在这里插入图片描述

        

spring boot项目配置

业务场景:用户下单,整个业务逻辑由三个微服务构成:

  • 订单服务:根据采购需求创建订单。
  • 仓储服务:对给定的商品扣除库存数量。
  • 帐户服务:从用户帐户中扣除余额。
    在这里插入图片描述

        由于是Spring Boot单体项目,不同数据库的调用需要使用多数据源,在此基础上才能产生分布式事务。当出现业务异常时,需要undo_log表来进行事务回滚,所以每一个数据库都要建一个undo_log

注意:

  • 这个undo_log表并不是mysql自带的undo_log日志版本链,不要搞混了,
  • 这个undo_log表是SeataAT模式自带的表,用于数据回滚,AT模式不使用undo_log日志版本链进行回滚,
  • 而原生的XA模式(不是seataXA模式)的回滚就用的是mysql自带的undo_log日志版本链
# AT模式下的undo_log表
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 步骤一:引入seata依赖
  <!--seata对springboot的支持 -->
  <dependency>
      <groupId>io.seata</groupId>
      <artifactId>seata-spring-boot-starter</artifactId>
      <version>1.4.2</version>
  </dependency>

 

  • 步骤二:添加registry.conf文件,指定客户端配置文件地址
    在这里插入图片描述
    registry.conf配置内容如下
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  file {
    name = "file.conf"
  }
}

  • 步骤三:添加file.config文件,填写客户端请求stata服务的配置

file.config配置内容如下

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
# seata服务器配置
service {
  #transaction service group mapping
  # 配置组名称my_test_tx_group,用于找到在seata中属于自己的服务 
  vgroupMapping.my_test_tx_group = "default"
  vgroupMapping.multi-datasource-service-group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}
  • 步骤四:指定seata事务分组,用于从TC获取seata server服务实例
    由于 TC是独立部署的,可能这个TC集群服务了很多项目,它内部通过group做了服务隔离, 所以需要配置一个group用于确定seata server的服务实例
# Seata事务分组
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
  • 步骤五:配置@GlobalTransactional注解,发起全局事务

       springboot项目下的分布式事务,需要切换多数据源,而使用seata保证全局事务,需要对每个数据源进行一次代理,通过代理往TC注册分支事务信息!

       另外SeataAT模式依赖于本地事务,在下单 - 减库存 - 扣账户余额的过程中,每一个操作都需要加@Transtional注解,只需要在最外层加上@GlobalTransactional注解即可开启分布式事务,这也是seata对业务零侵入的体现。

  • 5.1 多数据源配置及其代理配置如下:

    @Configuration
    @MapperScan("com.tuling.mutiple.datasource.mapper")
    public class DataSourceProxyConfig {
    
        @Bean("originOrder")
        @ConfigurationProperties(prefix = "spring.datasource.order")
        public DataSource dataSourceMaster() {
            return new DruidDataSource();
        }
    
        @Bean("originStorage")
        @ConfigurationProperties(prefix = "spring.datasource.storage")
        public DataSource dataSourceStorage() {
            return new DruidDataSource();
        }
    
        @Bean("originAccount")
        @ConfigurationProperties(prefix = "spring.datasource.account")
        public DataSource dataSourceAccount() {
            return new DruidDataSource();
        }
    
        //指定数据源代理: 作用是数据源的AOP ,保存子事务信息到 TC事务协调者
        @Bean(name = "order")
        public DataSourceProxy masterDataSourceProxy(@Qualifier("originOrder") DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        @Bean(name = "storage")
        public DataSourceProxy storageDataSourceProxy(@Qualifier("originStorage") DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        @Bean(name = "account")
        public DataSourceProxy payDataSourceProxy(@Qualifier("originAccount") DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
    
        @Bean("dynamicDataSource")
        public DataSource dynamicDataSource(@Qualifier("order") DataSource dataSourceOrder,
                                            @Qualifier("storage") DataSource dataSourceStorage,
                                            @Qualifier("account") DataSource dataSourcePay) {
    
            DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
    
            // 数据源的集合
            Map<Object, Object> dataSourceMap = new HashMap<>(3);
            dataSourceMap.put(DataSourceKey.ORDER.name(), dataSourceOrder);
            dataSourceMap.put(DataSourceKey.STORAGE.name(), dataSourceStorage);
            dataSourceMap.put(DataSourceKey.ACCOUNT.name(), dataSourcePay);
    
            dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceOrder);
            dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
    
            DynamicDataSourceContextHolder.getDataSourceKeys().addAll(dataSourceMap.keySet());
    
            return dynamicRoutingDataSource;
        }
    
        //保存动态数据源可以在调用时实现数据源切换!
        @Bean
        @ConfigurationProperties(prefix = "mybatis")
        public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dynamicDataSource") DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);
        
            org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
            //使用jdbc的getGeneratedKeys获取数据库自增主键值
            configuration.setUseGeneratedKeys(true);
            //使用列别名替换列名
            configuration.setUseColumnLabel(true);
            //自动使用驼峰命名属性映射字段,如userId ---> user_id
            configuration.setMapUnderscoreToCamelCase(true);
            sqlSessionFactoryBean.setConfiguration(configuration);
            
            return sqlSessionFactoryBean;
        }
    
    }
    
  • 5.2 通过ThreadLocal绑定当前线程与某一个数据源的关系

    public class DynamicDataSourceContextHolder {
    
        private static final ThreadLocal<String> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.ORDER::name);
    
        private static List<Object> dataSourceKeys = new ArrayList<>();
    
        public static void setDataSourceKey(DataSourceKey key) {
            CONTEXT_HOLDER.set(key.name());
        }
    
        public static String getDataSourceKey() {
            return CONTEXT_HOLDER.get();
        }
    
        public static void clearDataSourceKey() {
            CONTEXT_HOLDER.remove();
        }
    
        public static List<Object> getDataSourceKeys() {
            return dataSourceKeys;
        }
    }
    
  • 5.3 基于AbstractRoutingDataSource的多数据源动态切换

    @Slf4j
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            log.info("当前数据源 [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
            return DynamicDataSourceContextHolder.getDataSourceKey();
        }
    }
    
  • 5.4 发起全局事务调用

        @Override
        //全局事务注解
        @GlobalTransactional(name="createOrder")
        public Order saveOrder(OrderVo orderVo){
            log.info("=============用户下单=================");
            //切换数据源
            DynamicDataSourceContextHolder.setDataSourceKey(DataSourceKey.ORDER);
            log.info("当前 XID: {}", RootContext.getXID());
            
            // 保存订单
            Order order = new Order();
            order.setUserId(orderVo.getUserId());
            order.setCommodityCode(orderVo.getCommodityCode());
            order.setCount(orderVo.getCount());
            order.setMoney(orderVo.getMoney());
            order.setStatus(OrderStatus.INIT.getValue());
        
            Integer saveOrderRecord = orderMapper.insert(order);
            log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
            
            //扣减库存,需要切换数据源
            log.info("=============扣减库存=================");
            storageService.deduct(orderVo.getCommodityCode(),orderVo.getCount());
            
            //扣减余额	需要切换数据源
            log.info("=============扣减余额=================");
            accountService.debit(orderVo.getUserId(),orderVo.getMoney());
            
        	//切换数据源
            log.info("=============更新订单状态=================");
            DynamicDataSourceContextHolder.setDataSourceKey(DataSourceKey.ORDER);
            //更新订单
            Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
            log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");
            
            return order;
            
        }
    

配置完成即可发起调用!下图是调用成功,无异常!
在这里插入图片描述
如果执行成功,如上所示,seata-server的日志如下:
在这里插入图片描述

        

② :Spring cloud微服务项目集成Seata

由于使用了微服务,我们可以把Seata Server注册到nacos做配置统一管理,具体步骤如下:

事务协调者TC环境配置

  • 步骤一:修改registry.conf文件注册中心、配置中心为nacos
    在这里插入图片描述

        此处与Spring Boot的配置不一样了,springboot项目需要指定file.conf从里边读取信息,需要修改registry.conffile.conf 文件。

        在springcloud项目中,我们使用nacos作为配置中心和注册中心管理配置,只需修改registry.conf文件即可!
在这里插入图片描述
在使用nacos配置中心时,最好新开一个namespace,与其他配置分离,防止数据混乱
在这里插入图片描述

  • 步骤二:获取/seata源码/script/config-center/config.txt,修改config.txt配置信息
    在这里插入图片描述
    主要修以下内容
    在这里插入图片描述
    配置事务分组, 要与客户端配置的事务分组一致
    (客户端properties配置:spring.cloud.alibaba.seata.tx‐service‐group=my_test_tx_group
    在这里插入图片描述
  • 步骤三:把第二步的配置信息同步到nacos
    config.txt文件的配置信息修改完毕后需要放入nacos配置中心,seata源码中已经提供了同步的脚本
    在这里插入图片描述
    cmd窗口中执行shell脚本,会自动把config.txt文件的配置信息推送到nacos对应的namespace
sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca

参数说明:

-h: IP地址:默认值 localhost
-p: 端口号:默认值 8848
-g: 配置分组:默认值为 ‘SEATA_GROUP’
-t: 租户信息:对应 Nacos 的命名空间ID字段, 默认值为空

  • 步骤四:启动nacos
  • 步骤五:启动Seata Server

都启动完毕后,打开nacos,发现seata服务已注册,配置信息已上传即可!
在这里插入图片描述
在这里插入图片描述

        

spring cloud项目配置

        spring cloud项目中,使用feign的远程调用取代了spring boot单机项目的多数据源,这也是分布式事务的另一个场景。

  • 步骤一:引入seata依赖
    环境准备:
    • seata:v1.4.0
    • spring cloud & spring cloud alibaba:
    • 在引入依赖时需要注意spring cloud alibabaseata的版本问题:spring cloud alibaba 2.1.2 及其以上版本使用seata1.4.0会出现如下异常 (但支持seata 1.3.0
      在这里插入图片描述

所以,如果想要使用seata:v1.4.0,需要对spring cloud alibaba:的版本进行降级

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.13.RELEASE</version>
    </parent>
    
<dependencies>   
	<!-- seata-->
	<dependency>
	    <groupId>com.alibaba.cloud</groupId>
	    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	    <version>2.1.1.RELEASE</version>
	    <exclusions>
	        <exclusion>
	            <groupId>io.seata</groupId>
	            <artifactId>seata-all</artifactId>
	        </exclusion>
	    </exclusions>
	</dependency>
	<!-- seata-->
	<dependency>
	    <groupId>io.seata</groupId>
	    <artifactId>seata-all</artifactId>
	    <version>1.4.0</version>
	</dependency>
	
	<!--nacos 注册中心-->
	<dependency>
	    <groupId>com.alibaba.cloud</groupId>
	    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	</dependency>
	
	<dependency>
	    <groupId>org.springframework.cloud</groupId>
	    <artifactId>spring-cloud-starter-openfeign</artifactId>
	</dependency>
</dependencies>
  • 步骤二:为每一个服务的数据库创建undo_log表
    springboot单体项目一样,发生业务异常时,也需要一个undo_log表来进行回滚!所以需要为每一个数据库建一个undo_log表,建表sql和上文一致!
            

  • 步骤三:使用DataSourceProxy代理数据源
    微服务项目可以只在common包里只写一份代理,其他服务引用common包即可!

    /**
     * 需要用到分布式事务的微服务都需要使用seata DataSourceProxy代理自己的数据源
     */
    @Configuration
    @MapperScan("com.tuling.datasource.mapper")
    public class MybatisConfig {
        
        /**
         * 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
         * 原生datasource前缀取"spring.datasource"
         * 其他服务引用后,会自动根据配置文件进行赋值!
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.druid")
        public DataSource druidDataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            return druidDataSource;
        }
        
        /**
         * 构造datasource代理对象,替换原来的datasource
         * @param druidDataSource
         * @return
         */
        @Primary
        @Bean("dataSource")
        public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
            return new DataSourceProxy(druidDataSource);
        }
        
        
        @Bean(name = "sqlSessionFactory")
        public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
            SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
            //设置代理数据源
            factoryBean.setDataSource(dataSourceProxy);
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            factoryBean.setMapperLocations(resolver.getResources("classpath*:mybatis/**/*-mapper.xml"));
            
            org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
            //使用jdbc的getGeneratedKeys获取数据库自增主键值
            configuration.setUseGeneratedKeys(true);
            //使用列别名替换列名
            configuration.setUseColumnLabel(true);
            //自动使用驼峰命名属性映射字段,如userId ---> user_id
            configuration.setMapUnderscoreToCamelCase(true);
            factoryBean.setConfiguration(configuration);
            
            return factoryBean.getObject();
        }
        
    }
    

其他服务引用后,会自动根据当前服务的配置文件对数据源赋值!例如账户服务:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000

注意: 启动类上需要排除DataSourceAutoConfiguration,否则会出现循环依赖的问题

@SpringBootApplication(scanBasePackages = "com.tuling",exclude = DataSourceAutoConfiguration.class)
public class AccountServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountServiceApplication.class, args);
    }
}
  • 步骤四:在每一个微服务上添加registry.conf配置

    • ①:将registry.conf文件拷贝到resources目录下,指定注册中心和配置中心都是nacos,此处与springboot项目不同,由于配置已经上到nacos上了,所以不用再添加file.conf配置信息了!

      registry {
        # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
        type = "nacos"
      
        nacos {
          serverAddr = "127.0.0.1:8848"
          namespace = ""
          cluster = "default"
          group = "SEATA_GROUP"
        }
      }
      
      config {
        # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
        type = "nacos"
      
        nacos {
          serverAddr = "127.0.0.1:8848"
          namespace = "226cc392-3523-4ac1-86a5-304fb9871048"
          group = "SEATA_GROUP"
        }
      }
      
    • ②:在yml中指定事务分组(和配置中心的service.vgroup_mapping 配置一一对应)

      spring:
        application:
          name: order-service
        cloud:
          nacos:
            discovery:
              server-addr: 127.0.0.1:8848
          alibaba:
            seata:
              tx-service-group:
                my_test_tx_group # seata 服务事务分组
      
      
  • 步骤四:在事务发起者中添加@GlobalTransactional注解,发起调用

        @GlobalTransactional(name="createOrder")
        public Order saveOrder(OrderVo orderVo) {
            log.info("=============用户下单=================");
            log.info("当前 XID: {}", RootContext.getXID());
            
            // 保存订单
            Order order = new Order();
            order.setUserId(orderVo.getUserId());
            order.setCommodityCode(orderVo.getCommodityCode());
            order.setCount(orderVo.getCount());
            order.setMoney(orderVo.getMoney());
            order.setStatus(OrderStatus.INIT.getValue());
        
            Integer saveOrderRecord = orderMapper.insert(order);
            log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
        
            //扣减库存
            storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
            
            //扣减余额   服务降级  throw
            Boolean debit= accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());
    
    //        if(!debit){
    //            // 解决 feign整合sentinel降级导致SeaTa失效的处理
    //            throw new RuntimeException("账户服务异常降级了");
    //        }
    
            //更新订单
            Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
            log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");
            
            return order;
        }
    

测试结果正常: 如果出现异常,各微服务一起成功或失败,符合分布式事务期望!

        

5. 服务降级对分布式事务的影响

        在微服务项目中,通常会配置服务降级策略,电商业务:下订单 - 减库存 - 扣账户余额,如果扣除账户余额失败后会执行下边的降级代码!

@Component
@Slf4j
public class FallbackAccountFeignServiceFactory implements FallbackFactory<AccountFeignService> {
    @Override
    public AccountFeignService create(Throwable throwable) {

        return new AccountFeignService() {
            @Override
            public Boolean debit(String userId, int money) {
                log.info("账户服务异常降级了");
                // 解决 feign整合sentinel降级导致Seata失效的处理  此方案不可取
                if(!StringUtils.isEmpty(RootContext.getXID())){
                //通过xid获取GlobalTransaction调用rollback回滚
                //  可以让库存服务回滚  能解决问题吗?  绝对不能用
                    GlobalTransactionContext.reload(RootContext.getXID()).rollback();
               }
            }
        };
    }
}

        上述代码通过xid获取GlobalTransaction调用rollback进行回滚,可以让库存服务回滚,但是订单还是生成了,分布式事务失效了!因为分布式事务没有感受到异常!那么在服务发生降级时如何使用分布式事务呢?

        思路:使用一个boolean类型标记是否发生降级,在主业务代码中根据boolean值,如果发生了降级,则抛出一个自定义异常,此异常可被全局事务捕捉到,进而触发分布式事务的回滚! 重写降级代码如下:

@Component
@Slf4j
public class FallbackAccountFeignServiceFactory implements FallbackFactory<AccountFeignService> {
    @Override
    public AccountFeignService create(Throwable throwable) {

        return new AccountFeignService() {
            @Override
            public Boolean debit(String userId, int money) {
                log.info("账户服务异常降级了");
                // 解决 feign整合sentinel降级导致Seata失效的处理  此方案不可取
                //
//                if(!StringUtils.isEmpty(RootContext.getXID())){
//                    //通过xid获取GlobalTransaction调用rollback回滚
                //  可以让库存服务回滚  能解决问题吗?  绝对不能用
//                    GlobalTransactionContext.reload(RootContext.getXID()).rollback();
//                }
                return true;
            }
        };
    }
}

主业务代码中根据boolean值来决定是否抛出自定义异常,进而触发分布式事务回滚

    @Override
    //@Transactional
    @GlobalTransactional(name="createOrder")
    public Order saveOrder(OrderVo orderVo) {
        log.info("=============用户下单=================");
        log.info("当前 XID: {}", RootContext.getXID());
        
        // 保存订单
        Order order = new Order();
        order.setUserId(orderVo.getUserId());
        order.setCommodityCode(orderVo.getCommodityCode());
        order.setCount(orderVo.getCount());
        order.setMoney(orderVo.getMoney());
        order.setStatus(OrderStatus.INIT.getValue());
    
        Integer saveOrderRecord = orderMapper.insert(order);
        log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
    
        //扣减库存
        storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
        
        //扣减余额   服务降级  throw
        Boolean debit= accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());
        
		//根据debit值来决定是否抛出自定义异常,进而触发分布式事务回滚
		//如果触发了降级,debit == true
        if(debit){
            // 解决 feign整合sentinel降级导致SeaTa失效的处理
            throw new RuntimeException("账户服务异常降级了");
        }

        //更新订单
        Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
        log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");
        
        return order;
    }

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值