分布式事务及SEATA介绍

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务中间件,主要解决微服务场景下的分布式事务问题。目前还在不断的开源升级中,其github链接:https://github.com/seata/seata

一.分布式事务产生背景

在介绍seata之前,首先说一下分布式事务的产生背景。首先,什么是事务?事务是作为逻辑单元执行的一组操作,要么全都成功,要么全失败。事务具有ACID特性。

提到事务,总绕不过经典的转账问题,将A账户上的1000元转到B上,最终目标是A账户-1000,B账户+1000,不能出现中间步骤。

一般情况下,我们通过spring框架中的@Transactional注解来保证单一数据源下的增删改查的一致性,但在以下几种情况,注解会失效:

1.跨数据库

随着业务的不断扩大,用户数再不断变多,百万千万甚至上亿的用户存在一张库甚至一张表中,为了解决数据库的瓶颈,分库是很常见的方案,不同的用户可能落在不同的库中,所以之前一个库里的事务操作,就变成了跨数据库的事务操作。

这种情况下,@Transactional注解就失效了,这就是跨数据库分布式事务问题。

2.微服务化

更多的情形是由于业务中的模块拆成了多个微服务后,同时调用多个微服务所产生的。
在这里插入图片描述

微服务下的银行转账情形是这样的:(1)调用交易系统服务创建订单 (2)调用支付系统服务记录支付明细 (3)调用账务系统执行A扣钱 (4)调用账务系统执行B扣钱。

每个系统对应一个独立的数据源,可能位于不同的机房。这就是跨服务分布式事务问题。

二.几种解决方案

1.两阶段提交(2PC)

说到两阶段提交,有一个比较简单易懂的例子,它有点类似游戏里打团本时候的过程,在组队打本的时候,为了方便成员间协作,队长会有就位确认的操作。
在这里插入图片描述

两阶段提交有两种角色:事务协调者和事务参与者。前者就好比上述的队长,而后者就类似队员。而“两阶段提交”的两阶段分别进行以下步骤:

第一阶段:协调者向参与者发送prepare请求,参与者收到请求后,本地执行与事务相关的数据更新等动作。执行完成后,告知参与者自己的决策:同意提交(本地事务执行成功),或者回滚(本地事务执行失败)。

第二阶段:如果协调者收到的都是同意,则向所有的参与者发送commit。参与者接到commit请求后,便提交本地事务,并释放锁资源。如果协调者收到了其中一个参与者发送的回滚请求,则向所有参与者发送abort。参与者收到abort请求后,回滚本地事务。

两阶段提交仍然存在着很多不足,比如:性能问题,单点故障问题等等。

2.TCC

2.1 TCC的基本原理

TCC是将事务提交分为try-catch-confirm三个阶段。它和两阶段提交有点类似,try为第一阶段,catch-confirm为第二阶段。与二阶段提交相比,它需要通过对业务逻辑的分解来实现分布式事务,是一种应用层面侵入业务的二阶段提交。另外TCC需要根据自身的业务模型来实现并发控制。

操作方法含义
Try预留业务资源/数据效验
Confirm确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等
Cancel取消执行业务操作,实际回滚数据,需保证幂等

还是以转账场景来举个简单的例子,这个场景需要两个TCC,一个加钱TCC,一个扣钱TCC。

首先对于扣钱的TCC,比如A转30元给B。A的账户余额有100元,需要扣除30元。这里的账户余额就是业务资源。所以在第一阶段,我们在try()中检查余额是否足够,并预留业务资源,即预留30元,其余的冻结。

在confirm接口中,将预留的30元扣除掉,而cancel是把扣掉的30元还给账户。

而对于加钱接口,try中不能直接加钱,因为第一阶段执行完后,加的钱就可以用了。而一阶段是可能回滚的,所以真正加钱的动作要放在confirm中,try可设为空操作,相应的,cancel中也没有资源可以释放,是一个空操作。只有真正提交时,再在confirm中给账户加钱。
对于具体业务场景而言,各组TCC中的操作都要针对业务模型自己设计并优化,包括并发控制。

2.2 TCC的异常控制

微服务架构下,很有可能出现网络超时,重发,机器宕机等一系列case,一旦遇到这些case,就会导致我们的分布式事务执行过程异常。从多年的使用情况看来,最主要的几种异常是空回滚,幂等和空悬挂。

空回滚

什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

什么样的情形会造成空回滚呢?简单来说,就是在调用一阶段catch操作的时候出现异常,比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

那会不会有空提交呢?理论上来说不会的,如果调用方宕机,那分布式事务默认是回滚的。如果是网络异常,那 RPC 调用失败,发起方应该通知 TC 回滚分布式事务,这里可以看出为什么是理论上的,就是说发起方可以在 RPC 调用失败的情况下依然通知 TC 提交,这时就会发生空提交,这种情况要么是编码问题,要么开发同学明确知道需要这样做。

那怎么解决空回滚呢?关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。因此,需要加分布式事务 ID 和分支事务 ID两个字段,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。我们可以在自己的业务表里加上这两个字段,也可以新建一张事务控制表。
在这里插入图片描述

幂等性

幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

什么样的情形会造成重复提交或回滚?提交或回滚是一次 TC 到参与者的网络调用。因此,网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。
在这里插入图片描述

那怎么解决空回滚呢?可以去记录每个分支的执行状态,每次执行前如果已执行了,就不再执行,否则正常执行。前面解决空回滚的时候已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

该状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。

防悬挂

首先什么是悬挂。悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

什么样的情况会造成悬挂?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。

怎么实现才能做到防悬挂?根据悬挂出现的条件先来分析下,悬挂是指二阶段 Cancel 执行完后,一阶段才执行。也就是说,为了避免悬挂,如果二阶段执行完成,那一阶段就不能再继续执行。因此,当一阶段执行时,需要先检查二阶段是否已经执行完成,如果已经执行,则一阶段不再执行;否则可以正常执行。那怎么检查二阶段是否已经执行呢?大家是否想到了刚才解决空回滚和幂等时用到的事务控制表,可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

这就是最主要的两种分布式事务的解决方案,下面说的seata实际上也是在这两种方案上做了一些改进和适配。

  1. 异步确保型

通过将一系列同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的阻塞操作的影响。

用消息队列来实现分布式事务是什么原理呢?举个例子,去很多饭店吃饭的时候,你付了钱之后,不会直接把饭给你,而是给你一张小票,凭票去排队区排队取饭。这张小票就是一个凭证,只要有这张小票在,就能确保你能拿到自己的饭。

分布式事务同理,那转账场景举例,当A账户扣除一万后,只要能生成一个凭证(消息),上面写着“B账户加一万元”。只要这条消息保存,最终就能凭这条消息让B账户加一万元。即完成最终一致性。而这种凭证的解决方案,有以下几种方式

(1)业务和消息耦合方式

A账户在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。

Begin transaction 
       update A set amount=amount-10000 where userId=1; 
       insert into message(userId, amount,status) values(1, 10000, 1); 
End transaction 
commit;

上述事务能保证只要A账户里被扣了钱,消息一定能保存下来。

当上述事务提交成功后,我们通过实时消息服务将此消息通知B账户,B账户处理成功后发送回复成功消息,A账户收到回复后删除该条消息数据。

(2)业务和消息解耦方式

要实现解耦,可以用消息队列。阿里的RocketMq实现了事务消息,可以实现分布式事务,接下来介绍一下

在这里插入图片描述

(3)事务消息

首先我们将会发送一个半(half) 消息到 MQ 中,通知其开启一个事务。这里半消息并不是说消息内容不完整,实际上它包含所有完整的消息内容。

这个半消息与普通的消息唯一的区别在于,在事物提交之前,这个消息对消费者来说是不可见的,消费者不会消费这个消息。一旦半消息发送成功,我们就可以执行数据库事务。然后根据事务的执行结果再决定提交或回滚事务消息。

如果事务提交成功,将会发送确认消息至 MQ,手续费系统就可以成功消费到这条消息。如果事务被回滚,将会发送回滚通知至 MQ,然后 MQ 将会删除这条消息。对于手续费系统来说,都不会知道这条消息的存在。这就解决了要么都成功,要么都失败的一致性要求。

如果我们提交/回滚事务消息失败怎么办?

对于这个问题,RocketMQ 给出一种事务反查的机制。我们需要需要注册一个回调接口,用于反查本地事务状态。RocketMQ 若未收到提交或回滚的请求,将会定期去反查回调接口,然后可以根据反查结果决定回滚还是提交事务。

不过呢,阿里已经把回查接口的实现给删除了,回查的接口不再开源了,目前公司的databus不支持事务消息,所以这种方法能否使用还有待研究。

三. SEATA的原理

seata是一款开源的分布式事务解决方案,主要针对性的解决两个问题:

对业务无侵入:即减少微服务化所带来的分布式事务问题对业务的侵入。业务代码最少只需要添加一行注解(@TxcTransaction)声明事务即可。业务与事务分离,将微服务从事务中解放出来,微服务关注于业务本身,不再需要考虑反向接口、幂等、回滚策略等复杂问题,极大降低了微服务开发的难度与工作量。
高性能:减少分布式事务解决方案带来的性能消耗。

seata主要有两种实现方案:AT模式和TCC模式。

AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
1.Seata架构
在这里插入图片描述

1.1 Seata术语

TC - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM - 资源管理器

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

这里提到了三个概念:全局事务,分支事务,资源。

1.2 什么是全局事务和分支事务?

Seata把一个分布式事务理解成了一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起提交,要么一起回滚。通常分支事务就是一个关系数据库的本地事务。

1.3 什么是资源?

AT 模式下,把每个数据库被当做是一个 Resource,Seata 里称为 DataSource Resource。业务通过 JDBC 标准接口访问数据库资源时,Seata 框架会对所有请求进行拦截,做一些操作。每个本地事务提交时,Seata RM(Resource Manager,资源管理器) 都会向 TC(Transaction Coordinator,事务协调器) 注册一个分支事务。当请求链路调用完成后,发起方通知 TC 提交或回滚分布式事务,进入二阶段调用流程。此时,TC 会根据之前注册的分支事务回调到对应参与者去执行对应资源的第二阶段。TC 是怎么找到分支事务与资源的对应关系呢?每个资源都有一个全局唯一的资源 ID,并且在初始化时用该 ID 向 TC 注册资源。在运行时,每个分支事务的注册都会带上其资源 ID。这样 TC 就能在二阶段调用时正确找到对应的资源。
TCC模式下Seata 框架把每组 TCC 接口当做一个 Resource,称为 TCC Resource。这套 TCC 接口可以是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自动扫描识别到 TCC 接口的调用方和发布方。如果是 RPC 的话,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。扫描到 TCC 接口的调用方和发布方之后。如果是发布方,会在业务启动时向 TC 注册 TCC Resource,与 DataSource Resource 一样,每个资源也会带有一个资源 ID。如果是调用方,Seata 框架会给调用方加上切面,与 AT 模式一样,在运行时,该切面会拦截所有对 TCC 接口的调用。每调用一次 Try 接口,切面会先向 TC 注册一个分支事务,然后才去执行原来的 RPC 调用。当请求链路调用完成后,TC 通过分支事务的资源 ID 回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 方法。

接下来分别说下AT模式和TCC模式(以下大部分内容来自seata官网:https://seata.io/zh-cn/docs/overview/what-is-seata.html

2.AT模式(业务侵入小)

2.1 整体机制

Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议。是二阶段提交的演变:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。——Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。降低锁范围,提高效率。

二阶段:

如果决议是全局提交,此时分支事务已经提交完成,提交异步化,清理日志,非常快速地完成。
如果决议是全局回滚,通过XID和BranchID找到相应的回滚日志记录,生成反向更新的sql并执行。

2.2 写隔离:

一阶段本地事务提交前,需要确保先拿到 全局锁 。
拿不到 全局锁 ,不能提交本地事务。
拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

2.3 读隔离

2.4 sql支持

Seata 事务目前支持 SELECT、INSERT、UPDATE、DELETE 的部分功能,这些类型都是已经经过Seata开源社区的验证。SQL 的支持范围还在不断扩大中。

使用限制

不支持 SQL 嵌套
不支持多表复杂 SQL
不支持存储过程、触发器
不支持批量更新 SQL

接下来以一个实例详细的介绍其工作机制,其中的重点就是回滚日志的使用。

业务表:product

FieldType
idbigint(20)
namevarchar(100)
sincevarchar(100)

AT 分支事务的业务逻辑:

update product set name = ‘GTS’ where name = ‘TXC’;

一阶段

过程:

解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

select id, name, since from product where name = 'TXC';

得到前镜像:

idnamesince
1TXC2014

执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
查询后镜像:根据前镜像的结果,通过 主键 定位数据。

select id, name, since from product where id = 1;

得到后镜像:

idnamesince
1GTS2014

插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
将本地事务提交的结果上报给 TC。

二阶段-回滚

收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = ‘TXC’ where id = 1;

提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

二阶段-提交

收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
3. TCC模式

TCC的原理和异常控制等内容前面已介绍过,seata对TCC做了适配。

四.结合springboot解读如何简单使用seata

这里我们依旧使用上面所述订单场景的示例,使用了springboot+Dubbo+Mybatis+Nacos+Seata来实现Dubbo分布式事务管理,使用Nacos作为Dubbo和seata的注册中心和配置中心,使用Mysql和Mybatis来操作数据。以下是从github上拉取的seata示例,按照官网文档进行的操作。

1.环境准备

1.1 nacos下载

在官网下载nacos,https://github.com/alibaba/nacos/releases/tag/1.1.3 并启动。

sh startup.sh -m standalone

在浏览器打开Nacos web 控制台:http://192.168.10.200:8848/nacos/index.html

输入nacos的账号和密码 分别为nacos:nacos

这个时候nacos成功启动。

1.2 seata server下载并启动

1.2.1 在 Seata Release 下载最新版的 Seata Server 并解压得到如下目录:

.
├──bin
├──conf
├──file_store
└──lib

1.2.2 conf/registry.conf 配置,

目前seata支持如下的file、nacos 、apollo、zk、consul的注册中心和配置中心。如果使用nacos,需要将配置文件中的type修改为“nacos”。

registry {
  # file nacos
  type = "nacos"

  nacos {
    serverAddr = "192.168.10.200:8848"
    namespace = ""
    cluster = "default"
  }
  file {
    name = "file.conf"
  }
}

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

  nacos {
    serverAddr = "192.168.10.200:8848"
    namespace = ""
  }

  file {
    name = "file.conf"
  }
}

这里再多嘴介绍一下什么是注册中心和配置中心

什么是配置中心?配置中心可以说是一个"大衣柜",内部放置着各种配置文件,你可以通过自己所需进行获取配置加载到对应的客户端.比如Seata Client端(TM,RM),Seata Server(TC),会去读取全局事务开关,事务会话存储模式等信息.
什么是注册中心?注册中心可以说是微服务架构中的”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用.比如Seata Client端(TM,RM),发现Seata Server(TC)集群的地址,彼此通信.

因为这里使用nacos作为配置中心和注册中心,将config和registry下的type都改为“nacos”。

serverAddr = “192.168.10.200:8848” :nacos 的地址
namespace = “” :nacos的命名空间默认为``
cluster = “default” :集群设置为默认 default

另外,seata0.9.0之后,配置如下, 其中namespace = “”

1.2.3 修改 conf/nacos-config.txt配置

配置的详细说明参考官网:https://seata.io/zh-cn/docs/user/configurations.html

这里主要修改了如下几项:

store.mode :存储模式 默认file 这里我修改为db 模式 ,并且需要三个表global_table、branch_table和lock_table
store.db.driver-class-name: 0.8.0版本默认没有,会报错。添加了 com.mysql.jdbc.Driver
store.db.datasource=dbcp :数据源 dbcp
store.db.db-type=mysql : 存储数据库的类型为mysql
store.db.url=jdbc:mysql://192.168.10.200:3306/seata?useUnicode=true : 修改为自己的数据库url、port、数据库名称
store.db.user=lidong :数据库的账号
store.db.password=cwj887766@@ :数据库的密码
service.vgroup_mapping.order-service-seata-service-group=default
service.vgroup_mapping.account-service-seata-service-group=default
service.vgroup_mapping.stock-service-seata-service-group=default
service.vgroup_mapping.business-service-seata-service-group=default
client.support.spring.datasource.autoproxy=true 开启数据源自动代理

db模式下的所需的三个表的数据库脚本位于seata\conf\db_store.sql

global_table的表结构

CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(64) DEFAULT NULL,
  `transaction_service_group` varchar(64) DEFAULT NULL,
  `transaction_name` varchar(64) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

branch_table的表结构

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(128) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

lock_table的表结构

create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(32) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

2.2.4 将 Seata 配置添加到 Nacos 中

2.2.5 启动seata server

2.服务启动

1.模块总览

库存服务:扣除给定商品的存储数量。——samples-stock :库存模块
订单服务:根据购买请求创建订单。——samples-order :订单模块
帐户服务:借记用户帐户的余额。——samples-account :用户账号模块
业务服务:处理业务逻辑。——samples-business :业务模块
samples-common :公共模块
请求的逻辑架构为:
在这里插入图片描述

先看下代码结构:
在这里插入图片描述

业务逻辑的具体实现主要体现在 订单服务的实现和业务服务的实现

订单服务实现代码

整个业务的实现逻辑

/**
     * 处理业务逻辑 正常的业务逻辑
     *
     * @Param:
     * @Return:
     */
    @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
    @Override
    public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
        log.info("开始全局事务,XID = " + RootContext.getXID());
        ObjectResponse<Object> objectResponse = new ObjectResponse<>();
        //1、扣减库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse stockResponse = stockDubboService.decreaseStock(commodityDTO);
        //2、创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse<OrderDTO> response = orderDubboService.createOrder(orderDTO);

        if (stockResponse.getStatus() != 200 || response.getStatus() != 200) {
            throw new DefaultException(RspStatusEnum.FAIL);
        }

        objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
        objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
        objectResponse.setData(response.getData());
        return objectResponse;
    }

我们只需要在业务处理的方法handleBusiness添加一个注解 @GlobalTransactional

timeoutMills: 超时时间
name :事务名称

2.数据库准备

运行数据库脚本构建数据库。这里需要建四张表。分别是:

与业务相关的t_account(账户表),t_order(订单表),t_stock(库存表)。
undo_log表。

3.application.yml配置,这里看一下seata的配置。

#====================================Seata Config===============================================
seata:
  enabled: true
  application-id: order-seata-example
  tx-service-group: my_test_tx_group # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
  client:
    rm-report-success-enable: true
    rm-table-meta-check-enable: false # 自动刷新缓存中的表结构(默认false)
    rm-report-retry-count: 5 # 一阶段结果上报TC重试次数(默认5)
    rm-async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000)
    rm:
      lock:
        lock-retry-internal: 10 # 校验或占用全局锁重试间隔(默认10ms)
        lock-retry-times: 30 # 校验或占用全局锁重试次数(默认30)
        lock-retry-policy-branch-rollback-on-conflict: true # 分支事务与其它全局回滚事务冲突时锁策略(优先释放本地锁让回滚成功)
    tm-commit-retry-count: 3 # 一阶段全局提交结果上报TC重试次数(默认1次,建议大于1)
    tm-rollback-retry-count: 3 # 一阶段全局回滚结果上报TC重试次数(默认1次,建议大于1)
    undo:
      undo-data-validation: true # 二阶段回滚镜像校验(默认true开启)
      undo-log-serialization: jackson # undo序列化方式(默认jackson)
      undo-log-table: undo_log  # 自定义undo表名(默认undo_log)
    log:
      exceptionRate: 100 # 日志异常输出概率(默认100)
    support:
      spring:
        datasource-autoproxy: true
  service:
    vgroup-mapping:
      my_test_tx_group: default # TC 集群(必须与seata-server保持一致)
    enable-degrade: false # 降级开关
    disable-global-transaction: false # 禁用全局事务(默认false)
    grouplist:
      default: 127.0.0.1:8091

tx-service-group:
事务分组:seata的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
service:
vgroup-mapping:
my_test_tx_group: default # TC 集群(必须与seata-server保持一致)指定事务分组至集群映射关系(冒号右侧的集群名需要与Seata-server注册到Nacos的cluster保持一致)

3.启动所有的sample模块

启动 samples-account-service、samples-order-service、samples-stock-service、samples-business-service

并且在nocos的控制台查看注册情况: http://192.168.10.200:8848/nacos/#/serviceManagement

我们可以看到上面的服务都已经注册成功。

4.测试

1.发送一个下单请求

启用postman,调用http://localhost:8104/business/dubbo/buy

返回结果如下:
在这里插入图片描述

控制台的输出:
在这里插入图片描述

看一下数据库里面的数据也都没有问题。
t_account:在这里插入图片描述

t_order:
在这里插入图片描述

2.回滚

可以将businessServiceimpl中的handleBusiness2中的代码注释去掉,用来测试发生错误时的回滚情况。

然后发送相同的请求,会发现响应结果报错。

account服务控制台日志

order服务控制台日志

order服务控制台日志

这个时候查看数据库里的数据,已经回滚了,和上面是一样的。另外需要注意的是,此时的undo_log表是空的,因为按照上面所说,回滚完成后undo_log中相应的记录就会被删除掉。想要看到undo_log中的数据,可以打断点来进行调试。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值