黑马程序员SpringCloud微服务开发与实战
版本
本课程在250服务器 \\192.168.0.250\alist\用户使用教程\视频教程\黑马程序员SpringCloud微服务开发与实战 下,使用版本如下
SpringCloud:2021.0.x aka Jubilee
SpringBoot: 2.6.x,2.7.x(Starting with 2021.0.3)
视频笔记在 飞书 密码是 j.N?-+4[
前端项目
前端项目地址 http://localhost:18080,登录账号密码 jack/123
创建微服务子模块
- 一个工程下创建多个模块,每个模块作为一个微服务,下图是创建新模块的方法

- 创建
Module后从其他模块的pom.xml中拷贝依赖和插件,记得在pom.xml上右键Reload Project - 制作启动类和多个包。新创建的模块默认带有工程名称
hmall需要手动添加本模块的名称作为包名,之后在com.hmall.item下创建多个包:controller、service、mapper等,并且创建启动类ItemApplication,注意修改启动类注解中的Mapper的路径

- 拷贝
resources下的yaml文件,并修改其中的端口号、服务名称、数据库名称等等 - 显示工程下的所有微服务

- 以上操作完成了新模块的准备工作,可以开始编码工作了
- 拆分后新的微服务有使用其他微服务的
api则需要在api模块中新增被依赖的微服务的feign客户端,然后在当前微服务的服务层中声明被依赖的feign客户端,例如private final UserClient userClient,注意声明feign客户端后要在启动类PayApplication头上使用注解@EnableFeignClients(basePackages = "com.hmall.api",defaultConfiguration = DefaultFeignConfig.class)扫描api模块中提供的feign客户端类,否则会报找不到UserClient。例如在拆分新的微服务pay-service时,在其服务层中用到了user-service微服务的api,需要现在api微服务hm-api的client包下新增feign客户端UserClient
package com.hmall.api.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author chanchaw
* @create 2025-08-06 15:50
*/
@FeignClient("user-service")
public interface UserClient {
@PutMapping("/money/deduct")
void deductMoney(@RequestParam("pw") String pw, @RequestParam("amount") Integer amount);
}
上面代码的 deductMoney 来自用户模块的 api :/users/money/deduct ,方法名称是 deductMoney,注意 feign 客户端的名称使用在网关中的名称 user-service, 具体请求的方法 put 要保证和用户模块的控制器中的 api 一直,参数也要保证一致。feign 客户端的目标是将被依赖的 api 制作为 feign 的 客户端 api,其他微服务通过调用 feign 客户端使用其他微服务的 api。
nacos
概述
nacos 主要用处:微服务集中管理、共享配置
准备数据库
在数据库服务器上执行下面脚本创建 nacos 的数据库,如果是测试环境注意修改数据库名称,不要重复
-- --------------------------------------------------------
-- 主机: 192.168.150.101
-- 服务器版本: 8.0.27 - MySQL Community Server - GPL
-- 服务器操作系统: Linux
-- HeidiSQL 版本: 12.2.0.6576
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- 导出 nacos 的数据库结构
DROP DATABASE IF EXISTS `ecomm-nacos`;
CREATE DATABASE IF NOT EXISTS `ecomm-nacos` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `ecomm-nacos`;
-- 导出 表 nacos.config_info 结构
DROP TABLE IF EXISTS `config_info`;
CREATE TABLE IF NOT EXISTS `config_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin DEFAULT NULL,
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL,
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL,
`c_use` varchar(64) COLLATE utf8_bin DEFAULT NULL,
`effect` varchar(64) COLLATE utf8_bin DEFAULT NULL,
`type` varchar(64) COLLATE utf8_bin DEFAULT NULL,
`c_schema` text COLLATE utf8_bin,
`encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info';
-- 正在导出表 nacos.config_info 的数据:~0 rows (大约)
DELETE FROM `config_info` where 1=1;
-- 导出 表 nacos.config_info_aggr 结构
DROP TABLE IF EXISTS `config_info_aggr`;
CREATE TABLE IF NOT EXISTS `config_info_aggr` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'datum_id',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL,
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='增加租户字段';
-- 正在导出表 nacos.config_info_aggr 的数据:~0 rows (大约)
DELETE FROM `config_info_aggr` where 1=1;
-- 导出 表 nacos.config_info_beta 结构
DROP TABLE IF EXISTS `config_info_beta`;
CREATE TABLE IF NOT EXISTS `config_info_beta` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) COLLATE utf8_bin DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info_beta';
-- 正在导出表 nacos.config_info_beta 的数据:~0 rows (大约)
DELETE FROM `config_info_beta` where 1=1;
-- 导出 表 nacos.config_info_tag 结构
DROP TABLE IF EXISTS `config_info_tag`;
CREATE TABLE IF NOT EXISTS `config_info_tag` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL COMMENT 'content',
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COLLATE utf8_bin COMMENT 'source user',
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_info_tag';
-- 正在导出表 nacos.config_info_tag 的数据:~0 rows (大约)
DELETE FROM `config_info_tag` where 1=1;
-- 导出 表 nacos.config_tags_relation 结构
DROP TABLE IF EXISTS `config_tags_relation`;
CREATE TABLE IF NOT EXISTS `config_tags_relation` (
`id` bigint NOT NULL COMMENT 'id',
`tag_name` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) COLLATE utf8_bin DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) COLLATE utf8_bin NOT NULL COMMENT 'data_id',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`nid` bigint NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='config_tag_relation';
-- 正在导出表 nacos.config_tags_relation 的数据:~0 rows (大约)
DELETE FROM `config_tags_relation` where 1=1;
-- 导出 表 nacos.group_capacity 结构
DROP TABLE IF EXISTS `group_capacity`;
CREATE TABLE IF NOT EXISTS `group_capacity` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
-- 正在导出表 nacos.group_capacity 的数据:~0 rows (大约)
DELETE FROM `group_capacity` where 1=1;
-- 导出 表 nacos.his_config_info 结构
DROP TABLE IF EXISTS `his_config_info`;
CREATE TABLE IF NOT EXISTS `his_config_info` (
`id` bigint unsigned NOT NULL,
`nid` bigint unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) COLLATE utf8_bin NOT NULL,
`group_id` varchar(128) COLLATE utf8_bin NOT NULL,
`app_name` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT 'app_name',
`content` longtext COLLATE utf8_bin NOT NULL,
`md5` varchar(32) COLLATE utf8_bin DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text COLLATE utf8_bin,
`src_ip` varchar(50) COLLATE utf8_bin DEFAULT NULL,
`op_type` char(10) COLLATE utf8_bin DEFAULT NULL,
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` text COLLATE utf8_bin NOT NULL COMMENT '秘钥',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='多租户改造';
-- 正在导出表 nacos.his_config_info 的数据:~0 rows (大约)
DELETE FROM `his_config_info` where 1=1;
-- 导出 表 nacos.permissions 结构
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE IF NOT EXISTS `permissions` (
`role` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`resource` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`action` varchar(8) COLLATE utf8mb4_general_ci NOT NULL,
UNIQUE KEY `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 正在导出表 nacos.permissions 的数据:~0 rows (大约)
DELETE FROM `permissions` where 1=1;
-- 导出 表 nacos.roles 结构
DROP TABLE IF EXISTS `roles`;
CREATE TABLE IF NOT EXISTS `roles` (
`username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`role` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
UNIQUE KEY `idx_user_role` (`username`,`role`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 正在导出表 nacos.roles 的数据:~1 rows (大约)
DELETE FROM `roles` where 1=1;
INSERT INTO `roles` (`username`, `role`) VALUES
('nacos', 'ROLE_ADMIN');
-- 导出 表 nacos.tenant_capacity 结构
DROP TABLE IF EXISTS `tenant_capacity`;
CREATE TABLE IF NOT EXISTS `tenant_capacity` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='租户容量信息表';
-- 正在导出表 nacos.tenant_capacity 的数据:~0 rows (大约)
DELETE FROM `tenant_capacity` where 1=1;
-- 导出 表 nacos.tenant_info 结构
DROP TABLE IF EXISTS `tenant_info`;
CREATE TABLE IF NOT EXISTS `tenant_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) COLLATE utf8_bin NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_id',
`tenant_name` varchar(128) COLLATE utf8_bin DEFAULT '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) COLLATE utf8_bin DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint NOT NULL COMMENT '创建时间',
`gmt_modified` bigint NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='tenant_info';
-- 正在导出表 nacos.tenant_info 的数据:~0 rows (大约)
DELETE FROM `tenant_info` where 1=1;
-- 导出 表 nacos.users 结构
DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
`username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(500) COLLATE utf8mb4_general_ci NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 正在导出表 nacos.users 的数据:~1 rows (大约)
DELETE FROM `users` where 1=1;
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES
('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', 1);
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
docker 安装
nacos 镜像文件在 250 服务器路径 \\192.168.0.250\alist\安装程序\cc\java\nacos ,该目录下还有环境配置文件 custom.env。上传到xdf250 虚拟机系统 ubuntu 目录 /home/chanchaw/Downloads/nacos/,将 tar 文件和配置文件都上传到这个目录。终端切换路径到这个目录下
# 在此之前先要安装 docker
# 进入链接 https://xdfznh.club/kb/develop/operation-system/linux/ubuntu/apt.html
# 查看如何设置 apt 安装时的网络代理,进入下面链接查看如何安装 docker
# https://xdfznh.club/kb/develop/operation-system/linux/ubuntu/docker.html
# 安装镜像文件到 docker
cd /home/chanchaw/Downloads/nacos/
sudo docker load -i nacos.tar
# 确认本地镜像文件存在
sudo docker image ls
# 修改环境变量配置文件 ./custom.env 中的数据库服务器地址为 nacos 数据库所在地址
# 以及其他的数据库参数:登录用户名、密码、端口号等等
# 执行创建容器,注意下面一定要有参数 --network=host,表示使用宿主机IP地址可以访问,否则后面访问不通
sudo docker run -d --network=host --name nacos --env-file ./custom.env -p 8848:8848 -p 9848:9848 -p 9849:9849 --restart=always nacos/nacos-server:v2.1.0-slim
# 查看日志,确认启动成功
sudo docker logs -f nacos
# 清除容器日志
# 1/2.查看容器ID,后面的 [container_name] 填写容器名称
# 会得到容器的完整ID,字符很长
docker inspect --format="{{.Id}}" [container_name]
# 2/2.根据容器ID清除日志
sudo truncate -s 0 /var/lib/docker/containers/<容器ID>/*-json.log
# 注意,配置文件 custom.env 中数据库名称不要使用特殊符号
# 2025年10月14日 再次安装时第一次使用数据库名称 ecomm-nacos,启动容器失败
# 第二次使用 'ecomm-nacos' 仍然失败
# 第三次修改了数据库名称为 nacos 启动成功(注意同步修改mysql中数据库名称)
# 如果使用了错误的配置文件创建了容器,先删除再重新创建
sudo docker rm nacos
# 设置iptables放行端口访问
sudo iptables -A INPUT -p tcp --dport 8848 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 8848 -j ACCEPT
# 访问地址如下,登录账号和密码都是 nacos
# IP地址是 docker 所在虚拟机系统的IP地址,不是虚拟机宿主机IP
http://192.168.0.224:8848/nacos
# 登录账号和密码都是 nacos
# docker启停nacos
sudo docker start nacos
sudo docker stop nacos
# 或者先删除已经创建的容器重新创建
sudo docker rm nacos
服务注册
为微服务添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
然后在项目配置文件 application.yaml 中新增配置项
spring:
cloud:
nacos:
server-addr: 192.168.0.224:8848 # nacos服务器IP地址和端口号
配置后启动项目即会自动注册服务。下面介绍在 IDEA 中使用同一个源码部署2个实例的方法。
先启动微服务 ItemService,然后拷贝其启动配置后修改端口号,启动第二个实例,之后会在 nacos 中看到一个微服务的两个实例

在 nacos 中的服务列表可以看到一个服务的两个实例

服务发现
类似服务注册,先添加依赖,在 application.yaml 中添加 nacos 服务器的配置,然后在业务服务中使用 - 先在类中自动注入 discoveryClient
private final DiscoveryClient discoveryClient;
private void handleCartItems(List<CartVO> vos) {
// TODO: 从单体代码改进为微服务代码
// 从原本单体项目中拷贝来后注释掉
// 微服务项目要做到单一职责,之后这里要通过RPC请求其他微服务
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// 2.1. 根据服务名称获取服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if(CollUtil.isEmpty(instances)){
System.out.println("未能获取到服务item-service的服务列表!");
return;
}
// 2.2. 负载均衡获取具体的服务并使用请求
ServiceInstance serviceInstance = instances.get(RandomUtil.randomInt(instances.size()));
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
ResponseEntity<List<ItemDTO>> res = restTemplate.exchange(
serviceInstance.getUri() + "/items?ids={ids}",
HttpMethod.GET, null,
// 声明返回数据类型是:List<ItemDTO>
new ParameterizedTypeReference<List<ItemDTO>>() {
},
// 将 itemIds 拼接为逗号间隔的字符串,填充到第一个参数的占位符 ids 上
Map.of("ids", CollUtil.join(itemIds, ","))
);
}
配置中心/共享
配置共享
nacos 还有功能是配置中心,可以为所有微服务设置相同的配置项,配置项中占位符是 ${},在其中使用冒号间隔,后面为默认值

一般的 springboot 项目在读取配置文件 application.yaml 后启动项目,在使用 nacos 配置中心后应该是先读取该配置中心的配置项,合并到 springboot 自己的配置文件后再启动项目,才能达到共享配置的目标。逻辑流程图如下

按照下面步骤实现:
- 添加
nacosConfig依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
- 项目中新增
nacos使用的启动配置文件bootstrap.yaml,从上图可以看出这个配置文件优先级别高,关键配置可以写在这里,特别是端口号
spring:
application:
name: cart-service
cloud:
nacos:
server-addr: 192.168.0.224:8848
config:
file-extension: yaml
shared-configs:
- data-id: shared-jdbc.yaml
- data-id: shared-log.yaml
- data-id: shared-swagger.yaml
server:
port: 8091
删除模块中之前的 application-dev.yaml 和 application-local.yaml 等分环境配置文件,只要保留一个 application.yaml 即可,主要为设置 nacos 的共享配置文件中占位符中的变量
feign:
okhttp:
enabled: true
hm:
db:
host: localhost
port: 3306
name: hm-cart
user: root
pw: chanchaw
swagger:
title: 黑马商城购物车接口文档
describe: 黑马商城购物车接口文档
package: com.hmall.cart.controller
- 在启动项目之前先保证最上面的 配置共享 中已经设置了配置文件
配置热更新
nacos 可以实现以微服务名称命名的配置文件热更新功能

按照下面步骤实现这个功能:
nacos中制作以微服务名称命名的配置文件cart-service.yaml- 微服务中制作配置文件读取类
- 在业务中通过自动注入获取配置数据并使用

动态路由
OpenFeign
简单案例
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
- 打开开关,在配置类中使用注解
EnableFeignClients
@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 编码实现 在微服务模块中创建包
client,里面存放OpenFeign客户端接口,例如本次创建接口client/ItemClient.java代码如下
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
// 声明该接口使用的微服务名称
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items") // 请求数据的 uri
// 接口方法声明请求用到的参数和返回值数据类型
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
// 业务中使用
// 注意下面 itemClient 是使用了 lombok 的注解 @RequiredArgsConstructor
// 实现自动注入构造方法,此处不需要使用 @Autowired
private final ItemClient itemClient;
// 直接调用,不需要自己写实现,类似 Mybatis,OpenFeign 会帮我们写实现
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
连接池
OpenFeign 通过 http 请求获取数据,默认使用 HttpURLConnection,不支持连接池,自己可以替换为 Apache HttpClient 或者 OKHttp,这两者都支持连接池。发起请求在 FeignBlockingLoadBalancerClient#delegate 成员变量中。
- 添加依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
- 配置,
feign是顶级配置,和spring是兄弟节点
feign:
okhttp:
enabled: true
最佳实践
按照上面简单案例的方法,微服务B如果需要用到微服务A的接口则需要创建一个 ItemClient,同样的微服务C如果要用到微服务A的接口也需要实现一个 ItemClient,当微服务多起来就会造成重复代码,重复功能的编写。需要将这部分代码重构,有两种方案,类似微服务项目的两种构建方法:
- 每个微服务都是一个单独的工程,所有工程归属在一个项目文件夹下。适合大型项目
- 整个项目是一个工程,每个微服务是工程下的一个模块。打开工程后可以看到所有微服务比较直观,适合中小型项目
类似上面项目的构建,使用 OpenFeign 也有两种构建方法
- 在微服务A中制作3个子模块如下图,其他微服务都调用
item-api请求商品数据,优点是功能的高内聚,每个微服务负责自己权限内的业务逻辑,不会产生耦合,缺点是每个微服务模块都会多出来3个子模块,导致整个工程子模块非常多

- 将所有微服务的
openFeign客户端集中到一个微服务模块hm-api中。相对于上面方法1的优点是不会产生很多模块,缺点是所有微服务的API都集中到一个子模块中,产生了耦合。本教程中采用了这个方案,这个方案创建模块的方法

然后要做如何初始化工作
- 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 创建包:
com.hmall.api.client用于存放OpenFeign客户端接口,com.hmall.api.dto用于存放dto类,还需要手动创建启动类ApiApplication.java,注意启动类或者其他配置类中使用注解@EnableFeignClients - 原本微服务
cart-service中直接使用OpenFeign的代码要更换。首先将OpenFeign的直接依赖去掉:去掉3个依赖:openfeign,loadbalancer,okhttp,换成引用hm-api模块。
<dependency>
<groupId>com.hmall.api</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
注意 groupId 和 artifactId 要参照模块 hm-api 的 pom.xml
- 微服务
cart-service中用到的OpenFeign客户端中涉及的实体类dto等,都删除掉,转而引用api-service中的实体类和dto,修改cart-service的启动类或者配置类的注解@EnableFeignClients(basePackages = "com.hmall.api"),显示给出扫描OpenFeign客户的包路径为新增的 API 模块api-service的客户端包路径。之后业务代码中声明private final ItemClient itemClient;以及使用都不用变
日志
OpenFeign 只会在 FeignClient 所在宝的日志级别为 DEBUG 时才会输出日志,而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值
- BASIC:仅记录请求的方法,URL以及相应状态码和执行时间
- HEADERS:在BASIC基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
由于 Feign 默认的日志级别就是NONE,所以默认我们看不到请求日志。下面介绍开启日志的方法:
- 在
api-service微服务中创建日志级别配置类DefaultFeignConfig代码如下,注意这里不需要使用注解让其成为配置类
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
/**
* @author chanchaw
* @create 2025-08-02 12:45
*/
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
- 在启用
OpenFeign的注解中设置配置类名称,在注解EnableFeignClients中设置为上面的配置类,注意是在其他微服务的启用注解,而不是在api-service的微服务启动类中
package com.hmall.cart;
import com.hmall.api.config.DefaultFeignConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableFeignClients(basePackages = "com.hmall.api",defaultConfiguration = DefaultFeignConfig.class)
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
网关
概述
网关微服务采用 SpringFlux 技术,不是 SpringMVC 项目,其他微服务都是 SpringBoot 项目,同时带有 SpringMVC。所以网关中不可使用 SpringMVC 相关的拦截器等,API 等。在 hm-common 制作有 SpringMVC 拦截器,而网关微服务依赖 hm-common ,导致网关默认会加载 SpringMVC 的拦截器从而报错,所以要在网关中排除相关 bean 的自动装配。在 hm-common 模块中 SpringMVC 相关的自动配置类头上要使用 ConditionalOnClass(DispatcherServlet.class) 显式给出在拥有 SpringMVC 的类 DispatcherServlet 的微服务才自动装配实现了 WebMvcConfigurer 接口的类。详情见模块 hm-common 的类 MvcConfig
用处
- 挂载所有微服务的
uri并转发请求 - 通过过滤器修改请求和响应
- 检查请求头认证当前请求是否已登录
- 解析
token获取用户身份并修改请求头向下传递给所有微服务使用 - 认证失败则修改响应结果提示前端没有权限(返回状态码401)
实现
网关使用 Spring Cloud Gateway 而不是 Netflix Zuul,后者是阻塞式的,需要性能调优,相比较后前者更有优势,并且后者已经暂停更新了。按照下面步骤制作
- 添加依赖,注意不能有
spring-boot-starter-web
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.heima</groupId>
<artifactId>hmall</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.hmall</groupId>
<artifactId>hm-gateway</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 手动创建
SpringBootApplication启动类,不需要其他任何注解 - 配置路由
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.0.224:8848
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**,/search/** # 微服务中的多个 controller 的 uri
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
注意,如果配置文件中 routes 上提示:Cannot resolve configuration property 'spring.cloud.gateway.routes' 表示没有网关的依赖,可能 pom.xml 没有添加网关依赖,或者添加之后没有 reload project。注意所有微服务都要被 nacos 管理,成功启动后在 nacos 管理页面应该可以看到。之后可以先测试指定服务是否能正常访问 http://localhost:8090/items/page 能访问通再测试通过网关访问 http://localhost:8080/items/page。
路由断言

路由过滤器

下面介绍为所有路由添加一个请求头的案例:
- 网关微服务的配置文件如下,
default-filters和routes同级,表示为所有路由添加,如果要为指定的一个路由,例如item-service添加请求头,则将其放在id:item-service的兄弟节点上
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.0.224:8848
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**,/search/** # 微服务中的多个 controller 的 uri
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
default-filters:
- AddRequestHeader=truth, anyone long-press like button will be rich
- 在业务代码中获取请求头的指定数据,获取请求头的指定数据
@ApiOperation("分页查询商品")
@GetMapping("/page")
public PageDTO<ItemDTO> queryItemByPage(PageQuery query,@RequestHeader(value="truth",required = false) String truth) {
System.out.println("truth:" + truth);
// 1.分页查询
Page<Item> result = itemService.page(query.toMpPage("update_time", false));
// 2.封装并返回
return PageDTO.of(result, ItemDTO.class);
}
全局过滤器
通过自定义过滤器实现用户的鉴权
GetewayFilter- 路由过滤器,作用域任意指定的路由,默认不生效,要配置到路由后生效GlobalFilter- 全局过滤器,作用范围是所有路由,声明后自动生效
网关过滤器
没有参数
下面是自己制作网关过滤器的代码,被注释的代码是没有排序功能的,应该采用下面的方法,可以设置过滤器的排序序号。这里要特别注意的是类名是有规则的,必须以 GatewayFilterFactory 结束,前面的 PrintAny 是的在配置文件中要配置的过滤器名称
package com.hmall.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author chanchaw
* @create 2025-08-05 9:43
*/
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
// return new GatewayFilter() {
// @Override
// public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// System.out.println("print any filter running!");
// return chain.filter(exchange);
// }
// };
// 上面返回匿名类的方法无法指定过滤器的序号,应该使用下面的方法
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running!");
return chain.filter(exchange);
}
}, -1);
}
}
制作配置类后在配置文件中做如下配置,下面是网管服务配置文件的完整内容,本次代码仅仅对应最后一行 PrintAny ,所以要特别注意自定义过滤器类名称
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.0.224:8848
gateway:
routes:
- id: item-service # 自定义id,保证唯一,可以不同于微服务的项目名称
uri: lb://item-service # lb 表示负载均衡,item-service 为微服务项目名称
predicates:
- Path=/items/**,/search/** # 微服务中的多个 controller 的 uri
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**,/usertest/**
default-filters: # 自定义请求头:{truth:anyone long-press like button will be rich}
- AddRequestHeader=truth, anyone long-press like button will be rich
- PrintAny
多个参数
类似上面制作过滤器类,其中带有内部静态类 Config 作为参数实体类,函数 shortcutFieldOrder 作为传入参数时遵照的顺序,还需要在构造方法中将参数实体类传递给父类,因为是父类调用我们的自定义过滤器类的,要让其知道使用的实体类。
package com.hmall.gateway.filters;
import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author chanchaw
* @create 2025-08-05 10:44
*/
@Component
public class TestParamsGatewayFilterFactory extends AbstractGatewayFilterFactory<TestParamsGatewayFilterFactory.Config> {
@Data
public static class Config {
private String sid;
private String name;
}
@Override
public List<String> shortcutFieldOrder(){
return List.of("sid","name");
}
public TestParamsGatewayFilterFactory(){
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("接受到的参数是:" + config.toString());
return chain.filter(exchange);
}
}, -2);
}
}
配置文件如下设置,最后一行是针对过滤器类 TestParamsGatewayFilterFactory 设置的参数,接受两个参数
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.0.224:8848
gateway:
routes:
- id: item-service # 自定义id,保证唯一,可以不同于微服务的项目名称
uri: lb://item-service # lb 表示负载均衡,item-service 为微服务项目名称
predicates:
- Path=/items/**,/search/** # 微服务中的多个 controller 的 uri
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**,/usertest/**
default-filters: # 自定义请求头:{truth:anyone long-press like button will be rich}
- AddRequestHeader=truth, anyone long-press like button will be rich
- PrintAny
- TestParams=1001,chanchaw
微服务共用登录信息
所有业务的微服务都依赖模块 hm-common ,网关微服务同样依赖 hm-common 是 SpringFlux 项目,没有 SpringMVC
在模块 hm-common 制作 SpringMVC 拦截器 UserInfoInterceptor 实现接口 HandlerInterceptor,从 request 获取用户ID后保存到 ThreadLocal 修饰的用户上下文 UserContext 中,在一次请求的整个生命周期可以随时获取用户ID,即一次请求中可能用到的所有微服务都可以通过 UserContext 获取到用户ID。
一个 SpringBoot 项目中通过注解 @Configuration 可以自动装配自定义的拦截器,但是业务微服务A想要在依赖 hm-common 时自动装配下面的类
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
还需要在 hm-common 在配置文件 \hm-common\src\main\resources\META-INF\spring.factories 中设置如下配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
其他业务微服务在依赖 hm-common 后才能自动扫描到上面的配置类。
以上是跨模块自动扫描配置类的实现方法,如果在一个模块内部不需要配置文件 spring.factories,只要通过注解 @Configuration 就可以了。此后还有一个问题,由于网关模块 hm-gateway 不是 SpringMVC 项目,在依赖 hm-common 并自动装配后初始化 SpringMVC 相关的 API 会报错,所以还需要在 SpringMVC 相关的自动装配类 MvcConfig 上使用条件引用 @ConditionalOnClass(DispatcherServlet.class)。表示只有包含类 DispatcherServlet.class 的模块才会自动装配该配置类,这个类被包含在 SpringMVC 中,这样就保证了只有 SpringMVC 的项目才会自动装配该配置类。
本案例见黑马微服务教程的模块 hm-common 涉及到的多个文件,在 957ff8e55d4b269711ef45219c1b1e82c7fd0ceb 后面的一个提交实现了该功能
MvcConfig.java,UserInfoInterceptor.java,spring.factories
本方法适用于通过网关请求微服务的情况,微服务之间的相互调用并不通过网关,所以是无法获取用户身份信息的。所以还要在 api 网关模块中制作 OpenFeign 拦截器,向请求头中写入用户信息。在模块 hm-api 的类 DefaultFeignConfig 中添加下面代码
// 制作 OpenFeign 的请求拦截器,写入用户信息
// 保证微服务之间的调用也能拿到发出请求的用户身份
// - 前面做的在网关中设置向请求头添加用户信息
// 在微服务之间调用API时无效,无法获取用户信息
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = Optional.ofNullable(UserContext.getUser()).orElse(0L);
requestTemplate.header("user-info", userId.toString());
}
};
}
此后调用 OpenFeign 客户端时都会向请求头中写入用户信息,下游被依赖的 api 就可以从请求头中获取用户信息了。
sentinel服务限流
概述
使用 sentinel 实现:请求限流、线程隔离、服务熔断、Fallback 。在微服务内部引入依赖客户端,安装服务端可监控各个服务端。服务端 sentinel-dashboard-1.8.6.jar 在 \\192.168.0.250\alist\安装程序\cc\java\sentinel-dashboard-1.8.6.jar 需要部署到非中文,没有特殊符号的路径下。sentinel 对于微服务的配置不会持久化,即重启服务端后要重新设置针对每个微服务的请求限流、线程隔离等等。新启动的 sentinel 服务端在 实时监控、簇点链路 中都不会显示数据,在有请求后才会有显示,所以要针对微服务的请求制作限流规则先自己请求一次,在 簇点链路 中找到请求再设置。
安装部署
在 xdf250 虚拟机系统 ubuntu 中安装 sentinel,将 sentinel 的 jar 拷贝到 /usr/local/bin/sentinel 下,制作名称为 /etc/systemd/system/sentinel-dashboard.service 的文件,内容如下(jar 不需要安装,通过 systemctl 制作守护进程)
[Unit]
Description=Sentinel Dashboard
After=syslog.target network.target
[Service]
# 基本配置
User=root
WorkingDirectory=/usr/local/bin/sentinel
ExecStart=/usr/bin/java -Xms256m -Xmx256m -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar /usr/local/bin/sentinel/sentinel-dashboard-1.8.6.jar
# 重启策略
SuccessExitStatus=143
Restart=always
RestartSec=5
# 日志配置(Sentinel控制台输出 + 系统日志)
StandardOutput=append:/var/log/sentinel/sentinel-dashboard.log
StandardError=append:/var/log/sentinel/sentinel-dashboard-error.log
SyslogIdentifier=sentinel-dashboard
# PID文件
PIDFile=/var/run/sentinel-dashboard.pid
[Install]
WantedBy=multi-user.target
执行下面命令创建 systemd 服务
# 赋予可执行权限
sudo chmod +x /etc/systemd/system/sentinel-dashboard.service
# 刷新守护进程管理器
sudo systemctl daemon-reload
# 设置开机启动
sudo systemctl enable sentinel-dashboard
# 启动服务
sudo systemctl start sentinel-dashboard
# 创建日志目录并设置权限
sudo mkdir -p /var/log/sentinel
sudo chown root:root /var/log/sentinel
# 重新加载systemd并启用服务
sudo systemctl daemon-reload
sudo systemctl enable sentinel-dashboard
# 启动服务(默认8080端口)
sudo systemctl start sentinel-dashboard
# 验证状态
sudo systemctl status sentinel-dashboard
# 查看日志
journalctl -u sentinel-dashboard -f
如果没有设置端口则默认是 8080,浏览器访问地址:http://主机IP地址:8090,默认登录账号密码都是 sentinel。下面是删除并重新创建服务的方法
# 停止服务
sudo systemctl stop sentinel-dashboard
# 禁用服务(防止开机自启)
sudo systemctl disable sentinel-dashboard
# 删除服务文件
sudo rm /etc/systemd/system/sentinel-dashboard.service
# 重新加载systemd(清除残留配置)
sudo systemctl daemon-reload
# 可选:删除日志和PID文件
sudo rm -f /var/run/sentinel-dashboard.pid
sudo rm -f /var/log/sentinel/sentinel-dashboard*.log
# 之后可重新制作 /etc/systemd/system/sentinel-dashboard.service 再次创建服务
微服务集成
注意所有业务数据库都需要创建 undo_log 表(所有用到分布式事务的业务涉及到的数据库都需要有这个表)
CREATE TABLE `undo_log` (
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id' COLLATE 'utf8mb4_general_ci',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization' COLLATE 'utf8mb4_general_ci',
`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 INDEX `ux_undo_log` (`xid`, `branch_id`) USING BTREE
)
COMMENT='AT transaction mode undo table'
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB;
- 微服务集成依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
- 微服务配置文件设置
sentinel控制台地址
spring:
application:
name: cart-service
cloud:
sentinel:
transport:
dashboard: 192.168.0.224:8090
异常情况
我的开发电脑上开了N多 wpm,有 l2tp,openvpn,wireguard,一开始运行后在 sentinel 控制台出现了 cart-service 页面,但是在 实时监控 页面没有显示,在页面 机器列表 中显示的IP地址是 wireguard 的,但是 sentinel 控制台服务器并没有安装 wireguard ,只是在局域网环境下,所以在项目中不管如何访问 cart-service 相关的 api 在 实时监控 页面都没有显示数据,后来在微服务配置文件中设置了客户端IP地址后再访问项目就可以看到 sentinel 的实时监控页面有数据显示了,注意下面的配置项目 client-ip
spring:
application:
name: cart-service
cloud:
sentinel:
transport:
dashboard: 192.168.0.224:8090
client-ip: 192.168.0.68
nacos:
server-addr: 192.168.0.224:8848
config:
file-extension: yaml
shared-configs:
- data-id: shared-jdbc.yaml
- data-id: shared-log.yaml
- data-id: shared-swagger.yaml
server:
port: 8091
请求限流
限流的测试方法见下面 jmeter 的使用
线程隔离
在微服务配置文件中做如下配置,下面第一个 enabled 表示将 sentinel 集成到 feign 中,微服务通过 feign 互相调用 api 时也会被 sentinel 监控到,也可以实现限流
feign:
sentinel:
enabled: true # 开启 Feign 对 sentinel 的集成,实现 sentinel 通过线程隔离功能限流功能
okhttp:
enabled: true
集成后通过 feign 客户端微服务互相调用 api 时也会被 sentinel 监控到。通过下面方法在购物车服务针对查询商品服务制作线程隔离,将商品服务的故障隔离出去,防止购物车因为出现太多线程被传染出现故障。

Fallback
在某个接口耗时太久,还频繁的调用这个接口,会拖慢整个服务的响应时长,此时会使用 FeignClient 的 Fallback 转而调用其他回调方法,不再请求实际的 api,有两种实现方式:
FallbackClass,无法对远程调用的异常做处理FallbackFactory,可以对远程调用的异常做处理,一般都会选择中方式
实现第二种方式,需要做两步:创建被调用的微服务 api 的 Fallback 工厂类、在被调用微服务 api 的接口头上通过注解使用该工厂类,下面是针对购物车服务 cart-service 调用商品服务 item-service 的 feign 客户端,微服务 hm-api 中的 ItemClient 做 Fallback 的步骤: 制作 feign 客户端的 fallback ,自然是在模块 hm-api 下创建包和类
创建包 fallback ,并创建类 ItemClientFallbackFactory 代码如下:
package com.hmall.api.fallback;
import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author chanchaw
* @create 2025-08-08 14:30
*/
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient(){
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("feign客户端请求 queryItemByIds 查询商品信息出现异常:", cause);
return Collections.emptyList();// 返回空集合
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
// 扣除库存是P0级错误,抛出给调用者,通知其失败的原因
throw new RuntimeException(cause);
}
};
}
}
在 ItemClient 通过注解使用上面的工厂类
package com.hmall.api.client;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import com.hmall.api.fallback.ItemClientFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
/**
* @author chanchaw
* @create 2025-08-02 7:48
*/
@FeignClient(value = "item-service", fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
@PutMapping("/stock/deduct")
void deductStock(@RequestBody List<OrderDetailDTO> items);
}
在自动配置类 DefaultFeignConfig 创建返回 bean - 下面代码中的方法 itemClientFallbackFactory 用于创建并返回该 bean,下面类中并没有使用自动装配类的注解,而是在会用到 feign 客户端的微服务例如 cart-service 的启动类中通过注解引用该自动配置类 @EnableFeignClients(basePackages = "com.hmall.api",defaultConfiguration = DefaultFeignConfig.class)
package com.hmall.api.config;
import com.hmall.api.fallback.ItemClientFallbackFactory;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import java.util.Optional;
/**
* @author chanchaw
* @create 2025-08-02 12:45
*/
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
// 制作 OpenFeign 的请求拦截器,写入用户信息
// 保证微服务之间的调用也能拿到发出请求的用户身份
// - 前面做的在网关中设置向请求头添加用户信息
// 在微服务之间调用API时无效,无法获取用户信息
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = Optional.ofNullable(UserContext.getUser()).orElse(0L);
requestTemplate.header("user-info", userId.toString());
}
};
}
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory(){
return new ItemClientFallbackFactory();
}
}
在具体的微服务(购物车微服务)启动类头上为注解 @EnableFeignClients 指定 feign 的配置类
@EnableFeignClients(basePackages = "com.hmall.api",defaultConfiguration = DefaultFeignConfig.class)
在 jmeter 中测试线程隔离时使用 fallback 的效果,不会报错出现异常,即下图的列 Error 中数据为0。在 sentinel 中设置调用 feign 客户端子请求 GET:http://item-service/items 的线程阈值是5,即一秒钟最多5个线程执行该请求,多余的线程都会走 fallback 快速返回,来保证请求响应的速度以及不会出现异常(报错)。

持久化
可以将 sentinel 的配置写入 nacos 的共享配置中,在微服务的配置文件中读取共享配置来实现持久化,在服务端的 web 管理页面中新增修改都是非持久化的
jmeter
软件在 xdf250 的路径是 \\192.168.0.250\alist\安装程序\cc\java\apache-jmeter-5.6.3.zip,运行 bin 目录下的 jmeter.bat,下面演示限流的压测,使用脚本在 \\192.168.0.250\alist\安装程序\cc\java\黑马微服务雪崩测试.jmx,拖放到 jmeter 后使用 限流测试

以上设置在 sentinel 的位置如下图

seata
架构原理

部署 TC 服务
下面是 seata 的数据库脚本,后面要在配置文件中指定该数据库服务器的地址 - 一般将数据库创建在 seata 运行的服务器上
CREATE DATABASE IF NOT EXISTS `seata`;
USE `seata`;
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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
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 = utf8mb4;
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),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
拷贝 seata 的安装文件 \\192.168.0.119\alist\安装程序\cc\devtools\java\seataAndConf.tar.gz ,其中包含 docker 镜像文件和相关的配置文件,到服务器目录 /usr/local/bin/ 下,按照下面步骤解压并安装部署
# 创建目录并赋予所有权限
cd /usr/local/bin/
# 解压文件后修改新创建的目录名称为 seata
sudo tar -xzvf seataAndConf.tar.gz
sudo chmod -R 777 seata
# 将解压后的 seata 镜像文件纳入 docker 中
sudo docker load -i ./seata/seata.tar
# 修改配置文件 /usr/local/bin/seata/application.yml 中 seata 要连接的数据库所在服务器的IP地址,登录账号和密码等
# 如果关联 nacos 中使用了命名空间,记得修改两处的命名空间配置项:
# seata.config.nacos.namespace
# seata.registry.nacos.namespace
# 同时注意修改 nacos 所在服务器地址,酌情是否需要修改 nacos 服务器地址的两个配置项:
# seata.config.nacos.server-addr
# seata.registry.nacos.server-addr
# 执行下面命令创建 seata 容器
# 参数 -v 表示将 /usr/local/bin/seata/ 目录挂载到容器中的 /seata-server/resources
# 即 /usr/local/bin/seata/application.yml 通过挂载 docker 文件系统称为配置文件
# 注意要使用 --network=host 参数,表示 seata 容器使用宿主机的网络
# 注意设置参数 SEATA_IP 为 seata 所在服务器的使用的IP地址
sudo docker run --name seata -d \
--privileged=true \
--network=host \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.0.123 \
-v /usr/local/bin/seata:/seata-server/resources \
seataio/seata-server:1.5.2
# 查看创建容器的日志,确保容器创建成功
sudo docker logs -f seata
两个步骤确认 seata 容器成功运行起来
- 通过命令
sudo docker logs -f seata看到成功运行,没有报错 - 在
nacos管理页面的 服务管理 > 服务列表 中会自动出现服务名称 seata-server
浏览器访问 http://seata服务IP地址:7099,登录账号密码都是 admin
需要为每个微服务配置 seata 的 tc 服务器(就是上一步中安装部署的 seata 服务),考虑到微服务会很多,在 nacos 中制作共享配置,此后每个微服务从 nacos 拉取配置。在 nacos 中创建如下共享配置

图片中的配置内容如下
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息取注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.0.224:8848 # nacos服务器地址
namespace: "" # 命名空间,默认为空
group: DEFAULT_GROUP # 分组,默认是 DEFAULT_GROUP
application: seata-server # seata 服务名称
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
在每个微服务中引入依赖使用 seata
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在微服务中的 nacos 配置文件 bootstrap.yaml 中集成 nacos 共享配置,下面代码中只有 seata 是新增的一行
spring:
application:
name: trade-service
cloud:
sentinel:
transport:
dashboard: 192.168.0.224:8090 # sentinel 服务器IP地址
client-ip: 192.168.0.68 # 被监控的微服务所在设备IP地址
http-method-specify: true # 开启区分请求 uri method:post,get,delete等
nacos:
server-addr: 192.168.0.224:8848
config:
file-extension: yaml
shared-configs:
- data-id: shared-jdbc.yaml
- data-id: shared-log.yaml
- data-id: shared-swagger.yaml
- data-id: shared-seata.yaml
server:
port: 8096
feign:
sentinel:
enabled: true # 开启 Feign 对 sentinel 的集成,实现 sentinel 通过线程隔离功能限流功能
okhttp:
enabled: true
XA模式
seata 提供多种保证事务的模式,其中 XA 模式(两阶段提交)是 X/Open 组织定义的分布式事务处理( DTP,Distributed Transcation Processing )标准,XA规范你描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。 Seata 的 XA 模式就如上图显示,左边的 TM 和右边的 TC 协同完成工作。该模式的工作流程如下:(两阶段提交的执行步骤)
一阶段工作:
- RM 注册分支事务到 TC
- RM 执行分支业务
sql不提交 - RM 报告执行状态到 TC
二阶段工作:
- TC 检测各个分支事务执行的状态
- 如果都成功,通知所有 RM 提交事务
- 如果有失败,通知所有 RM 回滚事务
- RM 接受 TC 指令,提交或者回滚事务
总之一句话,各个子业务分别执行自己的逻辑但是不提交,由 TC 逐个查看执行结果后通知 TM 执行所有业务一起提交或者一起回滚。RM 是每个业务逻辑,TC 是决策者,TM 是执行者。
这个模式优点:
- 事务的强一致性,满足 ACID 原则
- 常用数据库都支持,实现简单,并且没有代码侵入
缺点是:
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务 -
redis和mongo等数据库不支持
seata配置文件设置 XA模式,下面是nacos共享配置的完整代码,本次只添加了第一行
seata:
data-source-proxy-mode: XA # 开启数据源代理的 XA 模式,强一致性,保证数据一致
registry: # TC服务注册中心的配置,微服务根据这些信息取注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.0.224:8848 # 共享配置给微服务使用,所以天蝎nacos服务器地址
namespace: "" # 命名空间,默认为空
group: DEFAULT_GROUP # 分组,默认是 DEFAULT_GROUP
application: seata-server # seata 服务名称
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
- TM 所在的方法头上使用注解
@GlobalTransactional
选择多个商品到购物车后 “提交订单” 会执行两步操作:
- 减少商品库存
- 清空购物车
添加商品到购物车时只能添加库存允许范围内的数量,在 “提交订单” 之前在数据库中修改商品的库存数量,保证库存数量小于购物车中的数量,来制造库存不足的异常情况。在没有使用 seata 事务之前,会导致清空了购物车后扣除库存时出现异常,库存不会被扣减。在有事务保证的情况下应该是如果库存不足则提交订单从而不会清空购物车。
微服务 trade-service 的方法 OrderServiceImpl.java # createOrder 是提交订单后用于创建订单的方法,其中有调用上面两个微服务:清空购物车、扣减库存。所以在本方法头上使用注解 @GlobalTransactional ( seata 的事务注解),同样的需要在微服务 item-service 的 ItemServiceImpl.java # deductStock 方法上使用 spring 的事务注解 @Transactional,以及在微服务 cart-service 的方法 CartServiceImpl.java # removeByItemIds 方法上也使用事务注解 @Transactional 。这样就制作完成了一个完整的事务,使用上面的测试方法测试库存不足的情况,前端页面会提示无法下单,后端的库存不会减少,同时购物车也不会清空就对了。
同时注意扣减库存的方法 deductStock 中出现的异常不可吃掉,要通过 throw 抛出才能使事务生效从而回滚。
AT模式
seata 推荐使用 AT 模式,它和 XA 模式同样是分阶段提交的事务模型,不过弥补了 XA 模型中资源锁定周期过长的缺陷。XA 模式中每个 RM 在执行自己的 sql 逻辑后不提交,需要等待其他所有 RM 都执行完毕并汇报给 TC 后决定是全部一起提交还是所有 RM 一起回滚。这个过程中每个 RM 由于都没有提交会一直锁定数据库的资源,造成资源浪费,整个系统的吞吐量下降。而 AT 模式中每个 RM 在执行完毕后都会立即提交,不过在提交之前会记录一个 undo log,当多个 RM 中有一个失败则所有 RM 执行 undo log 恢复数据,起到和事务回滚一样的效果。本模式执行效率较高,所以是 seata 推荐使用的模式。
从数据安全性,一致性方面考虑,XA 模式是数据强一致性,因为每个 RM 会锁定资源等待所有 RM 一起执行提交或者一起回滚,在两个阶段过程中有其他用户的其他请求查询到的数据都是一致的。而 AT 模式在二阶段执行完毕之前可能造成不一致性。例如:购物车清理完毕,订单创建完毕,在扣减库存时出现异常导致需要执行 undo log,在执行之前,其他地方的查询可能会看到已经创建的订单和已经清空的购物车,只有在二阶段执行完毕之后才达到数据的一致性。所以本模式也称为最终一致性,而不是 XA 的强一致性。AT 模式还有个优势是不依赖于数据库的事务功能,即可以适用于 redis 等非关系型数据库(此类数据库可能没有数据库事务功能)。
- 在每一个微服务数据库中都要创建表
undo_log
-- 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
- 在微服务配置文件中设置模式为
AT。由于是默认模式,在seata的配置文件中删除显式指定为XA模式的配置即可,当然也可以是显式的给出
