我们先了解一下单体事务演变为分布式事务的过程。
1.单一事务
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示:
2.分布式事务
随着业务量的增长,一个单体服务已经不能承载主键递增的业务,此时很多系统都会做一些高性能的调整,比如服务拆分、分库分表,这就意味着,我们完成一个业务操作可能需要跨很多库才能完成,每个数据库只能保证自己的事务处理,而不能保证全局的事务一致性,这就出现了分布式事务来保证不同的资源数据一致性问题。常见的分布式事务场景有以下:
(1)跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂 的业务,一个业务中同时操作了9个库。下图演示了一个服务同时操作2个库的情况:
(2)分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆
分成了2个库:
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql:insert into user(id,name) values (1,“张三”),(2,“李四”)。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。 但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改 写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都 面临着分布式事务的问题。
(3)服务拆分(SOA)
微服务架构是目前一个比较一个比较火的概念。某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立 服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数 据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。
X/Open,即现在的open group,是一个独立的组织,主要负责制定各种行业技术标准。 就分布式事务处理
(Distributed Transaction Processing,简称DTP)而言,X/Open主要提供了以下参考文档:
DTP 参考模型: <>
DTP XA规范: << Distributed Transaction Processing: The XA Specification>>
构成DTP模型的5个基本元素:
**应用程序(Application Program ,简称AP):**用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资 源进行操作。
**资源管理器(Resource Manager,简称RM):**如数据库、文件系统等,并提供访问资源的方式。
**事务管理器(Transaction Manager ,简称TM):**负责分配事务唯一标识,监控事务的执行进度,并负责事务的提 交、回滚等。
**通信资源管理器(Communication Resource Manager,简称CRM):**控制一个TM域(TM domain)内或者跨TM域 的分布式应用之间的通信。
**通信协议(Communication Protocol,简称CP):**提供CRM提供的分布式应用节点之间的底层通信服务
在DTP本地模型实例中,由AP、RMs和TM组成,不需要其他元素。AP、RM和TM之间,彼此都需要进行交互,如下图所示:
这张图中(1)表示AP-RM的交互接口,(2)表示AP-TM的交互接口,(3)表示RM-TM的交互接口。
XA规范的最主要的作用是,就是定义了RM-TM的交互接口,XA规范除了定义的RM-TM交互的接口(XA Interface)之 外,还对两阶段提交协议进行了优化。
两阶段协议(two-phase commit)是在OSI TP标准中提出的;在DTP参考模型(<>)中,指定了全局事务的提交要使用two-phase commit协议;而XA规范(<< DistributedTransaction Processing: The XA Specification>>)只是定义了两阶段提交协议中需要使用到的接口,也就是上述提到 的RM-TM交互的接口,因为两阶段提交过程中的参与方,只有TM和RMs。
两阶段提交协议(Two Phase Commit)不是在XA规范中提出,但是XA规范对其进行了优化。而从字面意思来理解, Two Phase Commit,就是将提交(commit)过程划分为2个阶段(Phase):
阶段1:
TM通知各个RM准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就对工作内容进行持久化, 再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。在发送了否定答复并回滚了已经的工作后,RM就可以丢弃这个事务分支信息。 以mysql数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare"准备提交"请求,数据库收 到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成"可以提交",然后把结果返回给事务管理器
阶段2 :
TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有 的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。
以mysql数据库为例,如果第一阶段中所有数据库都prepare成功,那么事务管理器向数据库服务器发出"确认提 交"请求,数据库服务器把事务的"可以提交"状态改为"提交完成"状态,然后返回应答。如果在第一阶段内有任何一个数 据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库 服务器收不到第二阶段的确认提交请求,也会把"可以提交"的事务回撤。
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
1、同步阻塞问题。
两阶段提交方案下全局事务的ACID特性,是依赖于RM的。一个全局事务内部包含了多个独立的事务分支,这一组事 务分支要不都成功,要不都失败。各个事务分支的ACID特性共同构成了全局事务的ACID特性。也就是将单个事务分支的 支持的ACID特性提升一个层次到分布式事务的范畴。 即使在本地事务中,如果对操作读很敏感,我们也需要将事务隔离 级别设置为SERIALIZABLE。而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。如果我们使用mysql来支持XA分布式事务的话,那么最好将事务隔离级别设置为SERIALIZABLE,然而SERIALIZABLE(串行化)是四个事务隔离级别中最高的一个级别,也是执行效率最低的一个级别。
2、单点故障。
由于协调者的重要性,一旦协调者TM发生故障,参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那 么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
**3、数据不一致。**在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求
过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求,而在这部分参与者接到commit请求之后 就会执行commit操作,但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数 据不一致性的现象。 由于二阶段提交存在着诸如同步阻塞、单点问题等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。
三阶段提交(3PC),是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点:
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,
除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、 DoCommit三个阶段。
CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回
Yes响应,否则返回No响应。
1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并
进入预备状态。否则反馈No
PreCommit阶段
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事
务的中断。
1.发送中断请求 协调者向所有参与者发送abort请求。
2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
Case 1:执行提交1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送
doCommit请求。
2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
3.响应反馈 事务提交完之后,向协调者发送Ack响应。
4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
Case 2:中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超
时),那么就会执行中断事务。
1.发送中断请求 协调者向所有参与者发送abort请求
2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之
后释放所有的事务资源。
3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会
继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了
PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的
CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就
是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功
提交的几率很大。 )
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会
默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络
原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其
他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。
TCC是try confirm cancel的单词首字母缩写,是一个类似2pc理论的柔性分布式事务处理的解决
**Try操作:**对业务进行检查,比如检查数据库资源是否充足,然后在业务隔离层隔离业务活动需要的资源;
Confirm操作:这个操作的前提是Try操作成功执行,这一步是执行业务,并且无需检查资源情况,直接使用Try操作隔离预留的资源;
**Cancel操作:**执行这个操作是因为Try操作失败,释放掉Try操作预留的资源;
服务与服务之间通过请求/响应的同步通信机制进行交互;
主服务是提供对外接口,接收客户端请求,并且发起一个全局的业务活动并编排所有的事务参与者;
从服务是全局事务的参与者,提供Try、Confirm、Cancel三个接口,通过调用这些接口来使从服务的事务完成;
全局事务管理器是整个分布式事务的协调者,记录着全局事务管理的执行日志和事务状态,在Try操作阶段完成后根据成功与否调动Confirm或者Cancel接口,全局事务管理器是一个单独的服务。
通常有三种异常处理:空回滚、幂等、悬挂
**空回滚:**在没有执行Try 操作的情况下,调用 Cancel 操作,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功;
主要是因为分支事务所在的服务异常或网络异常,进而使调用记录为失败;也就是说,如果执行了Try操作就正常进行回滚,如果没有执行Try操作,那就是执行空回滚;执行了Try操作事务日志里会存在记录,Cancel操作可以根据有无记录判断是否为空回滚。
**幂挂:**确保 Confirm提交重试机制不会引发数据不一致,要求 TCC Try、Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。
悬挂: 就是Cancel 操作比 Try 操作先执行;
在全局事务管理器调用分支事务 的Try 操作时,是先注册分支事务,再执行全局事务管理器调用,如果此时全局事务管理器调用的网络发生拥堵,通常全局事务管理器调用是有超时时间的,全局事务管理器超时以后,就会回滚该分布式事务,可能回滚完成之后,全局事务管理器请求才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,则Try操作留出来的资源无法被利用造成悬挂。
简单来说:
TCC是第一阶段就把事务commit了(try接口),TCC的第二阶段是一个确认(Confirm)的阶段,也就是说只需要调用各个子系统里的confirm逻辑,所以在执行confirm逻辑的时候,并不会持有数据库的锁,所以不会产生性能问题。
XA是一个分布式事务规范,XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:
XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
官网:https://seata.io/zh-cn/index.html
源码: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples
seata版本:v1.4.0
在 Seata 的架构中,一共有三个角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。其中,TC 为单独部署的 Server 服务端,TM 和 RM嵌入到应用中的 Client 客户端。
在 Seata 中,一个分布式事务的生命周期如下:
1.TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务
的子事务关联在一起。
2.RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
3.TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
4.TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图
第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时
入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析
参考官方文档: https://seata.io/zh-cn/docs/dev/mode/at-mode.html
第二阶段
分布式事务操作成功,则TC通知RM异步删除undolog
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记
录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
整体执行流程
相比与其它分布式事务框架,Seata架构的亮点主要有几个:
应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
通过全局锁实现了写隔离与读隔离。
性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一 次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败 需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应 用开发一个补偿交易是否是值得?
全局锁
热点数据
相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销 比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点 数据,这个问题会更加严重。
回滚锁释放时间
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
以Windows下安装示例
Seata版本:1.4.0
SpringBoot版本:2.1.1.RELEASE
SpringCloud版本:Greenwich.SR3
SpringCloudAlibaba版本:2.1.1.RELEASE
1.下载Seata服务端代码
https://github.com/seata/seata
2.服务端代码是没有script代码的,我们需要去github源码目录找到script目录复制到本地(scipt存放着我们的配置文件和推送配置的脚本,后边会用到)
3.修改conf目录下的registry.conf文件
4.找到script/server/db下的mysql.sql
5.执行其中的脚本
-- -------------------------------- 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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
6.找到script/config-center目录下的config.txt,并修改以下信息
8.把config.txt文件的内容推送到nacos服务端
打开script\config-center\nacos目录,有两个文件一个是nacos-config.py 一个是nacos-config.sh 我们执行下边这个命令
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t 06e16720-5262-4ea3-9d3b-4ef211a69e67 -u nacos -w nacos
#-h:nacos地址
#-p:端口,默认8848
#-g:seata的服务列表分组名称
#-t:nacos命名空间id
#-u和-w:nacos的用户名和密码
9.这样我们就可以在nacos控制台看到Seata的配置信息了
10.启动服务端
双击seata\bin\seata.server.bat即可启动,出现以下界面表示启动成功
客户端有三个,订单服务、库存服务、账户服务,这里只展示一个服务的搭建过程
1.引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.1.1.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Greenwich.SR3version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.1.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-allartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.4.0version>
dependency>
这里有个版本问题spring cloud alibaba 2.1.2 及其以上版本使用seata1.4.0会出现如下异常 (支持seata 1.3.0)
2.每个微服务创建自己的undo_log日志表
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `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;
3.修改registry.conf,和服务端保持一致
4.修改yaml配置 tx-service-group要和服务端配置的保持一致 (SpringCLoudAlibaba 2.1.4支持在yaml中配置)
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 199787
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
feign:
sentinel:
enabled: true
5.添加配置类
//指定mapper扫描路径
@Configuration
@MapperScan("com.qy.springcloud.dao")
public class MyBatisConfig {
}
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
//由Seata代理数据源
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
//设置主键自增
configuration.setUseGeneratedKeys(true);
//启动驼峰映射
configuration.setUseColumnLabel(true);
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
}
6.主启动类要去掉数据源的自动装配,负责会有循环依赖问题
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class AccountMain2003 {
public static void main(String[] args) {
SpringApplication.run(AccountMain2003.class,args);
}
}
7.启动微服务客户端,服务端出现以下信息表示我们的客户端也配置成功
1.在事务发起者添加@GlobalTransactional注解,在服务调用方做一个异常
@Override
@GlobalTransactional
public void createOrder(Order order) {
log.info("开始创建订单");
log.info("当前 XID: {}", RootContext.getXID());
//创建订单
orderDao.createOrder(order);
log.info("订单创建成功");
//变更库存
log.info("开始远程调用库存服务,更改库存");
Response storageResult = storageService.decrease(order.getProductId(), order.getCount());
log.info("结束远程调用库存服务,更改库存");
//变更账户金额
log.info("开始远程调用账户服务,更改账户金额");
Response accountResult = accountService.decrease(order.getUserId(), order.getMoney());
log.info("结束远程调用账户服务,更改账户金额");
log.info("下订单成功,O(∩_∩)O");
int i = 10/0;
}
}
2.访问接口http://localhost:2001/order/createOrder
3.查看三张表的数据,订单没有创建,库存没有扣减、账户也没有扣减
订单表
账户表
库存表
此时到达了被调用方和调用方的事务一致性。(把异常搞到被调用方效果也是一样的,可以自行尝试)