基于阿里开源的分布分布Sentinel实现了服务的限流与容错,并详细介绍了Sentinel的式事实现式事核心技术与配置规则。简单介绍了服务网关,项务并对SpringCloud Gateway的目整核心架构进行了简要说明,也在项目中整合了SpringCloud Gateway网关实现了通过网关访问后端微服务. 同时,分布分布也基于SpringCloud Gateway整合Sentinel实现了网关的式事实现式事限流功能,详细介绍了SpringCloud Gateway网关的项务核心技术。在链路追踪章节,目整我们开始简单介绍了分布式链路追踪技术与解决方案,分布分布随后在项目中整合Sleuth实现了链路追踪,式事实现式事并使用Sleuth整合ZipKin实现了分布式链路追踪的项务可视化 。 在消息服务章节,目整我们介绍了MQ的分布分布使用场景,引入MQ后的式事实现式事注意事项以及MQ的选型对比,在项目中整合了RocketMQ,项务并给大家介绍了RocketMQ的核心技术。 在服务配置章节,我们首先介绍了服务配置与Nacos作为配置中心的相关概念,并在项目中整合了Nacos配置中心。接下来,又基于Nacos实现了动态刷新与配置共享。 在分布式事务篇章,云服务器我们简单介绍了分布式事务的核心原理与SpringCloud Alibaba技术栈中的Seata框架。接下来,我们就在项目中整合Seata框架实现分布式事务。 细心的小伙伴会发现,目前,我们的项目中是不支持分布式事务的。也就是说,如果我们调用订单微服务的下单接口提交订单,如果扣减库存失败了,订单依然会写入订单数据表,这是一种典型的分布式事务问题。 (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; (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 | +------+------------+-------------+-------------+ 这里,我们以id为1001的商品为例,此时发现商品的库存为100。 (3)查询订单数据表,源码下载如下所示。 mysql> select * from t_order; 可以发现订单数据表为空。 (4)查询订单条目数据表,如下所示。 mysql> select * from t_order_item; 可以看到,订单条目数据表为空。 (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 if (result.getCode() == 1001){ throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams)); } if (result.getCode() != HttpCode.SUCCESS){ throw new RuntimeException("库存扣减失败"); } log.info("库存扣减成功"); 修改后的部分代码如下所示。 Result if (result.getCode() == 1001){ throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams)); } if (result.getCode() != HttpCode.SUCCESS){ throw new RuntimeException("库存扣减失败"); } log.info("库存扣减成功"); int i= 1 / 0; (2)修改订单微服务的io.binghe.shop.order.controller.OrderController类中注入的OrderService的bean名称,将其修改为orderServiceV8,如下所示。服务器租用 @Autowired @Qualifier(value = "orderServiceV8") (3)分别启动Nacos、Sentinel、ZinKin、RocketMQ,并启动用户微服务,商品微服务,订单微服务和服务网关。打开浏览器访问http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。 返回的原始数据如下所示。 可以看到,下单减库存时,系统已经抛出了异常。 (4)查看各个微服务和网关输出的日志信息,分别如下所示。 可以看到,用户微服务无异常信息。 商品微服务输出的日志如下所示。获取到的商品信息为:{ "id":1001,"proName":"华为","proPrice":2399.00,"proStock":100} 可以看到,商品微服务无异常信息。 订单微服务输出的日志如下所示。提交订单时传递的参数:{ "count":1,"empty":false,"productId":1001,"userId":1001} 库存扣减成功 服务器抛出了异常:{ } 可以看到,订单微服务抛出了ArithmeticException异常。 网关服务输出的日志如下所示。执行前置过滤器逻辑 执行后置过滤器逻辑 访问接口主机: localhost 访问接口端口: 10001 访问接口URL: /server-order/order/submit_order 访问接口URL参数: userId=1001&productId=1001&count=1 可以看到,网关服务无异常信息。 (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; (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 | +------+------------+-------------+-------------+ 可以看到,此时商品数据表中,id为1001的商品库存数量由100变成了99,减少了1个库存数量。 (3)查看订单数据表,如下所示。 mysql> select * from t_order; 可以看到,订单数据表为空。 (4)查看订单条目数据表,如下所示。 mysql> select * from t_order_item; 可以看到,订单条目数据表为空。 综上,在下单扣减库存的业务逻辑中,在订单微服务中扣减完商品库存后,抛出了异常,导致商品库存被扣减了。但是,订单数据却没有写入到数据库中,出现了分布式事务问题。接下来,我们就在项目中整合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。 (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 } (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="" 「注意:在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文件所在的目录,执行如下命令。 其中,命令中的每个参数含义如下所示。 执行命令后的结果信息如下所示。 ========================================================================= Complete initialization parameters, total-count:89 , failure-count:0 ========================================================================= 可以看到,整个配置执行成功。 (8)打开Nacos的配置管理-配置列表界面,切换到seata命名空间,可以看到有关Seata的配置都注册到Nacos中了,如下所示。 (9)在MySQL数据库中创建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 这里,也将mysql.sql文件放在了项目的doc/nacos/config/chapter25目录下。 (10)启动Seata服务,进入在命令行进入Seata安装文件的seata/seata-server-1.4.2/bin目录,执行如下命令。 可以看到,在启动Seata的命令行输出了如下信息。 说明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 注意,这里要在shop数据库中执行mysql.sql脚本,同样的,我会将这里的mysql.sql文件放到项目的doc/nacos/config/chapter25目录下,并重命名为mysql_client.sql。 (1)在商品微服务shop-product的pom.xml文件中引入Seata依赖,如下所示。 spring-cloud-starter-alibaba-seata (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 其中,配置的Nacos的namespace与group与registry.conf文件中的一致。 (1)在订单微服务shop-product的pom.xml文件中引入Seata依赖,如下所示。 spring-cloud-starter-alibaba-seata (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 (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,如下所示。 (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; (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 | +------+------------+-------------+-------------+ 这里,我们以id为1001的商品为例,此时发现商品的库存为100。 (3)查询订单数据表,如下所示。 mysql> select * from t_order; 可以发现订单数据表为空。 (4)查询订单条目数据表,如下所示。 mysql> select * from t_order_item; 可以看到,订单条目数据表为空。 (1)分别启动Nacos、Sentinel、ZinKin、RocketMQ,Seata,并启动用户微服务,商品微服务,订单微服务和服务网关。打开浏览器访问http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。 返回的原始数据如下所示。 (2)查看各个微服务和网关输出的日志信息,分别如下所示。 说明用户微服务无异常信息。 商品微服务输出的日志如下所示。获取到的商品信息为:{ "id":1001,"proName":"华为","proPrice":2399.00,"proStock":100} 说明商品微服务无异常信息。 值得注意的是,整合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 看上去应该是有事务回滚了。 订单微服务输出的日志如下所示。提交订单时传递的参数:{ "count":1,"empty":false,"productId":1001,"userId":1001} 库存扣减成功 服务器抛出了异常:{ } 说明订单微服务抛出了ArithmeticException异常。 同时,商品微服务会输出如下日志。 Branch Rollbacked result: PhaseTwo_Rollbacked 看上去应该是有事务回滚了。 网关服务输出的日志如下所示。执行前置过滤器逻辑 执行后置过滤器逻辑 访问接口主机: localhost 访问接口端口: 10001 访问接口URL: /server-order/order/submit_order 访问接口URL参数: userId=1001&productId=1001&count=1 可以看到,网关服务无异常信息。 通过微服务打印出的日志信息,可以看到,有事务回滚了。 (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; (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 | +------+------------+-------------+-------------+ 可以看到,此时商品数据表中,id为1001的商品库存数量仍然为100。 (3)查看订单数据表,如下所示。 mysql> select * from t_order; 可以看到,订单数据表为空。 (4)查看订单条目数据表,如下所示。 mysql> select * from t_order_item; 可以看到,订单条目数据表为空。 至此,我们成功在项目中整合了Seata解决了分布式事务的问题。本章总览
分布式事务问题
查询数据表数据
重现分布式事务问题
查询数据表数据
搭建并整合Seata
搭建Seata基础环境
项目整合Seata
导入unlog表
商品微服务整合Seata
订单微服务整合Seata
验证Seata事务
重置数据库数据
查询数据表数据
验证Seata事务
查询数据表数据