分布式事务与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阶段,用于检测事务开始之前各个分支事务的状态,所有分支事务状态都正常情况下才会开始事务,就可以有效避免了分支事务的无效锁定问题,减少了分支事务在异常场景下对数据的锁定时长(正常场景并不会减少锁定时长)。
- cancommit阶段相比于2PC到底优化了什么?
-
第二阶段:precommit
这个阶段可以完全类比2PC的prepare阶段。事务协调者收到各个分支事务在第一阶段回复的OK的信息后,进入第二阶段precommit阶段,事务协调者给所有分支事务发送precommit信息,分支事务收到信息后开始执行事务,DML同样是锁定数据记录(行锁+间隙锁),这个锁定结束与2PC相比来说多了一种。- 事务协调者通知:事务协调者会在第二阶段收到各个分支事务的返回,如果都是ok,则执行docommit,否则执行cancel
- 分支事务锁定超时:这与2PC没有啥区别,分支事务拥有超时机制,避免数据的长久锁定
- 事务协调者等待分支事务超时:3PC相比于2PC的另外一个不同的是引入了协调者的超时机制,当协调者在一定时间无法接受分支事务的回调后,则认为分支事务已经异常,将发送事务回滚信息给各个分支事务。
-
第三阶段:docommit
这个阶段和2PC的commit阶段是一样的,该阶段需要依赖3PC的各个分支事务在第二阶段的返回信息- 所有分支事务都成功了:
- 执行docommit:所有分支事务执行提交,然后将处理结果回传事务协调者
- 所有分支事务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,可靠消息,最大努力交付消息,事务消息,补偿,全局事务,自动补偿。可一站式解决以上分布式事务问题
- seata
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没有全局锁因为他分支事务不提交