微服务实战系列之SpringCloud Alibaba:
在微服务实战系列之SpringCloud Alibaba学习(一)中,学习了搭建三大微服务并完成交互开发与测试。
在微服务实战系列之SpringCloud Alibaba学习(二)中,针对微服务会遇到的一些问题,使用了一些微服务组件来解决,实现了用户微服务、商品微服务和订单微服务之间的远程调用,并且实现了服务调用的负载均衡。也基于阿里开源的Sentinel实现了服务的限流与容错,并详细介绍了Sentinel的核心技术与配置规则 。如下:
Nacos
来实现服务的注册与发现Ribbon
来实现服务调用的负载均衡Sentinel
实现接口限流在微服务实战系列之SpringCloud Alibaba学习(三)中,简单介绍了服务网关,并对SpringCloud Gateway的核心架构进行了简要说明,也在项目中整合了SpringCloud Gateway网关实现了通过网关访问后端微服务,另外,也基于SpringCloud Gateway整合Sentinel实现了网关的限流功能。
SpringCloud Gateway
网关实现了通过网关访问后端微服务以及限流功能在微服务实战系列之SpringCloud Alibaba学习(四)中,学习分布式链路追踪技术与解决方案。随后在项目中整合Sleuth实现了链路追踪,并使用Sleuth整合ZipKin实现了分布式链路追踪的可视化
。
在微服务实战系列之SpringCloud Alibaba学习(五)中,学习消息服务
:MQ、项目整合RocketMQ
以及RocketMQ核心技术
。
在微服务实战系列之SpringCloud Alibaba学习(六)中,学习服务配置
:服务配置介绍与Nacos核心概念,项目整合Nacos配置中心以及实现动态刷新与配置共享。
在微服务实战系列之SpringCloud Alibaba学习(七)中,学习分布式事务
:分布式事务核心原理与Seata介绍以及项目整合Seata实现分布式事务
Seata相关的内容来自Seata官网。
链接:https://seata.io/zh-cn/docs/overview/what-is-seata.html
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式
,为用户打造一站式的分布式解决方案。
前提
整体机制
两阶段提交协议的演变:
写隔离
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 「全局锁
」 ,本地提交释放本地锁。tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 「全局锁
」 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 「全局锁
」 。
tx1 二阶段全局提交,释放 「全局锁
」 。tx2 拿到 「全局锁
」 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 「全局锁
」,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 「全局锁
」 等锁超时,放弃 「全局锁
」 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 「全局锁
」 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 「脏写
」 的问题。
读隔离
在数据库本地事务隔离级别 「读已提交(Read Committed)
」 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 「读未提交(Read Uncommitted)
」 。
如果应用在特定场景下,必需要求全局的 「读已提交
」 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 「全局锁
」 ,如果 「全局锁
」 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 「全局锁」 拿到,即读取的相关数据是 「已提交
」 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
工作机制
以一个示例来说明整个 AT 分支的工作过程。
update product set name = 'GTS' where name = 'TXC';
一个分布式的全局事务,整体是 「两阶段提交」 的模型。全局事务是由若干分支事务组成的,分支事务要满足 「两阶段提交」 的模型要求,即需要每个分支事务都具备自己的:
根据两阶段行为模式的不同,我们将分支事务划分为 「Automatic (Branch) Transaction Mode
」 和 「Manual (Branch) Transaction Mode
」.
AT 模式(参考链接 TBD)基于 「支持本地 ACID 事务
」 的 「关系型数据库
」:
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 「自定义」 的分支事务纳入到全局事务的管理中。
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
适用场景
优势
缺点
目前,我们的项目中是不支持分布式事务的。也就是说,如果我们调用订单微服务的下单接口提交订单,如果扣减库存失败了,订单依然会写入订单数据表,这是一种典型的分布式事务问题。
(1)打开cmd终端,进入MySQL命令行,并进入shop商城数据库,如下所示。
C:\Users\binghe>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 5.7.35 MySQL Community Server (GPL)
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use shop;
Database changed
(2)查看商品数据表,如下所示。
mysql> select * from t_product;
+------+------------+-------------+-------------+
| id | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为 | 2399.00 | 100 |
| 1002 | 小米 | 1999.00 | 100 |
| 1003 | iphone | 4999.00 | 100 |
+------+------------+-------------+-------------+
3 rows in set (0.00 sec)
这里,我们以id为1001的商品为例,此时发现商品的库存为100。
(3)查询订单数据表,如下所示。
mysql> select * from t_order;
Empty set (0.00 sec)
可以发现订单数据表为空。
(4)查询订单条目数据表,如下所示。
mysql> select * from t_order_item;
Empty set (0.00 sec)
可以看到,订单条目数据表为空。
(1)复制商品微服务的io.binghe.shop.order.service.impl.OrderServiceV7Impl
类为io.binghe.shop.order.service.impl.OrderServiceV8Impl
类,后续的操作在io.binghe.shop.order.service.impl.OrderServiceV8Impl
类中进行,修改OrderServiceV8Impl
类中上的@Service注解中的bean名称为orderServiceV8,并修改saveOrder()方法的代码。这里,只列出有改动的部分代码。
在saveOrder()方法中的扣减商品库存和发送RocketMQ消息之间加入一行代码int i = 1 / 0;
,使其扣减库存成功后抛出异常。
修改前的部分代码如下所示。
Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
if (result.getCode() == 1001){
throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams));
}
if (result.getCode() != HttpCode.SUCCESS){
throw new RuntimeException("库存扣减失败");
}
log.info("库存扣减成功");
rocketMQTemplate.convertAndSend("order-topic", order);
修改后的部分代码如下所示。
Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
if (result.getCode() == 1001){
throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams));
}
if (result.getCode() != HttpCode.SUCCESS){
throw new RuntimeException("库存扣减失败");
}
log.info("库存扣减成功");
int i= 1 / 0;
rocketMQTemplate.convertAndSend("order-topic", order);
「注意:这里只给出了修改的部分代码,修改后的完整代码,加入【冰河技术】获取。」
(2)修改订单微服务的io.binghe.shop.order.controller.OrderController
类中注入的OrderService的bean名称,将其修改为orderServiceV8,如下所示。
@Autowired
@Qualifier(value = "orderServiceV8")
private OrderService orderService;
(3)分别启动Nacos、Sentinel、ZinKin、RocketMQ,并启动用户微服务,商品微服务,订单微服务和服务网关。打开浏览器访问http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。
返回的原始数据如下所示。
{"code":500,"codeMsg":"执行失败","data":"/ by zero"}
可以看到,下单减库存时,系统已经抛出了异常。
(4)查看各个微服务和网关输出的日志信息,分别如下所示。
获取到的用户信息为:{"address":"北京","id":1001,"password":"c26be8aaf53b15054896983b43eb6a65","phone":"13212345678","username":"binghe"}
可以看到,用户微服务无异常信息。
获取到的商品信息为:{"id":1001,"proName":"华为","proPrice":2399.00,"proStock":100}
更新商品库存传递的参数为: 商品id:1001, 购买数量:1
可以看到,商品微服务无异常信息。
提交订单时传递的参数:{"count":1,"empty":false,"productId":1001,"userId":1001}
库存扣减成功
服务器抛出了异常:{}
java.lang.ArithmeticException: / by zero
可以看到,订单微服务抛出了ArithmeticException异常。
执行前置过滤器逻辑
执行后置过滤器逻辑
访问接口主机: localhost
访问接口端口: 10001
访问接口URL: /server-order/order/submit_order
访问接口URL参数: userId=1001&productId=1001&count=1
访问接口时长: 1054ms
可以看到,网关服务无异常信息。
(1)打开cmd终端,进入MySQL命令行,并进入shop商城数据库,如下所示。
C:\Users\binghe>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 5.7.35 MySQL Community Server (GPL)
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use shop;
Database changed
(2)查看商品数据表,如下所示。
mysql> select * from t_product;
+------+------------+-------------+-------------+
| id | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为 | 2399.00 | 99 |
| 1002 | 小米 | 1999.00 | 100 |
| 1003 | iphone | 4999.00 | 100 |
+------+------------+-------------+-------------+
3 rows in set (0.00 sec)
可以看到,此时商品数据表中,id为1001的商品库存数量由100变成了99,减少了1个库存数量。
(3)查看订单数据表,如下所示。
mysql> select * from t_order;
Empty set (0.00 sec)
可以看到,订单数据表为空。
(4)查看订单条目数据表,如下所示。
mysql> select * from t_order_item;
Empty set (0.00 sec)
可以看到,订单条目数据表为空。
综上,在下单扣减库存的业务逻辑中,在订单微服务中扣减完商品库存后,抛出了异常,导致商品库存被扣减了。但是,订单数据却没有写入到数据库中,出现了分布式事务问题。接下来,我们就在项目中整合Seata来解决分布式事务问题。
接下来,我们就正式在项目中整合Seata来实现分布式事务。这里,我们主要整合Seata的AT模式。
(1)到https://github.com/seata/seata/releases/tag/v1.4.2链接下载Seata的安装包和源码,这里,下载的是1.4.2版本,如下所示。
这里我下载的都是zip压缩文件。
(2)进入Nacos,选择的命名空间,如下所示。
点击新建命名空间,并填写Seata相关的信息,如下所示。
可以看到,这里我填写的信息如下所示。
「这里,需要记录下命名空间的ID:seata_namespace_001,在后面的配置中会使用到。」
点击确定后如下所示。
可以看到,这里为Seata在Nacos中创建了命名空间。
(3)解压Seata安装文件,进入解压后的seata/seata-server-1.4.2/conf目录,修改registry.conf注册文件,修改后的部分文件内容如下所示。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "seata_namespace_001"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "seata_namespace_001"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
其中,namespace的值就是在Nacos中配置的Seata的命名空间ID:seata_namespace_001。
「注意:这里只列出了修改的部分内容,完整的registry.conf文件可以到项目的doc/nacos/config/chapter25目录下获取。」
(4)修改Seata安装文件的seata/seata-server-1.4.2/conf目录下的file.conf文件,修改后的部分配置如下所示。
store {
mode = "db"
publicKey = ""
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
「注意:这里只列出了修改的部分内容,完整的file.conf文件可以到项目的doc/nacos/config/chapter25目录下获取。」
(5)在下载的Seata源码的seata-1.4.2/script/config-center
目录下找到config.txt文件,如下所示。
将其复制到Seata安装包解压的根目录下,如下所示。
接下来,修改Seata安装包解压的根目录下的config.txt文件,这里还是只列出修改的部分,如下所示。
service.vgroupMapping.server-order-tx_group=default
service.vgroupMapping.server-product-tx_group=default
store.mode=db
store.publicKey=""
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?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
store.db.user=root
store.db.password=root
store.redis.sentinel.masterName=""
store.redis.sentinel.sentinelHosts=""
store.redis.password=""
「注意:在config.txt中,部分配置的等号“=”后面为空,需要在等号“=“后面添加空字符串""。同样的,小伙伴们可以到项目的doc/nacos/config/chapter25目录下获取完整的config.txt文件。」
(6)在下载的Seata源码的seata-1.4.2/script/config-center/nacos目录下找到nacos-config.sh文件,如下所示。
将nacos-config.sh文件复制到Seata安装文件解压目录的seata/seata-server-1.4.2/scripts
目录下,其中scripts目录需要手动创建,如下所示。
(7).sh文件是Linux操作系统上的脚本文件,如果想在Windows操作系统上运行.sh文件,可以在Windows操作系统上安装Git后在运行.sh文件。
接下来,在Git的Bash命令行进入Seata安装文件中nacos-config.sh文件所在的目录,执行如下命令。
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -t seata_namespace_001 -u nacos -w nacos
其中,命令中的每个参数含义如下所示。
-h:Nacos所在的IP地址。
-p:Nacos的端口号。
-g:分组。
-t:命名空间的ID,这里我们填写在Nacos中创建的命名空间的ID:seata_namespace_001。如果不填,默认是public命名空间。
-u:Nacos的用户名。
-w:Nacos的密码。
执行命令后的结果信息如下所示。
=========================================================================
Complete initialization parameters, total-count:89 , failure-count:0
=======================================================================
Init nacos config finished, please start seata-server.
可以看到,整个配置执行成功。
(8)打开Nacos的配置管理-配置列表界面,切换到seata命名空间,可以看到有关Seata的配置都注册到Nacos中了,如下所示。
(9)在MySQL数据库中创建seata数据库,如下所示。
create database if not exists seata;
接下来,在seata数据库中执行Seata源码包seata-1.4.2/script/server/db目录下的mysql.sql脚本文件,mysql.sql脚本的内容如下所示。
-- -------------------------------- 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(128),
`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;
这里,也将mysql.sql文件放在了项目的doc/nacos/config/chapter25目录下。
(10)启动Seata服务,进入在命令行进入Seata安装文件的seata/seata-server-1.4.2/bin
目录,执行如下命令。
seata-server.bat -p 8091 -h 127.0.0.1 -m db
可以看到,在启动Seata的命令行输出了如下信息。
i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
说明Seata已经启动成功。
至此,Seata的基础环境搭建完毕。
在我们开发的微服务程序中,订单微服务下单成功后会调用库存微服务扣减商品的库存信息,而用户微服务只提供了查询用户信息的接口。这里,我们在商品微服务和订单微服务中整合Seata。
我们使用的是Seata的AT模式,需要我们在涉及到使用Seata解决分布式事务问题的每个业务库中创建一个Seata的undo_log数据表,Seata中本身提供了创建数据表的SQL文件,这些SQL文件位于Seata源码包下的seata-1.4.2/script/client/at/db
目录中,如下所示。
这里,我们使用mysql.sql脚本。mysql.sql脚本的内容如下所示。
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
注意,这里要在shop数据库中执行mysql.sql脚本,同样的,我会将这里的mysql.sql文件放到项目的doc/nacos/config/chapter25目录下,并重命名为mysql_client.sql。
(1)在商品微服务shop-product的pom.xml文件中引入Seata依赖,如下所示。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
(2)修改商品微服务shop-product的bootstrap.yml,修改后的文件如下所示。
spring:
application:
name: server-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: product_group
shared-configs[0]:
data_id: server-all.yaml
group: all_group
refresh: true
discovery:
server-addr: 127.0.0.1:8848
alibaba:
seata:
tx-service-group: ${spring.application.name}-tx_group
profiles:
active: dev
seata:
application-id: ${spring.application.name}
service:
vgroup-mapping:
server-product-tx_group: default
registry:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: nacos
password: nacos
group: SEATA_GROUP
namespace: seata_namespace_001
application: seata-server
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: nacos
password: nacos
group: SEATA_GROUP
namespace: seata_namespace_001
其中,配置的Nacos的namespace与group与registry.conf
文件中的一致。
(1)在订单微服务shop-product的pom.xml文件中引入Seata依赖,如下所示。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
(2)修改订单微服务shop-order的bootstrap.yml,修改后的文件如下所示。
spring:
application:
name: server-order
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: order_group
shared-configs[0]:
data_id: server-all.yaml
group: all_group
refresh: true
discovery:
server-addr: 127.0.0.1:8848
alibaba:
seata:
tx-service-group: ${spring.application.name}-tx_group
profiles:
active: dev
seata:
application-id: ${spring.application.name}
service:
vgroup-mapping:
server-order-tx_group: default
registry:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: nacos
password: nacos
group: SEATA_GROUP
namespace: seata_namespace_001
application: seata-server
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: nacos
password: nacos
group: SEATA_GROUP
namespace: seata_namespace_001
(3)修改订单微服务的io.binghe.shop.order.service.impl.OrderServiceV8Impl类的saveOrder()方法,在saveOrder()方法上添加Seata的@GlobalTransactional注解,如下所示。
@Override
@GlobalTransactional
public void saveOrder(OrderParams orderParams) {
//省略具体方法代码
}
至此,搭建并整合Seata完毕,就是这么简单。
这里,首先将商品数据表t_product中id为1001的数据的库存信息重置为100,如下所示。
update t_product set t_pro_stock = 100 where id = 1001;
(1)打开cmd终端,进入MySQL命令行,并进入shop商城数据库,如下所示。
C:\Users\binghe>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 5.7.35 MySQL Community Server (GPL)
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use shop;
Database changed
(2)查看商品数据表,如下所示。
mysql> select * from t_product;
+------+------------+-------------+-------------+
| id | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为 | 2399.00 | 100 |
| 1002 | 小米 | 1999.00 | 100 |
| 1003 | iphone | 4999.00 | 100 |
+------+------------+-------------+-------------+
3 rows in set (0.00 sec)
这里,我们以id为1001的商品为例,此时发现商品的库存为100。
(3)查询订单数据表,如下所示。
mysql> select * from t_order;
Empty set (0.00 sec)
可以发现订单数据表为空。
(4)查询订单条目数据表,如下所示。
mysql> select * from t_order_item;
Empty set (0.00 sec)
可以看到,订单条目数据表为空。
(1)分别启动Nacos、Sentinel、ZinKin、RocketMQ,Seata,并启动用户微服务,商品微服务,订单微服务和服务网关。打开浏览器访问http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。
返回的原始数据如下所示。
{"code":500,"codeMsg":"执行失败","data":"/ by zero"}
(2)查看各个微服务和网关输出的日志信息,分别如下所示。
获取到的用户信息为:{"address":"北京","id":1001,"password":"c26be8aaf53b15054896983b43eb6a65","phone":"13212345678","username":"binghe"}
说明用户微服务无异常信息。
获取到的商品信息为:{"id":1001,"proName":"华为","proPrice":2399.00,"proStock":100}
更新商品库存传递的参数为: 商品id:1001, 购买数量:1
说明商品微服务无异常信息。
值得注意的是,整合Seata后,商品微服务同时输出了如下日志。
rm handle branch rollback process:xid=192.168.0.111:8091:6638572304823066625,branchId=6638572304823066634,branchType=AT,resourceId=jdbc:mysql://localhost:3306/shop,applicationData=null
Branch Rollbacking: 192.168.0.111:8091:6638572304823066625 6638572304823066634 jdbc:mysql://localhost:3306/shop
xid 192.168.0.111:8091:6638572304823066625 branch 6638572304823066634, undo_log deleted with GlobalFinished
Branch Rollbacked result: PhaseTwo_Rollbacked
看上去应该是有事务回滚了。
提交订单时传递的参数:{"count":1,"empty":false,"productId":1001,"userId":1001}
库存扣减成功
服务器抛出了异常:{}
java.lang.ArithmeticException: / by zero
说明订单微服务抛出了ArithmeticException异常。
同时,商品微服务会输出如下日志。
Branch Rollbacked result: PhaseTwo_Rollbacked
[192.168.0.111:8091:6638572304823066625] rollback status: Rollbacked
看上去应该是有事务回滚了。
执行前置过滤器逻辑
执行后置过滤器逻辑
访问接口主机: localhost
访问接口端口: 10001
访问接口URL: /server-order/order/submit_order
访问接口URL参数: userId=1001&productId=1001&count=1
访问接口时长: 1632ms
可以看到,网关服务无异常信息。
通过微服务打印出的日志信息,可以看到,有事务回滚了。
(1)打开cmd终端,进入MySQL命令行,并进入shop商城数据库,如下所示。
C:\Users\binghe>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 5.7.35 MySQL Community Server (GPL)
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use shop;
Database changed
(2)查看商品数据表,如下所示。
mysql> select * from t_product;
+------+------------+-------------+-------------+
| id | t_pro_name | t_pro_price | t_pro_stock |
+------+------------+-------------+-------------+
| 1001 | 华为 | 2399.00 | 100 |
| 1002 | 小米 | 1999.00 | 100 |
| 1003 | iphone | 4999.00 | 100 |
+------+------------+-------------+-------------+
3 rows in set (0.00 sec)
可以看到,此时商品数据表中,id为1001的商品库存数量仍然为100。
(3)查看订单数据表,如下所示。
mysql> select * from t_order;
Empty set (0.00 sec)
可以看到,订单数据表为空。
(4)查看订单条目数据表,如下所示。
mysql> select * from t_order_item;
Empty set (0.00 sec)
可以看到,订单条目数据表为空。
至此,我们成功在项目中整合了Seata解决了分布式事务的问题。
参考:
本文是参考冰河技术得SpringCloud Alibaba实战专栏内容整理的,只是方便学习使用(侵权删),具体想要了解或想要获取源码的,请参考下面冰河技术的链接,关注公众号获取源码。
SpringCloud Alibaba
冰河技术:《SpringCloud Alibaba实战》