使用Seata解决分布式事务问题
最近看黑马微服务课程学习了分布式事务的相关原理,使用 Seata 保障了分布式事务的一致性。
一、分布式事务的原理?
1.分布式事务的概念
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务的事务就是一个分支事务。整个业务称为全局事务。
2.分布式事务的解决思路
分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单: 就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。
3.本项目问题分析
在订单服务中,当执行下单事务时,会调用到购物车服务的清理购物车事务和商品服务的扣减商品库存事务,由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务。整个业务中,各个本地事务是有关联的。因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败。为了保障事务的一致性,我们使用了Seata来解决。
二、Seata原理
Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,旨在提供高性能和简单易用的分布式事务服务。Seata支持多种事务模式,包括AT、TCC、SAGA和XA模式。
Seata的核心组件
Seata主要由以下三个核心组件组成:
- 事务协调器(TC):维护全局事务的运行状态,负责协调并驱动全局提交或回滚。
- 事务管理器(TM):事务发起方,控制全局事务的范围,负责开启一个全局事务,并最终发起全局提交或回滚。需要告知 TM 方法的入口来监控方法,方法执行完报告结束。
- 资源管理器(RM):事务参与方,管理本地事务正在处理的资源,负责向TC注册本地事务、汇报本地事务状态,接收TC的命令来驱动本地事务的提交或回滚。
其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。故使用Seata需要搭建独立的 TC 服务和在微服务中引入 seata 依赖。
Seata的事务模式
- AT模式(Automatic Transaction):无侵入式的分布式事务解决方案,适合不希望对业务进行改造的场景。Seata会在第一阶段拦截并解析用户的SQL,生成更新前后的镜像数据,形成undo log,并自动生成事务第二阶段的提交和回滚操作。
- TCC模式(Try-Confirm-Cancel):高性能的分布式事务解决方案,适用于对性能要求较高的场景。业务开发者需要实现Try、Confirm和Cancel三个操作。
- SAGA模式:适用于业务流程长且需要保证事务最终一致性的业务系统。SAGA模式在第一阶段提交本地事务,无锁,长流程情况下可以保证性能。
- XA模式:基于数据库实现的分布式事务协议,适用于需要严格事务一致性的场景。
Seata的执行流程
Seata的分布式事务执行流程可以大致分为两阶段:
- 第一阶段:TM向TC申请开启一个全局事务,生成唯一的全局事务标识XID。RM向TC注册分支事务,汇报资源准备状况,并与XID进行绑定。
- 第二阶段:TM向TC发起XID下的所有分支事务的全局提交或回滚请求。TC汇总事务信息,决定分布式事务是提交还是回滚,并通知所有RM提交或回滚资源。
三、部署 TC 服务
1.导入数据库表 seata-tc.sql
Seata支持多种存储模式,但考虑到持久化的需要,我们一般选择基于数据库存储。执行课前资料提供的《seata-tc.sql》,导入数据库表:
2.准备配置文件,上传到虚拟机。
课前资料准备了一个seata目录,其中包含了seata运行时所需要的配置文件。我们将整个seata文件夹拷贝到虚拟机的目录下。
3.Docker 部署 seata。
要确保nacos、mysql都在hmall网络中。使用 docker network connect [网络名] [容器名] 把 nacos 加入到 docker 的 hmall 网络中。
[root@localhost ~]# docker network connect hmall nacos
[root@localhost ~]# docker network inspect hmall
[
{
"Name": "hmall",
"Id": "552cf8d3197cd5b0b0748f118cc6c06088a22bebf1cb4,
"Created": "2024-10-06T18:55:52.622410748+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"6730a8bc926cbc71dcae9e543cab0124c3c0aa9f469cca0{
"Name": "nacos",
"EndpointID": "e692c7b119fadc979487dd6e024ec47b4dfd70cad7b7",
"MacAddress": "02:42:ac:13:00:03",
"IPv4Address": "172.19.0.3/16",
"IPv6Address": ""
},
"a568d1a93d44d944a116d5f87581a54b140c9702be7df7f{
"Name": "mysql",
"EndpointID": "db7ee9f800b83f9f47377247fbd6af54f74556333274",
"MacAddress": "02:42:ac:13:00:02",
"IPv4Address": "172.19.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "hm-net",
"com.docker.compose.project": "root",
"com.docker.compose.version": "2.27.1"
}
}
]
把资料里的 seata-1.5.2.tar 镜像复制到虚拟机的某目录,并在该目录使用 docker load 命令加载镜像seataio/seata-server:1.5.2。
[root@localhost ~]# ls
anaconda-ks.cfg docker-compose.yml hm-service.jar nacos
demo Dockerfile mysql nginx
[root@localhost ~]# cd seata
[root@localhost seata]# ls
seata seata-1.5.2.tar
[root@localhost seata]# docker load -i seata-1.5.2.tar
f1b5933fe4b5: Loading layer 5.796MB/5.796MB
9b9b7f3d56a0: Loading layer 3.584kB/3.584kB
edd61588d126: Loading layer 80.28MB/80.28MB
1d1c7c0590ed: Loading layer 92.65MB/92.65MB
8909b2b120e4: Loading layer 4.394MB/4.394MB
89270025dfd6: Loading layer 65.02kB/65.02kB
52888cf30d11: Loading layer 687.6kB/687.6kB
0f1a3ddd34be: Loading layer 3.448MB/3.448MB
Loaded image: seataio/seata-server:1.5.2
[root@localhost seata]# dis
REPOSITORY AGE ID CREATED SIZE
rent 6f8521de97 2 months ago 731MB
root-hmall 93632b323d 3 months ago 365MB
nginx 27c0f683c3 5 months ago 188MB
redis 49ed81b42b 5 months ago 117MB
mysql 57d623b190 6 months ago 586MB
seataio/seata-server a5368b6720 2 years ago 186MB
nacos-registry.cn-hangzhou.cr.aliyuncs.com/nacos/nacos-serveaddbd025a1 2 years ago 322MB
用 docker run 命令将seataio/seata-server:1.5.2镜像部署为 seata 容器并添加到 hmall 网络中,以便 nacos 能访问到。注意seata容器的IP地址要改为自己的虚拟机地址。
[root@localhost seata]# docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.239.146 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hmall \
-d \
seataio/seata-server:1.5.2
196a345ea778fd10bfb35a27e5c3d267ff00ed029239e4df0d91d21a3efe
[root@localhost seata]# docker ps
CONTAINER ID IMAGE CREATED STATUS PORTS NAMES
196a345ea778 seataio/seata-server:1.5.2 13 seconds ago Up 6 seconds 7091/tcp, 0.0.0.0:7099->7099/tcp, :::8099->8099/tcp seata
6730a8bc926c nacos-registry.cn-hangzhou.cr.aliyuncs.com/na 3 months ago Up 2 days 0.0.0.0:8848->8848/tcp, :::9848-9849->9848-9849/tcp nacos
a568d1a93d44 mysql 3 months ago Up 2 days 0.0.0.0:3306->3306/tcp, : mysql
此时在hmall网络中可以查看到nacos,seata,mysql三个容器均已添加进来。
[root@localhost seata]# docker network inspect hmall
[
{
"Name": "hmall",
"Id": "552cf8d3197cd5b0b0748f118cc6c06088a22bebf1cb4
"Created": "2024-10-06T18:55:52.622410748+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"196a345ea778fd10bfb35a27e5c3d267ff00ed029239e4d
"Name": "seata",
"EndpointID": "047ead758a071b8805d95642d38ad
"MacAddress": "02:42:ac:13:00:04",
"IPv4Address": "172.19.0.4/16",
"IPv6Address": ""
},
"6730a8bc926cbc71dcae9e543cab0124c3c0aa9f469cca0
"Name": "nacos",
"EndpointID": "e692c7b119fadc979487dd6e024ec
"MacAddress": "02:42:ac:13:00:03",
"IPv4Address": "172.19.0.3/16",
"IPv6Address": ""
},
"a568d1a93d44d944a116d5f87581a54b140c9702be7df7f
"Name": "mysql",
"EndpointID": "db7ee9f800b83f9f47377247fbd6a
"MacAddress": "02:42:ac:13:00:02",
"IPv4Address": "172.19.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "hm-net",
"com.docker.compose.project": "root",
"com.docker.compose.version": "2.27.1"
}
}
]
my error:如果用docker run命令部署时把网络名写错了,会导致创建了容器却没有启动,再次用docker run命令时会提示容器名重复。此时应该删除创建的容器,重新运行docker run命令。或者用docker start seata启动容器,并用docker network connect hmall seata把seata容器加入hmall网络。
四、在微服务中集成 seata 客户端
1.在项目中引入 seata 依赖
为了方便各个微服务集成seata,我们需要把seata配置共享到nacos,因此trade-service模块不仅仅要引入seata依赖,还要引入nacos依赖。
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2.在微服务中配置 TC 服务地址
在 nacos 上添加一个共享的 seata 配置,命名为 shared-seata.yaml:
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.239.146:8848 # nacos地址要改成自己的
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
在cart微服务的bootstrap.yaml里的nacos共享配置中添加对文件shared-seata.yaml的引用。同理为item和trade服务也使用bootstrap.yaml配置,并修改application.yaml配置。
bootstrap.yaml配置如下:
spring:
application:
name: trade-service # 微服务名称
# 1.用于在 nacos中注册微服务名称
# 2.用于 nacos 中的共享配置 shared-log.yaml读取信息
# profiles:
# active: dev
# 这部分是被注释掉的配置,用于指定 Spring Boot 的活动配置文件(profile)。例如,active: dev 表示使用开发环境的配置文件。也可以在spring的启动类配置指定。
cloud:
# 这是 Spring Cloud 的配置节点
nacos:
# 这是 Nacos 的配置节点
server-addr: 192.168.239.146:8848
# 这是 Nacos 服务器的地址和端口号,用于连接 Nacos 配置中心和服务注册中心。
config:
# config::这是 Nacos 配置的子节点。
file-extension: yaml
# file-extension: yaml:指定配置文件的扩展名为 yaml。
shared-configs:
# shared-configs::这是共享配置的列表,表示从 Nacos 中加载的共享配置文件
- data-id: shared-jdbc.yaml
# 共享配置文件 shared-jdbc.yaml,通常用于数据库配置。
- data-id: shared-log.yaml
# 共享配置文件 shared-log.yaml,通常用于日志配置。
- data-id: shared-swagger.yaml
# 共享配置文件 shared-swagger.yaml,通常用于 Swagger 配置。
- data-id: shared-seata.yaml
# 共享配置文件 shared-seata.yaml,通常用于 seata 配置
jdk非11时,会出现启动失败的报错信息,需要在启动类的配置中添加虚拟机JVM参数--add-opens java.base/java.lang=ALL-UNNAMED,允许反射访问受限的模块。
--add-opens 是一个JVM参数,用于在模块化的Java 9及以上版本中打开模块的特定包,以便其他模块可以访问该包中的非公共成员。具体来说,--add-opens java.base/java.lang=ALL-UNNAMED 的含义如下:
java.base:这是Java平台的基础模块,包含核心类库。java.lang:这是java.base模块中的一个包,包含基本的Java类,如String、Object等。ALL-UNNAMED:表示将java.lang包中的所有非公共成员开放给所有未命名模块(即未显式声明模块的代码)。
这个参数通常用于解决模块化系统中的访问限制问题,特别是在使用反射或第三方库时。
启动所有微服务,可以在虚拟机的seata日志中看到下面的信息。这些日志信息表明,trade-service、cart-service和item-service三个服务的TM和RM都成功注册到Seata的TC,并且都使用了客户端版本1.5.2。这意味着这些服务已经准备好参与分布式事务。
09:15:00.677 INFO --- [ettyServerNIOWorker_1_1_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='trade-service', transactionServiceGroup='hmall'},channel:[id: 0x174690ba, L:/172.19.0.4:8099 - R:/192.168.239.1:1807],client version:1.5.2
09:15:02.792 INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://192.168.239.146:3306/hm-trade', applicationId='trade-service', transactionServiceGroup='hmall'},channel:[id: 0x6dc3f9e6, L:/172.19.0.4:8099 - R:/192.168.239.1:1825],client version:1.5.2
09:15:05.459 INFO --- [ettyServerNIOWorker_1_3_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='cart-service', transactionServiceGroup='hmall'},channel:[id: 0x09570acc, L:/172.19.0.4:8099 - R:/192.168.239.1:1846],client version:1.5.2
09:15:07.857 INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://192.168.239.146:3306/hm-cart', applicationId='cart-service', transactionServiceGroup='hmall'},channel:[id: 0x539dae2d, L:/172.19.0.4:8099 - R:/192.168.239.1:1853],client version:1.5.2
09:15:08.032 INFO --- [ettyServerNIOWorker_1_5_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='item-service', transactionServiceGroup='hmall'},channel:[id: 0xf7997c65, L:/172.19.0.4:8099 - R:/192.168.239.1:1857],client version:1.5.2
09:15:09.585 INFO --- [rverHandlerThread_1_3_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://192.168.239.146:3306/hm-item', applicationId='item-service', transactionServiceGroup='hmall'},channel:[id: 0x23d69cb2, L:/172.19.0.4:8099 - R:/192.168.239.1:1867],client version:1.5.2
五、XA模式
1.XA模式的原理:
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对 XA 规范 提供了支持。
Seata的XA模式如下:
一阶段的工作:
- RM注册分支事务到TC
- RM执行分支业务sql但不提交
- RM报告执行状态到TC
二阶段的工作:
- TC检测各分支事务执行状态 如果都成功,通知所有RM提交事务 如果有失败,通知所有RM回滚事务
- RM接收TC指令,提交或回滚事务
2.XA模式的优缺点:
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
3.实现XA模式:
(1)修改application.yaml文件,开启XA模式。
在配置文件中指定要采用的分布式事务模式。我们可以在Nacos中的共享shared-seata.yaml配置文件中设置:
(2)给发起全局事务的入口方法添加@GlobalTransactional注解。
(3)重启服务并测试
先打开虚拟机的seata服务并用默认账号密码admin登录,以便微服务能够连接到。
设置某商品库存为100,将其加入购物车,点击结算。
此时购物车内容还没有被删除,接着在mysql中将该商品的库存修改为0,提交订单,提示“下单失败”。
此时观察后端服务的日志信息。trade服务显示“库存不足!”
cart服务显示发生了分支回滚。
rm handle branch rollback process:xid=192.168.239.146:8099:45648435336224769,branchId=45648435336224778,branchType=XA,resourceId=jdbc:mysql://192.168.239.146:3306/hm-cart,applicationData=null
13:59:56:085 INFO 22260 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.239.146:8099:45648435336224769 45648435336224778 jdbc:mysql://192.168.239.146:3306/hm-cart
13:59:56:102 INFO 22260 --- [h_RMROLE_1_2_32] i.s.rm.datasource.xa.ResourceManagerXA : 192.168.239.146:8099:45648435336224769-45648435336224778 was rollbacked
13:59:56:103 INFO 22260 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
从日志信息来看,Seata在处理XA模式的分支回滚过程。以下是详细解读:
-
INFO日志:
- 时间:13:59:56:085
- 组件:
io.seata.rm.AbstractRMHandler - 信息:处理分支回滚过程,具体信息为
xid=192.168.239.146:8099:45648435336224769, branchId=45648435336224778, branchType=XA, resourceId=jdbc:mysql://192.168.239.146:3306/hm-cart, applicationData=null。 - 解释:Seata的资源管理器(RM)正在处理一个XA模式的分支回滚请求,涉及到资源
jdbc:mysql://192.168.239.146:3306/hm-cart。
-
INFO日志:
- 时间:13:59:56:102
- 组件:
io.seata.rm.datasource.xa.ResourceManagerXA - 信息:回滚成功,具体信息为
192.168.239.146:8099:45648435336224769-45648435336224778 was rollbacked。 - 解释:Seata的XA资源管理器成功回滚了分支事务,涉及到的XID和分支ID分别为
192.168.239.146:8099:45648435336224769和45648435336224778。
-
INFO日志:
- 时间:13:59:56:103
- 组件:
io.seata.rm.AbstractRMHandler - 信息:分支回滚结果,具体信息为
Branch Rollbacked result: PhaseTwo_Rollbacked。 - 解释:Seata的资源管理器成功完成了分支回滚过程,回滚结果为
PhaseTwo_Rollbacked,表示第二阶段回滚成功。
这些日志信息表明,Seata在XA模式下成功处理了分支回滚请求,并且回滚过程顺利完成。
此时观察mysql中cart服务的数据库信息,也并没有被删除,可以验证事务回滚确实发生了。
item服务在更新库存时报错,提示库存不足,并发生了分支回滚。
rm handle branch rollback process:xid=192.168.239.146:8099:45648435336224769,branchId=45648435336224772,branchType=XA,resourceId=jdbc:mysql://192.168.239.146:3306/hm-item,applicationData=null
13:59:56:179 INFO 23832 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.239.146:8099:45648435336224769 45648435336224772 jdbc:mysql://192.168.239.146:3306/hm-item
13:59:56:183 INFO 23832 --- [h_RMROLE_1_2_32] i.s.rm.datasource.xa.ResourceManagerXA : 192.168.239.146:8099:45648435336224769-45648435336224772 was rollbacked
13:59:56:183 INFO 23832 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
从日志信息来看,Seata在处理XA模式的分支回滚过程。以下是详细解读:
-
INFO日志:
- 时间:13:59:56:179
- 组件:
io.seata.rm.AbstractRMHandler - 信息:处理分支回滚过程,具体信息为
xid=192.168.239.146:8099:45648435336224769, branchId=45648435336224772, branchType=XA, resourceId=jdbc:mysql://192.168.239.146:3306/hm-item, applicationData=null。 - 解释:Seata的资源管理器(RM)正在处理一个XA模式的分支回滚请求,涉及到资源
jdbc:mysql://192.168.239.146:3306/hm-item。
-
INFO日志:
- 时间:13:59:56:183
- 组件:
io.seata.rm.datasource.xa.ResourceManagerXA - 信息:回滚成功,具体信息为
192.168.239.146:8099:45648435336224769-45648435336224772 was rollbacked。 - 解释:Seata的XA资源管理器成功回滚了分支事务,涉及到的XID和分支ID分别为
192.168.239.146:8099:45648435336224769和45648435336224772。
-
INFO日志:
- 时间:13:59:56:183
- 组件:
io.seata.rm.AbstractRMHandler - 信息:分支回滚结果,具体信息为
Branch Rollbacked result: PhaseTwo_Rollbacked。 - 解释:Seata的资源管理器成功完成了分支回滚过程,回滚结果为
PhaseTwo_Rollbacked,表示第二阶段回滚成功。
这些日志信息表明,Seata在XA模式下成功处理了分支回滚请求,并且回滚过程顺利完成。
此时观察mysql中item服务的数据库信息,也并没有被删除,可以验证事务回滚确实发生了。
六、AT模式
1.AT模式的原理
Seata主推的是AT模式,AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
Seata的AT模式如下:
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
2.AT与XA的对比
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致。
如何选择XA和AT模式?
看业务对一致性和性能要求来选择。如果业务对一致性要求不那么高,侧重性能,允许出现短暂的不一致,则选AT模式。如果对于一致性要求高,绝对不允许出现不一致,则选择XA模式。
3.实现AT模式:
(1)添加seata-at.sql即undo_log表到微服务对应的数据库中。
seata的客户端在解决分布式事务的时候需要记录一些中间数据即AT模式的数据快照,保存在数据库中。因此我们要先准备一个这样的表。将课前资料的seata-at.sql分别文件导入hm-trade、hm-cart、hm-item三个数据库中。
(2)修改application.yml文件,将事务模式改为AT模式。
(3)给发起全局事务的入口方法添加@GlobalTransactional注解。
(4)重启服务并测试
将商品库存改回100,加入购物车,结算。
在mysql数据库中将该商品的库存修改为0,提交订单,提示“下单失败”。
观察后端日志信息。trade服务显示“库存不足!”
cart服务发生了事务回滚。
: ==> Preparing: DELETE FROM cart WHERE (user_id = ? AND item_id IN (?))
15:04:38:560 DEBUG 24656 --- [nio-8082-exec-7] com.hmall.cart.mapper.CartMapper.delete : ==> Parameters: 1(Long), 100001986110(Long)
15:04:38:571 DEBUG 24656 --- [nio-8082-exec-7] com.hmall.cart.mapper.CartMapper.delete : <== Updates: 1
15:04:38:979 INFO 24656 --- [h_RMROLE_1_2_32] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=192.168.239.146:8099:45648435336224938,branchId=45648435336224945,branchType=AT,resourceId=jdbc:mysql://192.168.239.146:3306/hm-cart,applicationData={"autoCommit":false}
15:04:38:981 INFO 24656 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.239.146:8099:45648435336224938 45648435336224945 jdbc:mysql://192.168.239.146:3306/hm-cart
15:04:39:088 INFO 24656 --- [h_RMROLE_1_2_32] i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.239.146:8099:45648435336224938 branch 45648435336224945, undo_log deleted with GlobalFinished
15:04:39:091 INFO 24656 --- [h_RMROLE_1_2_32] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
从日志信息来看,Seata在处理AT模式的分支回滚过程。以下是详细解读:
-
SQL执行:
- 时间:15:04:38:560
- 组件:
com.hmall.cart.mapper.CartMapper.delete - 信息:准备执行SQL语句
DELETE FROM cart WHERE (user_id = ? AND item_id IN (?)),参数为1(Long)和100001986110(Long)。 - 结果:更新了1条记录。
-
INFO日志:
- 时间:15:04:38:979
- 组件:
RmBranchRollbackProcessor - 信息:处理分支回滚过程,具体信息为
xid=192.168.239.146:8099:45648435336224938, branchId=45648435336224945, branchType=AT, resourceId=jdbc:mysql://192.168.239.146:3306/hm-cart, applicationData={"autoCommit":false}。 - 解释:Seata的资源管理器(RM)正在处理一个AT模式的分支回滚请求,涉及到资源
jdbc:mysql://192.168.239.146:3306/hm-cart。
-
INFO日志:
- 时间:15:04:38:981
- 组件:
io.seata.rm.AbstractRMHandler - 信息:分支回滚中,具体信息为
Branch Rollbacking: 192.168.239.146:8099:45648435336224938 45648435336224945 jdbc:mysql://192.168.239.146:3306/hm-cart。 - 解释:Seata的资源管理器正在回滚分支事务,涉及到的XID和分支ID分别为
192.168.239.146:8099:45648435336224938和45648435336224945。
-
INFO日志:
- 时间:15:04:39:088
- 组件:
AbstractUndoLogManager - 信息:删除undo log,具体信息为
xid 192.168.239.146:8099:45648435336224938 branch 45648435336224945, undo_log deleted with GlobalFinished。 - 解释:Seata的undo log管理器成功删除了undo log,表示全局事务已完成。
-
INFO日志:
- 时间:15:04:39:091
- 组件:
io.seata.rm.AbstractRMHandler - 信息:分支回滚结果,具体信息为
Branch Rollbacked result: PhaseTwo_Rollbacked。 - 解释:Seata的资源管理器成功完成了分支回滚过程,回滚结果为
PhaseTwo_Rollbacked,表示第二阶段回滚成功。
这些日志信息表明,Seata在AT模式下成功处理了分支回滚请求,并且回滚过程顺利完成。
购物车内容没有被清空,证明确实实现事务回滚了。
item服务显示库存不足,不能扣减商品库存。
undo_log表在执行回滚后就删除了,如果想要观察,可以在扣减库存处使用debug打断点。
七、总结
- 学习了分布式事务问题和Seata的原理。
- 在虚拟机中导入数据库表 seata-tc.sql,使用Docker部署TC服务,并将seata和nacos两个容器均加入hmall网络。
- 在微服务中集成Seata,包括引入依赖,添加nacos共享配置shared-seata.yaml,修改bootstrap.yaml和application.yaml配置,在微服务的数据库中添加undo_log表以保存AT模式的数据快照,给发起全局事务的入口方法添加@GlobalTransactional注解。jdk非11时,要在启动类的配置中添加虚拟机JVM参数--add-opens java.base/java.lang=ALL-UNNAMED。
- 学习了XA模式和AT模式的原理,使用,区别。