分布式事务方案与Seata详解

分布式事务与Seata详解

  • 一、分布式事务
    • 1.什么是分布式事务
    • 2.分布式事务解决方案-2PC
    • 3.分布式事务解决方案-3PC
    • 4.分布式事务解决方案-TCC
    • 5.分布式事务解决方案-XA
    • 6.可靠消息最终一致性
      • 6.1 本地消息表
      • 6.2 事务消息
    • 7.最大努力通知
    • 8.SAGA
    • 9.分布式事务解决方案思考
  • 二、Seata 简介与环境搭建
    • 1.Seata简介
    • 2.环境搭建
      • 2.1 使用Docker安装Seata
      • 2.2 docker cp容器内的配置文件进行更改
      • 2.3 更改配置文件applicaion.yml的注册中心和配置中心
      • 2.4 更改Nacos中配置文件的数据库信息
      • 2.5 验证Seata的可用性
    • 3.Seata集群
  • 三、Seata的各种分布式方案支持
    • 1.AT模式
      • 1.1 依赖引入
      • 1.2 配置更改
      • 1.3 测试使用
      • 1.4 关于错误:can not get cluster name in registry config 'service.vgroupMapping.default_tx_group'
      • 1.5 GlobalTransactional失效问题
      • 1.6 无法获取异常时手动回滚全局事务
      • 1.7 AT模式的脏写和脏读问题
      • 1.8 解决AT模式下的脏读脏写问题
      • 1.9 AT模式下全局锁与DB锁的死锁问题
    • 2.XA方案

一、分布式事务

这一部分对分布式事务的信息进行普及,第二部分说下主流开源的分布式事务的解决方案

1.什么是分布式事务

在高并发场景下单个数据库无法承载系统的正常运行,需要对数据库进行垂直或者水平拆分,这样就可能会出现单个服务操作多个数据库的现象,此时就需要考虑事务在不同数据库中对于ACID的满足,也就是分布式事务了,此外服务的拆分也会涉及,比如单个服务拆分成多个,如何保证多个服务中的事物满足ACID也属于分布式事务的范畴。

  • 单个服务调用不同库
    单个服务调用不同库

  • 不同服务调用数据库
    在这里插入图片描述

以上就是分布式事务的典型场景了,一般项目中做了分库分表的拆分,或者有高并发场景都会涉及到分布式事务的使用。这里还需要说下分布式事务处理模型(DTP),Mysql、Oracle一般都是支持2PC协议(比如在redo log的持久化时就是用的二阶段提交),国际开放组织为了减少规范不统一造成的问题,出具了DTP,对分布式事务进行了规范,该规范中有如下角色:

AP: Application Program,即应用程序,也就是事务的发起者,负责定义事务的操作
RM: Resouce Manager,资源管理者,可以将它理解成数据库,他管理者数据库资源,他应该具有提交事务和回滚事务的能力
TM: Transaction Manager,事务管理者,用以管理RM,协调完成分布式事务

2.分布式事务解决方案-2PC

2PC即二阶段提交(two-phase commit protocol),2pc是一个经典的强一致性、中心化的原子提交协议,这里的中心化指的是2pc需要依赖一个中心化的调解者结点,用来调解各个事务参与结点,2pc的事务处理过程被分为了两个阶段。

  • 第一阶段:prepare
    事务管理器给每个参与者发送prepare消息,各个事务参与者收到消息后开始执行事务,这个事务是本地事务,如果使用的是Mysql的话,那就是单个的Mysql的事务,此时会有undo log 、redo log产生,不过此时的分支事务并没有提交,而是一个阻塞状态,在Mysql中DML是通过默认加行锁+间隙锁来实现的(如果更新条件是唯一索引确定唯一数据则降级为行锁),所以在事务提交之前,当前库的数据是被锁定的。那这个锁定或者叫分支事务的阻塞需要到什么时候呢?分两种情况

    • 事务协调者通知:在第二阶段,事务协调者会根据各个分支事务的返回情况来判断整体的提交或者回滚,分支事务收到后提交或者回滚分支事务,接触数据的锁定。
    • 分支事务锁定超时:分支事务锁定一定时间后无法接收协调者的处理信息,则会释放锁定回滚分支事务,这是分支事务的超时机制(注意协调者通知分支分支事务时并没有超时机制)
  • 第二阶段:commit/cancel
    在第一个阶段各个分支事务需要将各自的处理信息反馈给事务协调者,如果有任何一个分支事务处理失败,则事务协调者发送事务回滚信息给各个分支事务,各个分支事务收到后则回滚分支事务,若是全部分支事务在第一阶段都是返回的成功则事务协调者会给所有分支事务发送提交的信息,各个分支事务收到commit信息后,提交分支事务解除数据的锁定,分支事务若是全部提交成功此时还会返回一个信息给到事务协调者,如果这个信息都是成功的则分布式事务完成提交,若此时是部分分支事务失败则继续重试,直到commit成功,同理若是发送的是cancel也是同样的过程。需要注意的是在事务协调者通知分支事务的场景中(2pc)并没有设计超时机制。

    • 问题1:事务协调者挂掉,导致各个分支事务的数据长时间被锁住,需要依赖分支事务的超时机制释放锁
    • 问题2:若是事务协调者在发送commit或者cancel请求后挂掉,此时部分分支事务收到了请求,部分没有收到就会出现数据不一致的情况。
    • 问题3:分支事务的数据处于长时间锁定状态,此时存在性能问题
    • 问题4:各个分支事务可能在第一阶段开始前就有存在不可用的状态,此时协调者并不知情,会直接发送prepare,这样就会导致部分分支事务的数据进入锁定状态,此时这些锁定注定是会被回滚的,属于无效锁定
  • 你觉得2PC哪些场景需要优化
    问题1问题2优化:
    前两个问题都有一个共同的假设,就是事务协调者挂了,所以第一个优化思路就是事务协调者做高可用架构,避免单节点的宕机风险。
    问题3优化:
    分支事务长时间锁定数据问题,这个优化可以考虑不锁定,不锁定就需要直接提交事务,直接提交事务则需要手动写事务的回滚代码,或者就是减少事务的锁定时间,这个优化方向就是Seata里的AT模式,AT模式就是一种优化后的2PC
    问题4优化:
    既然事务协调者在开始prepare之前并不知道所有的分支事务状态,那就先检测下状态,以防止无效锁定,这个优化方向其实就是3PC

  • 业界哪些场景方案属于2PC
    3PC:优化了一阶段开始前无法感知分支事务的状态的问题
    AT:优化了一阶段数据锁定时间较长的问题,AT模式下分支事务直接提交

3.分布式事务解决方案-3PC

3PC(three-phase commit)是对2PC的一个改进,上面也说了2PC存在明显的问题,并不是一个多么优秀的方案,那3PC是一个什么过程呢,3PC将2PC的第一阶段prepare分成了cancommit、precommit两个阶段,3PC还有一个docommit阶段。3PC是优化2PC而来,自然是针对缺点进行了改进,一起看看三个阶段都做了什么

  • 第一阶段:cancommit
    这是相对于2PC实质性多出来的一步,这一步事务协调者TM需要询问所有的分支事务也就是资源拥有者RM是否可以完成事务的操作(一般服务健康就行),如果都恢复OK,事务协调者参会准备进入第二步,如果有任何一个分支事务不OK都不会进入第二步,也就不会开启分布式事务。

    • cancommit阶段相比于2PC到底优化了什么?
      仔细一看就会知道,3PC其实就是多了cancommit阶段,而3PC的precommit与2PC的prepare其实没区别。那3PC的cancommit到底优化了什么呢?其实在2PC的问题4中已经说了,2PC的第一阶段开始之前可能就会存在部分分支事务是不健康或者不可用的,但是事务协调者并不知道,直接开始prepare阶段就会造成部分分支事务的数据被锁定了,这些数据注定被回滚(因为存在部分分支事务的异常)所以他们的锁定都属于无效锁定,3PC通过引入cancommit阶段,用于检测事务开始之前各个分支事务的状态,所有分支事务状态都正常情况下才会开始事务,就可以有效避免了分支事务的无效锁定问题,减少了分支事务在异常场景下对数据的锁定时长(正常场景并不会减少锁定时长)。
  • 第二阶段:precommit
    这个阶段可以完全类比2PC的prepare阶段。事务协调者收到各个分支事务在第一阶段回复的OK的信息后,进入第二阶段precommit阶段,事务协调者给所有分支事务发送precommit信息,分支事务收到信息后开始执行事务,DML同样是锁定数据记录(行锁+间隙锁),这个锁定结束与2PC相比来说多了一种。

    • 事务协调者通知:事务协调者会在第二阶段收到各个分支事务的返回,如果都是ok,则执行docommit,否则执行cancel
    • 分支事务锁定超时:这与2PC没有啥区别,分支事务拥有超时机制,避免数据的长久锁定
    • 事务协调者等待分支事务超时:3PC相比于2PC的另外一个不同的是引入了协调者的超时机制,当协调者在一定时间无法接受分支事务的回调后,则认为分支事务已经异常,将发送事务回滚信息给各个分支事务。
  • 第三阶段:docommit
    这个阶段和2PC的commit阶段是一样的,该阶段需要依赖3PC的各个分支事务在第二阶段的返回信息

    • 所有分支事务都成功了:
      • 执行docommit:所有分支事务执行提交,然后将处理结果回传事务协调者
        • 所有分支事务docommit执行成功:事务完成
        • 所有分支事务docommit存在失败:重新尝试推送,直到成功
    • 所有分支事务存在失败:
      • 执行cancel:回滚所有分支事务
    • 部分分支事务响应超时:
      发送cancel,继续尝试重试
  • 你觉得3PC哪些场景需要优化
    很明细3PC相对于2PC来说优化了,分支事务开始之前各分支事务状态不可知的情况,解决了部分分支事务异常导致的其他分支事务数据无效锁定的问题。但是对于2PC存在的其他问题,3PC并没有解决,也即使依然存在着事务协调者的单点故障问题,依然存在着分支事务的数据锁定问题。优化思路这里就不说了,问题和2PC都是一样的,优化思路自然也是一样的。

4.分布式事务解决方案-TCC

TCC(Tryp Commit Cancel),其实也属于2PC的范畴,他也是分为两个阶段,第一个阶段是try,是通知分支事务进行事务开启并处理数据,此时数据处于锁定状态,commit则是提交数据,用于通知分支事务的数据可以提交了,cancel则是回滚分支事务。操作细节与2PC并无多大区别,这里就不重复说了,说下TCC之所以叫TCC的原因,主要是因为各个分支事务都必须对try、commit、cancel三个操作进行业务逻辑实现,也就是说TCC,并不像2PC、3PC那样依赖RM对事务的实现,他的逻辑通过自己定义实现,这是他和2PC、3PC的本质区别。

  • TCC的适用场景
    TCC对代码的侵入会比较高,需要自己定义复杂度也会更高,而且实现过程中事务、幂等等常见都是需要考虑的因素,不过TCC优点也是支持自定义,这样就可以有更多的可定制化的操作。市面上支持TCC的事务框架还是比较多的,这里列举几个。
    • seata
      地址:https://github.com/seata/seata
      说明:seata是2019年1月开源,它是基于阿里云的全局事务服务GTS,https://www.aliyun.com/aliware/txc的开源版本
    • tcc-transaction
      地址:https://github.com/changmingxie/tcc-transaction
      说明:tcc-transaction不和底层使用的rpc框架耦合,也就是使用dubbo、thrift、web service、http等都可
    • hmily
      地址:https://github.com/yu199195/hmily
      说明:由碧桂园工程师开发,异步高性能分布式事务tcc开源框架。支持dubbo、springcloud、motan等rpc框架进行分布式事务
    • easyTransaction
      地址:https://github.com/QNJR-GROUP/EasyTransaction
      说明:柔性事务,分布式事务,TCC,SAGA,可靠消息,最大努力交付消息,事务消息,补偿,全局事务,自动补偿。可一站式解决以上分布式事务问题

5.分布式事务解决方案-XA

XA其实是一种规范,并不是事务的具体实现方式,简单说就是XA定义了分布式事务的标准,然后有不同的方案实现,比如2PC,3PC其实都可以算是一种XA的不同方式。那什么事XA呢?

是X/OPEN (一个独立的组织我们熟悉的华为、甲骨文、IBM等都在该组织内)提出的分布式事务处理规范。XA则规范了TM与RM之间的通信接口,在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。目前知名的数据库,如Oracle, DB2,mysql等,都是实现了XA接口的,都可以作为分支事务的RM,也就是说我们使用Oracle、Mysql等作为分布式事务的RM是完全没有问题的。

那XA有哪些规范呢,如下:

规范描述
xa_reg向TM中注册一个RM。
xa_unreg从TM中注销RM
xa_close使用RM终止AP
xa_commit告诉RM提交事务分支。
xa_complete测试一个异步操作完成。
xa_end从事务分支中解除线程。
xa_forget允许RM丢弃其对某个已完成的交易分支的知识。
xa_open询问RM是否准备提交事务分支。
xa_prepare要求RM为提交事务分支做好准备。
xa_recover获取一个列表XIDs,该RM已经准备或异步完成的事务分支。 xa_rollback 告诉RM回滚到一个事务分支。
xa_start开始或恢复一个线程关联并请求线程执行RM中的事务,此时需要跟着XID

从上面可以看到XA定义了分布式事务处理中的各种接口应该实现哪些动作,比如RM注册到TM,RM开始执行事务与提交回滚等。Mysql在5.0.3开始实现了这些规范,所以Mysql是完全可以作为RM的存在的。但是我们真正使用事务时并不是直接使用的数据库的事务,而是通过应用程序来操作数据库的事务。比如Spring中的声明式事务或者编程式事务,声明式事务底层通过AOP来操作数据库事务,那这里就相当于RM其实并不是严格意义上的数据库了,应该是一个应用程序,所以说到XA就必须得体JTA(Java Transaction API)可以将JTA看成是java的XA规范,JTA规定了XA规范在java中的实现,用于在Java应用层面实现对分布式事务的管控。
下面是JTA定义的标准接口:

javax.transaction.TransactionManager : 事务管理器,负责事务的begin, commit,rollback 等命令。
javax.transaction.UserTransaction:用于声明一个分布式事务。
javax.transaction.TransactionSynchronizationRegistry:事务同步注册
javax.transaction.xa.XAResource:定义RM提供给TM操作的接口
javax.transaction.xa.Xid:事务xid接口。

这样就可以实现逻辑闭环了,比如ShardingSphere就是对XA与JPA的一种实现,不过JPA一般是适用于单服务对不同数据库操作的分布式事务场景。

6.可靠消息最终一致性

可靠消息最终一致性,是指当事务发起执行完本地事务后通过MQ发送一条消息给到分支事务,分支事务一定是可以通过MQ接收到这条消息的,然后达到信息的最终一致性。

6.1 本地消息表

本地消息表是在本地增加一张消息表,用以存储其他分支事务需要处理的信息,这样就可以实现在本地事务完成时,本地消息表的存储同时成功同时失败,然后事务成功后可以通过MQ或者定时任务等进行消息通知,分支事务根据接收到的信息进行处理。此时有可能存在信息同步没有实时成功的情况,应该有定时任务去巡检本地消息表,对于失败的任务做继续处理,直到成功。
这种方式会有信息的一定延时,强调的是最终一致性,对于信息实时性要求特别高的,则会不适用,这里的MQ只需要能保证信息的一致即可,无需支持事务消息。

在这里插入图片描述

  • 本地消息适用于哪些场景
    这里强调的是最终一致性,对于时效性要求高的业务场景不建议使用,有可能产生信息的短暂不一致。但是对于一致性有要求但不是特别高,就可以使用这种,这种方案设计请求代码简单。

6.2 事务消息

必须是支持事务的MQ才可以,阿里开源的RocketMQ是一个不错的选择,RocketMQ事务消息的本质是通过将本地消息表的动作移动到了RocketMQ的服务端,从而实现事务消息的投递的。他的原理如下图(详细参见RocketMQ官方文档:https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage):
在这里插入图片描述

下面对上面的过程进行简单说明:

  • 1.生产者将消息发送至Apache RocketMQ服务端。

  • 2.Apache RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。

  • 3.生产者开始执行本地事务逻辑。

  • 4 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

    • 4.1 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。

    • 4.2 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。

  • 5.在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 说明 服务端回查的间隔时间和最大回查次数,请参见参数限制。

  • 6.生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

  • 7.生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

事务消息的本质:
事务消息的本质就是通过半消息将本地消息表的信息移动到了MQ的服务端,通过交互保证了MQ与本地事务的原子性,在保证了本地事务的结束后,然后将半消息标记为可投递状态,从而保证了本地事务与MQ的一致性。但是事务消息并不能保证下游系统与MQ的一致性,需要下游系统自己做消息可靠性的处理。

  • 事务消息适用场景
    事务消息的本质还是最终一致性,其实和本地消息表是一个原理。都无法保证本地事务和分支事务的实时一致,如果对数据的强一致性要求特别高的话,是不适用的,如果可以接受短暂的不一致,那么这种方案是一种可行的高效的解决办法,话又说回来即使采用XA这种强一致性的分布式事务方案,也无法保证信息的绝对一致,因为网络的问题和单机的不可靠性都可能产生信息不一致的情况。

7.最大努力通知

最大努力通知(Best-effort delivery)是最简单的一种柔性事务,适用于对于时效性要求不高的业务场景,或者被动接收方与主动发送方的依赖性不高的场景,比如银行转账处理成功了,需要发送一条通知信息,此时这个通知信息可以使用最大努力通知方案进行消息通知,最大努力通知方案一般符合以下特点:

  • 不可靠消息:
    利用消息中间件进行推送,但是消息可靠性无法保证,业务主动方处理完毕后将消息发送给MQ,业务被动方通过MQ接收消息,然后处理自己的信息。此场景业务主动方只发送N次消息,N次后仍失败则不会继续重试。
  • 定期校对:
    业务被动方需要定期去业务主动方拉取信息,以获取失败的处理信息,恢复丢失的业务信息

最大努力通知的优缺点:
优点是实现简单,只需要很少的代码就可以实现该方案,缺点是只适用于柔性事务,最大努力通知对于事务的一致性只要求最终一致,所以对于强一致性的事务不适用,比如转账时转账方和收账方的场景肯定不可以使用这种方式,因为他们对同时成功同时失败是有强要求的。这种场景只是适合通知类等不在主要流程范围内的业务。

8.SAGA

SAGA其实XA一样都是比较早一些的规范,SAGA是将一个大事务拆分成多个本地事务,多个本地事务构成一个事务链。这个事务链就是分布式事务的模型。链上的每个点都是一个本地事务,且本地事务的执行后会直接提交,而不会像其他方案一样锁定数据(2PC时说过这个问题)。
在这里插入图片描述

那SGGA怎么保证事务链执行失败后的回滚问题呢?SAGA中还规定每个本地事务都需要实现一个事务的回滚动作,这个是需要自己定义的(这里明显有一个问题,就是事务的读已提交问题,事务回滚很可能会导致其他事务提交的信息一起被回滚了),当事务链执行异常时有两种方式进行策略:

  • 对于失败的事务进行重试:
    事务链中当出现事务回滚时,针对失败的事务进行失败重试,重复尝试调用,争取对事务事务的成功。

  • 全部回滚:
    事务链中事务出现失败时,从失败的事务开始向前依次开始回滚,途中若出现事务回滚失败,则继续重试回滚。

  • SAGA模式的优缺点
    SAGA是将各个本地事务形成一个事务链来处理事务的,这种策略适合什么场景呢?他比较适合跨公司系统的事务的处理,比如跨行转账的业务中,A银行扣款成功,B银行也应该扣款成功。但是如果B失败了,A银行需要做对应的重试或者回滚操作,其实就是可以使用SAGA这种模型的(只是假设这么处理,现实不一定这么做)。

    • 优点:
      单个事务中的数据不会长时间锁定,出现资源挂起的情况
    • 缺点:
      事务的回滚依赖各个事务自定义回滚代码,复杂度提高,且本地事务提交后,信息对其他事务已经可见,会造成分布式事务中的可见性问题,比如重复读问题,且一旦异常回滚,需要考虑事务回滚或会不会回滚其他事务的信息等。

9.分布式事务解决方案思考

上面已经介绍了常见的分布式事务的方案、XA(2PC、3PC)、TCC、SAGA、最大努力通知、可靠消息最终一致性等方案。他们各自有各自的特点,说不好谁最好谁最不好,根据自己的业务场景选择对应的方案才是正解,一句话适合自己的才是最好的。下面简单总结下各个方案的特点并给出适用于的各个场景,以帮助大家快速识别各自的适用方案。
XA(2PC、3PC): 强一致性,适合对数据实时性要求比较高的业务场景,可能会出现资源挂起,分支事务数据长时间锁定问题。
TCC:补偿事务方案,通过自定义try、commit、cancel来让业务自己保证事务的可靠性,自己实现复杂度高,需要考虑事务、幂等等各种问题
SAGA:补偿事务方案,需要定义事务的回滚方案,事务链依据本地事务的回滚方案进行回滚,存在事务的可见性问题,可能会回滚其他事务的已提交信息。
最大努力通知:弱一致性方案,对数据的实时性要求不好,事务通知失败后可转为定时进行重试。实时性不高,准确性也不高。
可靠消息最终一致性:弱一致性方案,如果对数据的实时性要求不是瞬间一致,可以选用该方案,该方案的本质是通过本地消息表来保证远端的分支事物的成功性,最大缺点是弱一致性,最大优点是支持的并发高,他的并发支持度是XA、TCC等不可以比的。

在分布式场景中还有一句话就是最好的分布式事务方案就是不要使用分布式事务,因为所有的方案都不是完美的,要想完美就别用分布式事务,所以如非必须还是别用分布式事务。

二、Seata 简介与环境搭建

上面已经介绍了分布式事务的各种实现方案,下面可以来个实操了,后面会详细的介绍Seata与他支持的各种分布式事务方案。
官网:https://seata.apache.org/zh-cn/docs/v1.6/overview/what-is-seata
代码:https://github.com/apache/incubator-seata/tags

1.Seata简介

Seata是一款由阿里开元,目前更新在Apache的一款开源的分布式事务的解决方案,致力于提供高性能和简单易用的分布式服务。Seata为用户提供了AT、TCC、SAGA、XA等四种事务模式,其中默认使用AT模式。Seata是阿里商用的GTS(Global Transaction Service)的开源版本。
Seata中主要包含三个角色:

TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

在这里插入图片描述

  • 简述TC、TM、RM的工作流程
    在第一部分已经介绍了各种分布式事务的方案,其中XA、2PC、3PC中,TM、RM都是已经出现过了,但是没有TC,那这个TC是做什么的呢?我们可以先做个角色假设:TC是Seata的服务端,TM是Seata的客户端,RM则是JDBC或者数据库。下面已我们用Seata服务端、Seata客户端、JDBC来说下Seata处理分布式事物的过程:

    1.Seata客户端向Seata服务端报告开启全局事务
    2.Seata客户端通知JDBC开始事务了(这里的JDBC并不是原始的,已经被代理了)
    3.JDBC收到分支事务开启通知,开始向Seata服务端注册自己,他们拥有共同的XID
    4.注册完成后不同服务的JDBC开始执行各自的分支事务,并向Seata服务端报告自己的事务状态
    5.Seata客户端在收到各个分支事务的返回信息后,开始提交全局事务
    6.Seata服务端收到客户端的提交后,检查各个分支事务在自己这边报告的状态,以决定全局事务是提交还是回滚
    7.假如分支事务状态都是成功的,那么Seata服务端向各个JDBC发送事务提交请求,否则发送回滚请求

2.环境搭建

Seata分为客户端和服务端,服务端单人TC的角色负责管理RM的状态和提交回滚全局事务,所以我们需要对服务端进行搭建。这里根据我的组件版本,我选择的是Seata1.5.2作为服务端,使用Docker进行搭建。
在这里插入图片描述

2.1 使用Docker安装Seata

这里需要先直接安装下Seata服务,方便获取配置文件,后面就可以将该容器内部的配置文件直接拿到后进行更改了,这里的8091是TC的通讯端口,7091是Tomcat的端口,这里如果是静态ip就加上-e SEATA_IP=192.168.150.140这个,不是的话就别加这个参数了。

# -e SEATA_IP 指定注册到注册中心的IP,-e SEATA_PORT 指定端口
docker run --name seata-server -d \
        -p 8091:8091 \
        -p 7091:7091 \
        -e SEATA_IP=192.168.150.140 \ 
        -e SEATA_PORT=8091 \
        seataio/seata-server:1.5.2

在这里插入图片描述
创建完成后使用docker ps -a 查看全部的docker服务,可以看到,创建的seata已经正常运行了,如下:
在这里插入图片描述

2.2 docker cp容器内的配置文件进行更改

执行下面的命令,创建工作空间并复制容器内配置文件到本地

# 创建seata的工作空间
mkdir -p /apps/seata
cd /apps/seata

# copy容器内的配置信息做数据卷
docker cp seata-server:/seata-server/resources ./

执行完以上命令后,展示如下:
在这里插入图片描述

2.3 更改配置文件applicaion.yml的注册中心和配置中心

下面是修改前的配置文件信息展示。

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: file
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: file
  store:
    # support: file 、 db 、 redis
    mode: file
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

这里需要着重关注的是config、registry、store三个模块,分别是配置中心、注册中心、数据库的配置信息,这里默认都是使用file。先更改config、和registry信息如下(配置模版可以参考application.example.yml):

千万注意避坑1:不要更改registry和config的namespace这个值就保持默认为空,1.5.2版本中,这个值更改了以后,后面即使客户端保持和你更改的一致也是无法使得配置生效,我已经验证了,别更改这个值就用默认的空就行,客户端也这么配,如果nacos设置了用户名和密码的记得配上

千万注意避坑2:registry中的cluster不要随便改,这个值需要和下面的新增的nacos中的配置文件中的service.vgroupMapping.default_tx_group=default保持一致,客户端的配置也是需要读取这个service.vgroupMapping.default_tx_group=default,所以如果对这些配置不清晰,最好别改

千万注意避坑3:最好别改这个SEATA_GROUP,这个分组是seata自用的分组,可以用以区分和普通服务和配置的区别

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos
    nacos:
      server-addr: 192.168.150.140:8848
      namespace:
      group: SEATA_GROUP
      username: nacos
      password: nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    preferred-networks: 192.168.* # 该配置是优先使用该段ip进行注册
    nacos:
      application: seata-server # 声明seata服务名
      server-addr: 192.168.150.140:8848 # 更改为自己的地址
      group: SEATA_GROUP
      namespace:
      cluster: default
      username: nacos # 根据实际值进行设置
      password: nacos # 根据实际值进行设置
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""

然后删除上面的容器,重新根据最新的配置信息进行创建容器,命令如下:

# 删除之前的容器,6a 是我的id
docker rm -f 6a 

# 从新建立seata容器
docker run -d --name seata-server \
        -p 8091:8091 \
        -p 7091:7091 \
        -v /apps/seata/resources/application.yml:/seata-server/resources/application.yml \
        -e SEATA_IP=192.168.150.140 \
        seataio/seata-server:1.5.2	

验证服务注册如下:

在这里插入图片描述

下面还需要一步就是需要在nacos中建立配置文件信息,在public中新建配置文件seataServer.properties,配置信息的内容如下,这里的改动点是将该配置文件中value为空的配置项进行注释了,不然会导致配置文件不生效,其他还未更改:

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
# service.default.grouplist=127.0.0.1:8091
# service.enableDegrade=false
# service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true

server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, DB, and redis configuration values are optional.
store.mode=file
store.lock.mode=file
store.session.mode=file
#Used for password encryption
# store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
#store.redis.mode=single
#store.redis.single.host=127.0.0.1
#store.redis.single.port=6379
#store.redis.sentinel.masterName=
#store.redis.sentinel.sentinelHosts=
#store.redis.maxConn=10
#store.redis.minConn=1
#store.redis.maxTotal=100
#store.redis.database=0
#store.redis.password=
#store.redis.queryLimit=100

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

新建时选项如下(配置文件的原文在incubator-seata-1.5.2\incubator-seata-1.5.2\script\config-center\config.txt中):
在这里插入图片描述
这里提交时提示说格式错误的话建议基本都是因为部分配置项=后没有赋值提示的,请参照我上面的配置进行更改。这里更改完后再重启下seata使得配置信息生效

# 重启
docker restart seata-server
# 查看日志
docker logs seata-server

可以看到下面的日志显示已经正常加载了配置信息,说明配置无问题:
在这里插入图片描述

2.4 更改Nacos中配置文件的数据库信息

上面已经将配置文件移到了nacos中,我们便可以在Nacos中更改配置信息了,这里对数据库信息进行更改。

  • 初始化数据库
    Seata需要依赖数据库,比如他的TM的注册信息,需要保存的log信息等,数据库的脚本位置在:incubator-seata-1.5.2\incubator-seata-1.5.2\script\server\db\mysql.sql
    下面是1.5.2 的数据库脚本:

    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS `branch_table`
    (
        `branch_id`         BIGINT       NOT NULL,
        `xid`               VARCHAR(128) NOT NULL,
        `transaction_id`    BIGINT,
        `resource_group_id` VARCHAR(32),
        `resource_id`       VARCHAR(256),
        `branch_type`       VARCHAR(8),
        `status`            TINYINT,
        `client_id`         VARCHAR(64),
        `application_data`  VARCHAR(2000),
        `gmt_create`        DATETIME(6),
        `gmt_modified`      DATETIME(6),
        PRIMARY KEY (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS `lock_table`
    (
        `row_key`        VARCHAR(128) NOT NULL,
        `xid`            VARCHAR(128),
        `transaction_id` BIGINT,
        `branch_id`      BIGINT       NOT NULL,
        `resource_id`    VARCHAR(256),
        `table_name`     VARCHAR(32),
        `pk`             VARCHAR(36),
        `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_status` (`status`),
        KEY `idx_branch_id` (`branch_id`),
        KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `distributed_lock`
    (
        `lock_key`       CHAR(20) NOT NULL,
        `lock_value`     VARCHAR(20) NOT NULL,
        `expire`         BIGINT,
        primary key (`lock_key`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
    

    操作完如下:
    在这里插入图片描述

  • 初始化AT所需undo表
    Seata默认使用AT模式,AT模式需要各个业务库存在undolog表(seata自己的库不需要),所以我们还需要在业务库增加undolog表,注意分布式事务肯定是跨库的,需要在所有需要支持分布式事务的库中新增这个表。

    -- 注意此处0.3.0+ 增加唯一索引 ux_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,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    

    这里我作为验证随便整了两个库,搞了两个表,两个表的结构一模一样只是表明不同bus_one、bus_two,然后对应的放入了undolog表,这里贴下我的bus_one的sql:

    CREATE TABLE `bus_one` (
      `id` bigint(20) NOT NULL COMMENT '主键',
      `name` varchar(255) DEFAULT NULL,
      `desc` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    

    在这里插入图片描述

  • 更改Nacos数据库配置信息
    上面已经将数据库信息初始化完成了,下面需要更改下数据库的配置信息,配置信息修改后如下所示,下面是完整的配置信息展示,服务端使用这个配置暂时已经无需变更了。这里主要更改的配置项,先进行截图展示:
    特别注意:下面三个db不可以大写,配置文件的注释写的大写,这里是个误导,笔者试了大写配置会导致启动失败
    在这里插入图片描述
    这里是我的完整配置信息,验证无问题的

    #For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
    #Transport configuration, for client and server
    transport.type=TCP
    transport.server=NIO
    transport.heartbeat=true
    transport.enableTmClientBatchSendRequest=false
    transport.enableRmClientBatchSendRequest=true
    transport.enableTcServerBatchSendResponse=false
    transport.rpcRmRequestTimeout=30000
    transport.rpcTmRequestTimeout=30000
    transport.rpcTcRequestTimeout=30000
    transport.threadFactory.bossThreadPrefix=NettyBoss
    transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
    transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
    transport.threadFactory.shareBossWorker=false
    transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
    transport.threadFactory.clientSelectorThreadSize=1
    transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
    transport.threadFactory.bossThreadSize=1
    transport.threadFactory.workerThreadSize=default
    transport.shutdown.wait=3
    transport.serialization=seata
    transport.compressor=none
    
    #Transaction routing rules configuration, only for the client
    service.vgroupMapping.default_tx_group=default
    #If you use a registry, you can ignore it
    service.default.grouplist=127.0.0.1:8091
    service.enableDegrade=false
    service.disableGlobalTransaction=false
    
    #Transaction rule configuration, only for the client
    client.rm.asyncCommitBufferLimit=10000
    client.rm.lock.retryInterval=10
    client.rm.lock.retryTimes=30
    client.rm.lock.retryPolicyBranchRollbackOnConflict=true
    client.rm.reportRetryCount=5
    client.rm.tableMetaCheckEnable=true
    client.rm.tableMetaCheckerInterval=60000
    client.rm.sqlParserType=druid
    client.rm.reportSuccessEnable=false
    client.rm.sagaBranchRegisterEnable=false
    client.rm.sagaJsonParser=fastjson
    client.rm.tccActionInterceptorOrder=-2147482648
    client.tm.commitRetryCount=5
    client.tm.rollbackRetryCount=5
    client.tm.defaultGlobalTransactionTimeout=60000
    client.tm.degradeCheck=false
    client.tm.degradeCheckAllowTimes=10
    client.tm.degradeCheckPeriod=2000
    client.tm.interceptorOrder=-2147482648
    client.undo.dataValidation=true
    client.undo.logSerialization=jackson
    client.undo.onlyCareUpdateColumns=true
    server.undo.logSaveDays=7
    server.undo.logDeletePeriod=86400000
    client.undo.logTable=undo_log
    client.undo.compress.enable=true
    client.undo.compress.type=zip
    client.undo.compress.threshold=64k
    #For TCC transaction mode
    tcc.fence.logTableName=tcc_fence_log
    tcc.fence.cleanPeriod=1h
    
    #Log rule configuration, for client and server
    log.exceptionRate=100
    
    #Transaction storage configuration, only for the server. The file, DB, and redis configuration values are optional.
    store.mode=db
    store.lock.mode=db
    store.session.mode=db
    #Used for password encryption
    #store.publicKey=
    
    #If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
    store.file.dir=file_store/data
    store.file.maxBranchSessionSize=16384
    store.file.maxGlobalSessionSize=512
    store.file.fileWriteBufferCacheSize=16384
    store.file.flushDiskMode=async
    store.file.sessionReloadReadSize=100
    
    #These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
    store.db.datasource=druid
    store.db.dbType=mysql
    store.db.driverClassName=com.mysql.jdbc.Driver
    store.db.url=jdbc:mysql://192.168.150.180:3306/seata?useUnicode=true&rewriteBatchedStatements=true
    store.db.user=root
    store.db.password=super
    store.db.minConn=5
    store.db.maxConn=30
    store.db.globalTable=global_table
    store.db.branchTable=branch_table
    store.db.distributedLockTable=distributed_lock
    store.db.queryLimit=100
    store.db.lockTable=lock_table
    store.db.maxWait=5000
    
    #These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
    store.redis.mode=single
    store.redis.single.host=127.0.0.1
    store.redis.single.port=6379
    #store.redis.sentinel.masterName=
    #store.redis.sentinel.sentinelHosts=
    store.redis.maxConn=10
    store.redis.minConn=1
    store.redis.maxTotal=100
    store.redis.database=0
    #store.redis.password=
    store.redis.queryLimit=100
    
    #Transaction rule configuration, only for the server
    server.recovery.committingRetryPeriod=1000
    server.recovery.asynCommittingRetryPeriod=1000
    server.recovery.rollbackingRetryPeriod=1000
    server.recovery.timeoutRetryPeriod=1000
    server.maxCommitRetryTimeout=-1
    server.maxRollbackRetryTimeout=-1
    server.rollbackRetryTimeoutUnlockEnable=false
    server.distributedLockExpireTime=10000
    server.xaerNotaRetryTimeout=60000
    server.session.branchAsyncQueueSize=5000
    server.session.enableBranchAsyncRemove=false
    server.enableParallelRequestHandle=false
    
    #Metrics configuration, only for the server
    metrics.enabled=false
    metrics.registryType=compact
    metrics.exporterList=prometheus
    metrics.exporterPrometheusPort=9898
    

    配置完成后重启Seata容器即可。

2.5 验证Seata的可用性

这里先不做分布式事务的验证,只对服务端的搭建成功与否进行验证。Seata默认访问地址:http://192.168.150.140:7091/#/login。
ip记得替换,默认账号密码在服务端的application.yml中配置,默认是:seata/seata
在这里插入图片描述
登录系统后可以看到如下页面基本OK了
在这里插入图片描述
不过这里还不可以百分百确定我们配置的数据库的配置是否起了作用,可以在seata数据库的下面这张表随便新增一条记录进行验证:
在这里插入图片描述
新增完成后可以在如下位置进行查看页面信息如果可以正常展示,则证明自己数据库的配置也没有问题了:
在这里插入图片描述
这样就可以确定我们搭建的Seata服务肯定是没有问题的了,验证完记得删掉垃圾数据。

3.Seata集群

Seata集群的实现比较简单,基于同一个注册中心即可,所以我们只需要将不同的Seata服务注册到同一个注册中心就行,注意数据库要使用同一个,保持数据库配置统一。

三、Seata的各种分布式方案支持

上面已经介绍完了服务端的搭建,已经可以开始客户端的实操了,这里客户端操作需要具备的基础技能包括:SpringBoot、SpringCloud、Nacos、OpenFeign、LoadBalancer、Mybatis等基础操作,这里就不做这些知识的延伸了,如果还不清晰建议画个半天补一补就行了。言归正传,Seata支持了四种模式的分布式事务:AT、XA、TCC、SAGA,默认使用AT模式,在第一部分介绍分布式事务时,介绍了XA(2pc、3pc)、TCC、SAGA、以及可靠消息最终一致性、最大努力通知等,但AT模式在上面介绍时并没细说,只是在2PC的优化方向里提了一句,这里先补充说下:

AT模式:
Seata的AT模式其实是2PC的优化,2PC存在一个明显的问题就是一阶段可能造成数据的锁定时间较长,造成系统并发度较低,还可能造成数据长时间的无效锁定挂起等问题,AT模式利用将分支数据与undo数据一起提交(AT模式下每个业务库都需要一个undolog表就是这个作用)所以本地事务就直接提交了,但全局事务回滚时,直接根据undolog进行回滚,这样解决了数据长时间锁定的问题,但同时也面临了一个新的问题,那就是同一条数据如果在回滚前已经被修改了,可能会造成数据的回滚覆盖,这个其实就是数据库隔离级别为读已提交的问题,所以具体使用哪个模式还需要根据自己的业务场景来定。

AT一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
AT二阶段:
全局事务成功,TC通知RM异步删除undolog
全局事务回滚,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

这里只对AT和XA展开说下,其他模式就不说了。

1.AT模式

前置条件
准备两个微服务,他们分别可以实现本服务内的数据存储,然后可以实现两个服务的跨服务调用,跨服务调用方式没有强制要求,RPC、Rest都是可以的,这里选用OpenFeign的Rest调用。然后就可以开始了。

1.1 依赖引入

这里其他依赖就不展示了,只展示使用的Seata依赖,我这里的环境如下:
在这里插入图片描述
引入的依赖如下:

<!--        引入seata依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.0.4.0</version>
</dependency>

1.2 配置更改

配置这块只需要更改application.yml即可,1.5.2 已经无需手动添加数据源代理了,这块已经默认帮我们做了,只需要配置好seata的信息即可,下面展示其中一个的配置信息(两个服务只有服务名不同,其他均相同):
注意事项在配置项里注释里写了,这里再说下:

  • 1.Seata的nacos配置与服务本身的nacos配置没有任何关系,不要搞混了
  • 2.Seata的config、registry中的namespace不好去定义,就和服务端一样保持空即可,服务端也不要去自定义这个值
server:
  port: 6100

spring:
  application:
    name: ebbing-testseata-one
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.150.180:3306/seata_bus_one?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
    username: root
    password: super
  cloud:
    nacos:
      server-addr: 192.168.150.140:8848
      user-name: nacos
      password: nacos
      discovery:
        namespace: public
        
seata:
  application‐id: ${spring.application.name}
 # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx‐service‐group: default_tx_group
  registry:
    type: nacos
    nacos:
      # 特别注意:这里需要声明seata服务端名称
      application: seata-server
      server‐addr: 192.168.150.140:8848
      # 注意这里保持和服务端一样,别自己定义namespace,保持为空
      namespace:
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server‐addr: 192.168.150.140:8848
      # 注意这里保持和服务端一样,别自己定义namespace,保持为空
      namespace:
      group: SEATA_GROUP
      data‐id: seataServer.properties

1.3 测试使用

使用起来也是很简单的只需要一个注解就行了GlobalTransactional就行了,下面是我的代码:

  • 注意1:只需要在全局事务开始时使用GlobalTransactional即可,无需在被调用方使用,但是被调用方必须是TM
  • 注意2:GlobalTransactional的name用以标识一组事务,rollbackFor用以标识回滚的异常
package com.cheng.ebbing.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cheng.ebbing.entity.BusOne;
import com.cheng.ebbing.feign.BusTwoServiceFeign;
import com.cheng.ebbing.mapper.BusOneMapper;
import com.cheng.ebbing.service.BusOneService;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import io.seata.tm.TransactionManagerHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 
 *
 * @author pcc
 * @date 2024-04-07 17:21:52
 */
@Service
public class BusOneServiceImpl extends ServiceImpl<BusOneMapper, BusOne> implements BusOneService {

    @Resource
    private BusTwoServiceFeign busTwoServiceFeign;
    @Override
    @GlobalTransactional(name = "hello",rollbackFor = Exception.class)
    public void testSeata(Long id) throws TransactionException {

        System.out.println("全局事务id:" + RootContext.getXID());

        BusOne busOne = new BusOne();
        busOne.setId(id);
        busOne.setName("busOne");
        busOne.setDescr("这是操作一的持久化");
        // 本地持久化
        baseMapper.insert(busOne);
        // 跨服务调用
        busTwoServiceFeign.getBusOnePage(id);
        // 模拟异常,回滚
        throw new RuntimeException("测试异常");
    }
}

然后就可以启动调用方和被调用方的代码了,启动后有如下信息展示则证明客户端没有问题了,不过很可能你就会碰到
can not get cluster name in registry config ‘service.vgroupMapping.default_tx_group’
这个问题,使用Seata这个问题碰到的概率很高,所以下面1.4单独说下,这里就不说了
在这里插入图片描述
然后就可以测试了,进行测试,测试后基本不会有啥以外,无论调用者还是被调用者接口异常都会触发全局事务的回滚,注意全局事务的回滚依赖异常,如果手动catch了异常,则无法回滚,我这里是没有catch,所以两个服务都正常回滚了,服务控制台输出如下:
在这里插入图片描述
然后在控制台还可以看到回归的事务信息:

在这里插入图片描述
到这里AT模式的正常场景就ok了。

1.4 关于错误:can not get cluster name in registry config ‘service.vgroupMapping.default_tx_group’

这个错误会很容易碰到,碰到这种问题的场景会有很多,但是根本只有一个,就是无法找到Seata服务端配置文件中service.vgroupMapping.default_tx_group的配置项,记住这才是根本原因,当然了报错已经说了,那造成这个问题的最可能原因会有好几点,我在这里列举下:

  • 1.Seata客户端的配置中心配置有问题
    配置中心的IP、Port、group这、username、password些项必须严格和Seata服务端对应,其次最主要的namespace不要赋值,就保持和服务端一样是空的,服务端也不好服务,Seata1.5.2 中即使客户端服务端的namespace一致,也会异常最后报上面的错误,此时是很难发现问题的,我找了3个小时最后才定位到,坑死了
  • 2.Seata客户端的注册中心配置有问题
    注册中心配置有问题也有可能会出这个异常,注意Seata客户端中的注册中心的配置application,配置的是Seata服务端的名称,这个不能错,其他的配置项也必须和服务端对应,namespace也必须一致为空。
  • 3.服务端配置文件更改了service.vgroupMapping.default_tx_group
    这个是是事务分组,用以区分不同的事务分组,改不改其实没啥影响,如果要改,客户端服务端是必须统一的,客户端的配置里我我在注释里提醒了
  • 4.更改了集群名称导致的
    集群名称有两个地方配置了,必须保持统一,第一个是Seata服务端的下面位置
    在这里插入图片描述
    还有一块在服务端Nacos的配置文件中的下面配置项,这里必须一致,要改都改,要不都别改:
    在这里插入图片描述

1.5 GlobalTransactional失效问题

笔者使用GlobalTransactional时碰到过注解失效的场景,具体原因还没有认真探究,这里先记录下问题,我写了如下代码,分布式事务在当前代码中失效了,获取XID时直接是null,也没有回滚

package com.cheng.ebbing.controller;

import com.cheng.ebbing.entity.BusOne;
import com.cheng.ebbing.feign.BusTwoServiceFeign;
import com.cheng.ebbing.service.BusOneService;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


/**
 * 
 *
 * @author pcc
 * @date 2024-04-07 17:21:52
 */
@RestController
@RequiredArgsConstructor
@RequestMapping("/busone" )
public class BusOneController {

    private final  BusOneService busOneService;

    @GetMapping("/do" )
    public String getBusOnePage(@RequestParam("id") Long id) throws TransactionException {
        return "success";
    }

    @GlobalTransactional(name = "hello",rollbackFor = Exception.class)
    public void testSeata(Long id) throws TransactionException {
        System.out.println("全局事务XID:"+RootContext.getXID());
        busOneService.testSeata(id);

    }
}

在这里插入图片描述
不过将全局事务的注解应用在service中就不会有问题,放到controller里直接就不生效了,这里验证了多次确实如此,先在这里记录了

1.6 无法获取异常时手动回滚全局事务

上面已经提到了全局事务的回滚需要依赖异常信息,也就是说如果异常发生在被调用方,那么被调用方如果catch了异常,调用方就不知道异常了,此时就会造成全局事务无法回滚,反之也是一样的,调用方的异常catch了,也会有这个问题。这里就需要用到手动回滚了,如果catch了异常我们就需要手动进行回滚

  • 调用方catch:在catch中进行手动回滚
  • 被调用方catch:需要约定异常的返回内容,根据返回内容或者状态码进行手动回滚

下面是代码示例:

package com.cheng.ebbing.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cheng.ebbing.entity.BusOne;
import com.cheng.ebbing.feign.BusTwoServiceFeign;
import com.cheng.ebbing.mapper.BusOneMapper;
import com.cheng.ebbing.service.BusOneService;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import io.seata.tm.TransactionManagerHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 
 *
 * @author pcc
 * @date 2024-04-07 17:21:52
 */
@Service
public class BusOneServiceImpl extends ServiceImpl<BusOneMapper, BusOne> implements BusOneService {

    @Resource
    private BusTwoServiceFeign busTwoServiceFeign;
    @Override
    @GlobalTransactional(name = "hello",rollbackFor = Exception.class)
    public void testSeata(Long id) throws TransactionException {

        System.out.println("全局事务id:" + RootContext.getXID());

        BusOne busOne = new BusOne();
        busOne.setId(id);
        busOne.setName("busOne");
        busOne.setDescr("这是操作一的持久化");
        try {
            // 本地持久化
            baseMapper.insert(busOne);
            // 跨服务调用
            busTwoServiceFeign.getBusOnePage(id);
            // 模拟异常,回滚
            throw new RuntimeException("测试异常");
        } catch (Exception e) {
            // 手动回滚
            TransactionManagerHolder.get().rollback(RootContext.getXID());
             e.printStackTrace();
        }

    }
}

可以看到此时仍然回滚了,这里使用的就是手动回滚:
在这里插入图片描述

1.7 AT模式的脏写和脏读问题

脏读:
读未提交时,读取到了其他事务未提交的数据,在全局事务中,一个全局事务未提交的数据被其他事务读取到了,就是脏读问题,AT模式下,分支事务直接提交,不在二阶段提交,所以可能会有这个问题。

脏写:
在读未提交和读已提交时都会有脏写问题(其实就是不可重复读),事务读取到了其他事务(已提交或未提交)的数据。在AT模式下如果只说全局事务的并发是没有脏写问题的,如果是两个全局事务同时操作一条数据,那么TC会有一个全局锁的竞争过程,竞争失败的会直接异常,此时不会有脏写问题,因为直接拒绝了并发的修改。那如果是全局事务与存储的本地事务并发就不一样了,全局事务开始先获取全局锁,获得后就会开始分支事务,进而操作数据,如果在全局事务的本地分支数据修改完成之后,全局事务未提交之前,有纯粹的本地事务修改数据,那么此时是可以修改成功的。

因此在AT模式中很可能会出现两个场景:

  • 全局事务的并发
    在AT模式下,TC端有全局锁控制,使用GlobalTransactional时,会先去申请全局锁,全局锁锁定的是全局事务涵盖的数据,被锁定的数据不允许并发修改,因此在AT模式下有全局并发事务修改同一条数据时,若是第一个全局事务未结束,那么第二个全局事务无法获得全局锁,因此第二个事务会直接失败。在这个场景下不会有脏读问题和脏写问题。

  • 全局事务与纯粹的本地事务的并发
    此时就会存在脏读与脏写问题了,如果一个全局事务A,和一个本地事务b并发修改同一条数据,且全局事务处于本地分支事务已经提交全局事务仍在第一阶段的情况下,本地事务b修改了这条数据,此时脏写问题就出现了,此时也会有脏读,也读取到了其他事务操作的数据了,而且一旦全局事务第二阶段回滚就会造成本地事务b的提交数据的回滚覆盖问题。这种问题的根源在于AT模式下本地事务在一阶段是提交的。

1.8 解决AT模式下的脏读脏写问题

  • 方案一:增加GlobalTransactional注解
    解决方案也比较简单,只需要为非全局事务的事务加上GlobalTransactional就行,让他变成全局事务,这样就不会有问题了,全局事务的并发是需要先竞争全局锁的,失败的直接回滚了,所以可以解决这个问题,但这不是一个好的方案,因为全局事务的开启相对于本地事务是一个较重的操作,降低系统性能,那该如何做呢,使用下面一种方案更好一些。

  • 方案二:增加GlobalLock注解
    在本地事务上增加该注解,使用该注解后,本地事务开始时会先检查对应数据的全局锁是否存在,不存在才可以开启,如果全局锁存在则直接提示全局锁超时异常,本地事务也会回滚,达到的效果是和增加GlobalTransactional一样的,但是他只需要判断全局锁,而不需要真正的开启全局事务

通过上面两种方式(推挤方式二)就可以避免数据的脏读和回滚覆盖问题了。那脏读呢?脏读的问题是读取到了其他事务未提交的数据,在增加了全局锁以后是不可能再读取到了,因为获取全局锁失败就直接异常回滚了。

1.9 AT模式下全局锁与DB锁的死锁问题

正常场景下,全局事务都是先获取全局锁,然后再开启DB锁(数据库的排它锁),若是事务都是这种顺序就不会有死锁问题了,但问题出在可能会存在本地事务未增加全局锁的场景,此时就有可能出现了。
假设全局事务A和本地事务b,本地事务b锁定了一行记录,全局事务持有了全局锁,申请DB锁就会出现锁等待问题。不过全局锁有超时时间,超时会自动释放(全局锁申请超时30s,持有超时60s),所以死锁会被自动解开,但是使用时我们也应该避免这种情况出现,禁止操作全局事务的数据不加全局锁。

2.XA方案

XA模式Seata中使用的2PC,也就是未经过优化的2PC解决方案。对比2PC和AT的差异我们还容易就可以猜到他们的不同了

  • 1.XA不在需要undolog表,因为第一阶段会进行数据锁定不提交事务
  • 2.XA需要更改客户端数据源代理方式

undolog删不删不影响使用,但是数据源的代理方式必须得改,而且我们只需要更改代理方式,可以无需手动设置代理,在Seata客户端添加如下配置:

# 客户端添加配置
seata:
  data-source-proxy-mode: XA
  # 省略了其他配置.......

使用的话和上面没有任何区别,这里不重复贴代码了,展示下结果信息的差异,AT模式下是先删除undolog在二阶段处理完成,而XA模式下是不会有这个步骤的,因为他不依赖于undolog,XA回滚依赖的是数据库自身的undo信息。
在这里插入图片描述
XA的其他使用与AT并无任何区别,不重复说了。
注意XA没有全局锁因为他分支事务不提交

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/532409.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

pytesseract,一个超强的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个超强的 Python 库 - pytesseract。 Github地址&#xff1a;https://github.com/madmaze/pytesseract 在当今数字化时代&#xff0c;文字识别技术扮演着越来越重要的角色。…

基于sd的单分支和双分支方案

单分支&#xff1a; 1.ip-adapter 2.photomaker 3.CLE-Diffusion 4.powerpaint 5.videocomposer 6.animateanything 7.pixeldance 8.aniamtediff 双分支&#xff1a; 1.controlnet 2.instanceid 3.brushnet 4.ootdiffusion 5.MagicAnimate 7.animateanyone

《QT实用小工具·十七》密钥生成工具

1、概述 源码放在文章末尾 该项目主要用于生成密钥&#xff0c;下面是demo演示&#xff1a; 项目部分代码如下&#xff1a; #pragma execution_character_set("utf-8")#include "frmmain.h" #include "ui_frmmain.h" #include "qmessag…

qtablewidget 事件过滤器 鼠标事件不生效

1. 创建了一个MouseEventFilter类&#xff0c;它覆盖了eventFilter()方法来拦截鼠标按下事件。然后&#xff0c;我们将这个事件过滤器安装到了QTableWidget上 2.记得注册事件过滤器 3.这里是头文件 看一下就行 4.return true代表事件被处理&#xff0c;不需要再处理&#xff…

深入了解Redis——哨兵机制

三&#xff0c;Redis哨兵机制 Redis的哨兵机制主要是为了提高Redis主从模型下的可用性&#xff0c;能保证主节点异常发生时还能够正常的运作并进行故障转移。哨兵机制为了实现这一点提供了以下这些功能&#xff1a; 节点监控下线判断领导者选举slave选举故障转移 在介绍这些…

目标检测——车牌图像数据集

一、重要性及意义 车牌图像识别的重要性及意义主要体现在以下几个方面&#xff1a; 智能交通管理&#xff1a;车牌图像识别技术是智能交通系统&#xff08;ITS&#xff09;的核心组成部分。通过自动识别车辆车牌&#xff0c;可以实现对交通违章行为的自动记录和处理&#xff…

【数据库】数据库应用系统生命周期

目录 1.为什么提出”软件工程“的思想&#xff1f; 2.为什么提出”瀑布模型“&#xff1f;缺点是什么&#xff1f; 3.为什么提出”快速原型模型“&#xff1f; 4.为什么提出”螺旋模型“&#xff1f; 5.关于数据库的英文缩写。 6.模型设计中的3条设计主线&#xff1a;数…

OpenHarmony分布式软总线API调用测试工具 softbus_tool使用说明

softbus_tool 是 OpenHarmony 分布式软总线 API 调用测试工具&#xff0c;文件结构如下图所示。 softbus_tool 能够将软总线 interfaces 目录下的一些常用接口集中起来&#xff0c;供设备间搭建一些场景时使用&#xff08;比如设备绑定、BR 组网&#xff0c;BLE 组网&#xff…

低成本高效益,电子画册才是品牌的重要选择

随着互联网的普及和数字化技术的进步&#xff0c;电子画册已成为许多品牌的重要选择。与传统印刷画册相比&#xff0c;电子画册具有低成本、高效益的优点&#xff0c;成为品牌宣传的新趋势。 具体来说&#xff0c;电子画册可以通过在线平台或移动设备轻松查看&#xff0c;无需额…

【学习】软件测试中为什么要进行接口测试?

接口测试是软件开发过程中不可或缺的一环&#xff0c;它主要是对软件系统中各个模块之间的接口进行测试&#xff0c;以验证它们是否能够正确地交互和协作。接口测试的目的是确保软件系统的各个部分能够无缝地协同工作&#xff0c;从而提高整个系统的质量和稳定性。 一、什么是接…

allegro图片导入及调整的详细方法

目录 1. 图片转换2. 图片导入3. 导入图片调整3.1 图层调整 1. 图片转换 allegro只能导入IPF格式的文件&#xff1a; 正常情况下我们的图片都是JPG、BMP或者其他常见格式&#xff0c;需要将之转换为IPF格式才能导入&#xff0c;这里有工具。 需要工具在此 ->BMP转IPF工具 …

JDBC笔记

文章目录 JDBC相关JDBC-API详解一、DriverManager1.DriverManager(驱动管理类)作用 二、Connection1.Connection(数据库连接对象)作用2.具体操作**获取执行SQL对象****事务管理** 三、Statement1.Statement作用执行SQL语句 四、ResultSet1.ResultSet(结果集对象)作用2.获取查询…

SpringBoot菜品分页查询模块开发(多表连接查询)

需要注意的地方 为什么创建VO类怎么进行多表连接查询分页查询的统一返回结果类PageResult分页查询Mapper的返回结果是Page<目标实体类> 需求分析与设计 一&#xff1a;产品原型 系统中的菜品数据很多的时候&#xff0c;如果在一个页面中全部展示出来会显得比较乱&…

C语言—每日选择题—Day68

第一题 1、运行以下C语言代码&#xff0c;输出的结果是&#xff08;&#xff09; #include <stdio.h> int main() {char *str[3] {"stra", "strb", "strc"};char *p str[0];int i 0;while(i < 3){printf("%s ",p);i;} retur…

代码随想录算法训练营Day50|LC123 买卖股票的最佳时机IIILC188 买卖股票的最佳时机IV

一句话总结&#xff1a;虽然是困难题&#xff0c;其实都只需要对122 买卖股票的最佳时机II稍作变形即可。 原题链接&#xff1a;123 买卖股票的最佳时机III 相较于买卖股票的最佳时机和股票II来说&#xff0c;本题加了最多操作两次的限制。那么这里实际上就可以直接用滚动数组…

JVM垃圾收集——垃圾收集器

文章目录 1、垃圾收集器的发展和分类1.1、评估垃圾收集器的性能指标1.1.1、吞吐量1.1.2、停顿时间1.1.3、吞吐量和停顿时间的比较 1.2、垃圾收集器的发展史1.3、垃圾收集器的分类1.4、查看默认的垃圾收集器 2、Serial收集器&#xff1a;串行回收3、ParNew收集器&#xff1a;并行…

基于Springboot中小企业设备管理系统设计与实现(论文+源码)_kaic

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&a…

运筹说 第110期|生灭过程和Poisson过程

上一期我们一起学习了排队论的基本概念&#xff0c;本期小编将带大家学习生灭过程和Poisson过程。 下面&#xff0c;让我们一起来学习生灭过程和Poisson过程吧&#xff01; 一、生灭过程简介 01引言 在排队论中&#xff0c;如果N(t)表示时刻t系统中的顾客数&#xff0c;则{N…

【电控笔记5】电流环pi参数整定

旋转坐标系下的电压方程&#xff0c;由id和iq计算出ud和uq 把常数项&#xff08;上面两个红框&#xff09;拿出来解耦合&#xff0c;作为前馈&#xff0c;如下 电流环传递函数 电流带宽 响应时间 另一种方法&#xff1a;内膜控制器