微服务简介
微服务是什么?
微服务是一种架构风格,将一个大型应用程序拆分为一组小型、自治的服务。每个服务都运行在自己独立的进程中,使用轻量级的通信机制(通常是HTTP或消息队列)进行相互之间的通信。这种方式使得每个服务可以独立开发、部署和扩展,同时也降低了整个应用程序的复杂性。微服务架构可以提高系统的可伸缩性、灵活性和可维护性,使得团队可以更快地开发和交付新的功能。
微服务项目结构图
单体的Java项目和分布式的Java项目的区别
单体项目
- 优点:发布简单,开发时无需考虑接口暴露问题。
- 缺点:项目耦合度会随着代码量的增大而提高,可读性底。
- 作用域:面向使用人群小的项目,开发成本底。(个人系统,个人博客,等等)
微服务项目
- 优点:大大降低耦合度,业务模块分布清楚,可读性高。
- 缺点:开发成本远远大于单体项目,需要考虑接口暴露问题。
- 作用域:面向使用人群大的项目。(电商项目,等等)
微服务结构
微服务对比
学习环境
jdk1.8,。
springboot版本为:2.3.9.RELEASE。
springcloud版本为:Hoxton.SR10。
eureka
eureka的作用:作为服务中心。
eureka的使用
1.创建eureka的注册中心
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
在application.yml中配置对应的注册信息
#在注册中心中的名字
spring:
application:
name: eurekaService
对应的服务端口
server:
port: 10086
因为eureka作为服务注册中心,所以也需要将自己注册到服务中心中
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
在启动类上添加注解驱动 @EnableEurekaServer
对应的效果图
2.将服务端和消费端都注册到eureka中。
此时服务端和客户端需要添加的依赖为下
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在application.yml编写注册信息
#要注册到的注册中心位置
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
spring:
application:
name: ”对应名字“
在restTemplate中将原本对应的地址位置修改为在eureka中的要调用的服务名字
注册中心效果图为下:
在未来我们的某个模块不一定只有一个,它可能是多个相同的模块,防止因为宕机的缘故导致服务无法使用。
这里我们就复制一个相同的服务
在对用的restTemplate的配置类添加 @LoadBalanced
进行测试,在调用两次接口后发现两个服务都被调用了一次,说明实现了负载均衡。
Ribbon
Ribbon流程
Ribbon负载均衡流程源码
在LoadBalancerInterceptor类中会调用intercept方法,其会获取对应的host名字,通过这个host去注册中心中查找对应的服务地址。
效果图为下:
获得到的serverName=user-service。
通过后序loadBalancer的execute方法,其会调用实现类下的execute方法,也就是RibbonLoadBalancerClient下的execute方法
通过getServer方法去eureka中拉去对应的名字的服务,可以得到user-service的实例,这里就是两个user-service实例。
在getServer方法中最终会调用到rule.choose方法
在该方法的作用就是选择对应的负载均衡的策略。
总共有四大种策略,默认使用轮番查询策略,也就是 RoundRobinRule。
最终只要通过添加@LoadBalanced就可以实现负载均衡。
负载均衡策略
设置策略的方法
1. 通过@Bean配置对应的策略,其作用域为全局。
@Bean
//设置为随机策略
public IRule getIRule() {
return new RandomRule();
}
2.通过在application.yml中进行配置,此方法的优点就是作用域小,可以只作用在某个服务模块上。
#对应要设置策略的服务名
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
饥饿加载
在默认情况下,只有当消费端访问对用的服务端时,服务端才会进行加载,所以在第一次访问服务端的耗时会很长。
为了提高访问效率,我们可以在项目启动时提前加载好对应的服务端,这里就可以使用饥饿加载。
配置方法为下:
ribbon:
eager-load:
enabled: true
#clients是个列表,所以可以设置多个
clients:
- user-service
这样在消费端第一次访问对应的服务端时就会大大减少消耗的时间。
nacos
nacos安装
下载并解压对应的nacos文件后,通过cmd进入nacos的bin目录,执行以下指令进行启动。
startup.cmd -m standalone
此时我们就可以访问localhost:8848/nacos,在该界面中的用户名和密码都是:nacos。
将对应的服务端和消费端的注册中心都改为nacos。
1.导入依赖
在父目录中导入的依赖为下:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在服务端和消费端导入的依赖为下:
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在服务端中的application.yml中配置为下:
spring:
cloud:
nacos:
server-addr: localhost:8848
启动微服务,效果图为下:
nacos服务分级存储模型
实现方法就是:将每个服务分布到不同的集群,这样就可以保证在某个集群失效时不会影响服务的使用。
例子:将所有的user-service设置到集群HZ,将order-service设置到集群BJ。
在application.yml的设置为下:
spring:
cloud:
nacos:
discovery:
#设置对应的集群名称
cluster-name: BJ
实现效果为下:
服务实例的权重设置
权重范围为:0~1,数字越大权重就越大。
在nacos中的是设置方式为下:
权重的使用的特殊情况。
当我们需要对某个模块进行升级时,我们不再需要重新发布项目,而是将当前服务的权重设置为0,这样用户访问是就不会调用当前的服务模块,用户对访问到项目功能的其他模块。
nacos设置负载均衡
在application.yml中配置以下信息:
#对应要设置策略的服务名
user-service:
ribbon:
#设置的哦负载均衡的策略
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
该策略为:存在多个拥有同样功能的集时,我们会先去访问,距离我们更近的集群,当该集群宕机时,才会访问其他相同功能的模块。
nacos环境隔离
在nacos中新建一个环境的方式。
id不设置的话,就会通过UUID自动生成。
在代码application.tml中设置服务的环境空间。
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: BJ
namespace: 419f5dac-5806-4e04-9017-6f7bf726ba41
注意:不同环境间的服务模块是无法相互调用的,所以如果出现500异常可能就是跨环境的错误。
nacos和eureka的区别
nacos设置非临时实例的设置为下:
spring:
cloud:
nacos:
discovery:
#设置为非临时实例
ephemeral:true
nacos配置管理
在nacos中配置对应的application.yml。
配置文件的名字的格式为:[服务名]-[对应的环境名].[文件格式]。(userservice-dev.yaml)
编写效果为下:
为了配合使用线上的配置文件,所以我们需要在springboot说明服务模块对应的线上的配置文件名。
需要在对应的服务模块引入对应的依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
在对应的服务上创建bootstrap.yaml 文件,在该文件中配置对应的线上配置文件信息。
spring:
application:
name: orderservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
# discovery:
# namespace: 419f5dac-5806-4e04-9017-6f7bf726ba41
在controller类中编写对应的属性
启动测试。
确实读到线上的配置,但此时我们在去更改线上的配置,发现获取的name还是历史版本的值,需要我们重启对应的服务模块才会进行更新,为了实现热更新,我们提供了两种解决方案。
热更新
1. 在属性对应的类上使用就注解:@RefreshScope。
进行测试,效果图为下:
更改前:
更新后:
2.创建对用的属性类,将该属性类交给spring容器管理,实现热更新。
创建对用的属性类:OrderProperties
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("com")
@Data
public class OrderProperties {
private String name;
}
在controller层中注入OrderProperties
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.OrderProperties;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
OrderProperties orderProperties;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
@GetMapping("/test")
public String test1() {
System.out.println("线上对应的名字为:" + orderProperties.getName());
return orderProperties.getName();
}
}
测试结果和方案一效果相同。
配置共享
在某个服务模块中,该模块中会存在多个环境: dev, pro, test,这些环境的配置在nacos中都是相互隔离的,为使得各个模块都可以读到某个配置,我们可以在nacos中创建一个配置共享文件。
测试环境就是:
orderservice-dev.yaml, orderservice-pro.yaml, orderservice-test.yaml。
我们可以创建一个配置文件,该文件的名字是固定的 ,[spring.application.name].yaml。
进行测试。
让orderservice服务去读取线上模块的共享配置。
修改对应的OrderProperties。
测试结果为下:
搭建nacos集群
集群结构
搭建步骤为下:
1.在本地数据库中创建nacos需要的数据库信息。
创建一个名字为 nacos的数据库,然后导入对应的sql。
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) 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 COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) 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 COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) 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 COMMENT 'source user',
`src_ip` varchar(50) 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=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) 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=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) 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=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' 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=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) 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=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) 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=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
下载对应的nacos压缩包,解压后将conf/cluster.conf.example文件重命名为cluster.conf。
在cluster.conf中怕配置各个 nacos的地址。
startup.cmd
修改/conf/application.properties中的mysql信息
复制三份nacos,并在application.properties中配置对应的端口,启动这些nacos。
启动指令为下:
startup.cmd
2. 配置nginx进行负载均衡操作了。
修改/conf/nginx.conf的配置。
#配置对应的nacos集群的地址
upstream nacos-cluster {
server 127.0.0.1:8841;
server 127.0.0.1:8842;
server 127.0.0.1:8843;
}
#配置代理
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
启动nginx,在我们使用项目的配置中nacos的地址就是localhost:80,80端口就是nginx代理后的结果。
Feign
feign的使用
在对应的服务模块引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在启动类上夹feign的注解驱动: @EnableFeignClients
在对用服务模块中创建被调用服务对应的feign 接口
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")//被调用模块的名称
public interface orderFeignClient {
@GetMapping("/user/{id}")
public User queryOrderByUserId(@PathVariable("id") Long id);
}
将调用服务的模块中controller的代码为下
import cn.itcast.order.FeignClient.UserFeignClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Autowired
UserFeignClient userFeignClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//通过获取的userId,调用userService获取对应的信息
User user = userFeignClient.queryOrderByUserId(order.getUserId());
System.out.println(user);
order.setUser(user);
// 4.返回
return order;
}
}
测试效果图为下
feign自定义配置
配置我们做的配置一般也就是 feign.logger.level。
logger的等级为下:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
logger的配置为下:
feign:
client:
config:
default: #如果是default表示全局配置,如果是对应的服务名,也就是作用在对应的服务模块
loggerLevel: FULL
使用java代码实现logger等级的配置
创建一个类进行配置。
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class orderConfiguration {
@Bean
public Logger.Level loginLevel() {
return Logger.Level.BASIC;
}
}
如果需要作用在全局,那么就在启动类上进行配置。
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = orderConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
如果需要作用在对用的服务上,我们只需要在对应服务的FeignClient接口上配置即可。
@FeignClient(value = "userservice", configuration = orderConfiguration.class)//被调用模块的名称
public interface UserFeignClient {
@GetMapping("/user/{id}")
public User queryOrderByUserId(@PathVariable("id") Long id);
}
feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
实现方法为下:
引入对应的依赖
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
在application.yml中配置对应的信息
feign:
httpclient:
enabled: true #开启feign对httpClient的支持
max-connections: 200 #最大的连接数
max-connections-per-route: 50 #每个路径找到的连接数
使用优化的方案
1.日志级别尽量用basic,保证日志的输出最少。
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
最佳实现方案
1.继承方式。
一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都集成改接口
优点:
-
简单。
-
实现了代码共享。
缺点:
-
服务提供方、服务消费方紧耦合。
-
参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解。
2.将Feign单独抽取成一个包,在未来需要使用时直接导入包即可。
方案二实现步骤
创建对应的Fiegn包,将相关的feign接口和pojo,配置类都抽取到此包。
在对应的服务模块中引入feign-api包。
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
将对应的配置进行导包,我们此时运行项目会发现userFeignClient在spring容器中找不到,这是因为Feign接口没有被项目在开始时扫描到,是可以为了让其扫描到,我们需要添加一些配置。
在@EnableFeignClients中配置配置扫描的包
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(basePackages = "com.huang.FeignClient")
//如果需要指定某个class的话就使用{}进行指定
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
进行测试,测试结果为下:
Gateway
什么是Gateway:
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Gateway的使用
创建一个新得模块,用于创建gateway。
导入相应得依赖。
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在applicatino.yml中配置规则
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
测试结果为下:
断言规则
gateway过滤器
过滤器的作用
过滤器存在很多个。
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
测试:在此我们测试AddRequestHeader过滤器,也就是添加请求头过滤器。
在application.yml配置过滤器
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
filters:
#添加对应的请求头, 此时请求头的名字为hello,值为:ostkaka!
- AddRequestHeader=hello, ostkaka!
进行测试。
在controller层中编写代码
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId, @RequestHeader(value = "hello", required = false) String str) {
// 根据id查询订单并返回
System.out.println("请求头的信息未为:" + str);
return orderService.queryOrderById(orderId);
}
在我们访问orderservice模块时,就会出现以下效果图
如果需要将该请求头设置作用在所有的服务模块上,我们可以使用default-filters。
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: #默认的过滤器配置,作用在全局
- AddRequestHeader=hello, ostkaka!
进行测试,和之前效果用于,在其他服务模块也可以获得对用的请求头。
全局过滤器
不同于default-filters,全局过滤器可以自定义过滤方式。
自定义步骤为下:
创建个类,用于实现GlobalFilter接口。
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@Order(0)//设置权重
public class GlobalFilterImpl implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
List<String> name = queryParams.get("name");
if(name.equals("admin")) {
//说明name = admin
//放行
return chain.filter(exchange);
}
//不符合条件,不放行
exchange.getResponse().setStatusCode(HttpStatus.valueOf(401));
//直接完成就不向下放行
return exchange.getResponse().setComplete();
}
}
该过滤器会直接在ioc容器中创建,直接作用在全局。
效果图为下:
过滤器的执行顺序
网关解决跨域问题
在网关模块中的application.yml中配置即可
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
后续使用CV即可。
Sentinel
雪崩问题
如果其中有个服务发生故障时,会导致关联的所有模块发生故障。
解决雪崩问题方案
1.超时处理
设置访问服务的超时时间,请求超过一定时间就返回错误信息,不会进行永久等待。
2.仓壁模式
船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
分配线程给对应的服务,如果对应二点线程耗尽后,也不会占用其他的线程,保证项目的其他模块正常运行。
3.断路器模式
由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。(也就是我们未来最常使用的解决方案)
当发现对应的服务模块的异常比例过高时,断路器就会执行熔断操作,就直接无法访问异常的服务模块,直接返回错误信息,保证其他服务模块的正常运行。
限流
流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
熔断技术的对比
sentinel整合springboot
1.运行sentinel。
2.引入依赖(springcloud对应的版本为:Hoxton.SR8)
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3.在application.yml中配置sentinel的配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 #sentinel的面板地址
访问对应的模块,进行测试
sentine就在实时监测对应模块的并发和异常情况。
流控
设置QPS为3就时表示一秒内最大的流量(最大并发)。
如果在一秒内的并发量大于三则会返回对应的错误信息。
流控模式
1.直接模式:只要并发量大于阈值则就直接返回错误信息。
2.关联模式:只要关联的资源的并发量大于阈值则会使得当前的资源返回错误信息。(关联的资源可以正常使用,不会出现返回错误信息的情况)
3.链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
例子:
两同个Controller下的请求,对用如同有个service, 只控制其中有个资源进行流控。(就是访问优先级的问题)
在controller层中新建对应的请求
import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
//请求模块
@RequestMapping("/query")
public String queryTest() {
orderService.getGoods();
return "进行了Query操作";
}
//保存请求
@RequestMapping("/save")
public String saveTest() {
orderService.getGoods();
return "进行了Save操作";
}
}
在service层中编写对应的代码
@SentinelResource("goods")//名字可以自定义
public void getGoods() {
System.out.println("查询到对应的商品信息了!");
}
在applicatino.yml中配置对应的配置
spring:
cloud:
sentinel:
web-context-unify: false #关闭springMVC资源整合,使得链路模式生效
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。设置修改关闭上下文整合。
进行测试,我们就可以看见存在两个goods在两个资源中。
在流控规则中新建对应的流控规则。
将入口资源设置为/order/query,也就是只有资源时/order/query时才会执行流控规则, /order/save资源不会存在该规则。(QPS设置为3)
在进行疯狂的刷新后,我们会发现在/order/save中无论我们怎么刷新都不会存在返回错误信息的情况,也就是流控规则没有在该资源中生效。
在/order/query中,我们进行疯狂刷新就会返回错误信息,也就是流控规则生效了。
流控效果
-
快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。(默认使用快速失败)
-
warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
新建一个流控规则:
在ApiPost中的设置为下:
测试效果为下:
在前三秒的预热中每秒只处理三个请求,其他七个请求全部返回错误信息,在三秒预热过后,每秒十个的请求都会被处理。
前三秒预热阶段,处于慢慢上升的阶段,慢慢到达峰值。
-
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。
而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。(最大为2000ms)
新建一个流控规则。
测试结果为下:
请求都成功了,且请求会进行排队等待。
请求的进入处于平缓的状态,保证每次从队列中取出相同数量的请求。
热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
设置热点规则
可以个相同参数不同值设置不同的阈值。(到达不同的流控的效果)
隔离和降级
线程隔离: 调用者在调用访问请求时每次都会开一个单独得线程,使得每次得请求都是线程隔离的。
服务降级:在服务模块添加断路器,统计失败访问的比例,如果过高时就会执行服务降级,使得调用者无法调用到该服务, 并返回错误信息。
FeignClient整合springboot实现返回降级信息
在消费者模块中application.yml中配置对应的信息
feign:
sentinel:
enabled: true #开启sentinel支持
编写服务降级逻辑(实现FallbackFactory接口方法, feign模块中编写)
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
public class UserFeignClientFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
//在create方法中编写UserFeign的服务降级方法
//如何存在多个接口,我们就需要配置每个接口的降级返回方法
return id -> {
//编写降级后返回的数据
return new User();
};
}
}
将该UserFeignClientFactory配置到ioc容器中
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DefaultFeignConfiguration {
@Bean
public UserFeignClientFactory getUserFeignClientFactory() {
return new UserFeignClientFactory();
}
}
在feign接口中设置fallbackFactory
import cn.itcast.feign.config.UserFeignClientFactory;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice", fallbackFactory = UserFeignClientFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
进行测试,在请求超过流控规则后,就会返回自定义的错误信息。
线程隔离
线程隔离的方式有两种:
1.线程池隔离(也就是给每个请求创建一个线程池)
2.信号量隔离(sentinel中的方法)
俩者的优缺点:
信号量隔离的设置为下:
单机阈值也就是一秒内最大的线程数。(如果线程数超过对应的阈值就会返回自定义的错误信息)
熔断降级
断路器控制熔断和放行是通过状态机来完成的:
熔断策略
1.满调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
规则解析:在1秒内请求数超过5,且请求速度超过0.5秒的比例大于0.5就进行服务降级,熔断5秒。
2.异常比例
规则解析:在1秒内,请求数超过5,且异常请求的比例大于0.5就进行服务降级,降级的时间为5秒。
3.异常数
规则解析:在一秒内,请求数超过5,且异常的个数超过2个就进行服务降级,降级的时间为5。
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
-
白名单:来源(origin)在白名单内的调用者允许访问
-
黑名单:来源(origin)在黑名单内的调用者不允许访问
-
资源名:就是受保护的资源,例如/order/{orderId}
点击左侧菜单的授权,可以看到授权规则:
-
流控应用:是来源者的名单,
-
如果是勾选白名单,则名单中的来源被许可访问。
-
如果是勾选黑名单,则名单中的来源被禁止访问。
-
流控应用就是请求头中对应键值对中的值,通过该值来判断授权类型。
例子:
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)。
最终授权步骤为下:
1.我们通过网关给请求头设置对应的信息,在消费者模块中的application.yml中配置,用于后续授权的判断,如果使用浏览器直接访问则不会带有该请求头。(配置带对应网关模块中)
gateway:
default-filters:
- AddRequestHeader=origin,gateway #geteway就是键值对中的值,也就是授权规则中流控应用的值
2.编写对应的方法,用于获取请求头中的值。(配置在对应的消费者模块中)
sentinel通过RequestOriginParser接口获取对应键值对中值的信息。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
对应的实现方法为下:
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component //配置到ioc容器中
//此方法会在发送请求时执行,用于和sentinel中对应请求的授权规则进行判断
//获得的value会和授权规则中的流控应用的值进行判断
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String value = httpServletRequest.getHeader("origin"); //origin就是请求头中键值对中的键
if(StringUtils.isEmpty(value)) {
//value为空,说明表示从GetWay来的
value = "blank";
return value;
}
return value;
}
}
3.在sentinel中设置对应请求的授权规则。
4.进行测试。
在给order对应的请求路径设置授权规则后
此时我们需要进行身份认证
进行身份认证后还是报错误信息,达到无法从浏览器发送对应服务请求的效果。
通过网关进行访问
正常访问,达到授权的效果。
自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
自定义对应的异常结果需要实现BlockExceptionHandler接口
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
在消费者模块中的自定义异常结果的代码为下:
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
进行测试效果为下:
通过判断对应异常类型来判断返回的信息。
Seata
CAP定理
1.一致性:用户访问分布式系统中的任意节点,得到的数据必须一致。
2.可用性:用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
3.分区容错性:因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
-
Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
-
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
-
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
解决分布式事务问题
-
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。(如果最终总事务失败,则子事务会进行相反操作,类似回滚,比如:新增操作改为删除操作)
-
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。(可以理解成一个整体德事务)
配置安装seata
配置及安装步骤为下:
1.修改/conf/registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties" #也就是对应在nacos配置中心中对应德配置文件
}
}
在nacos德配置中心中创建seataServer.properties
其中配置的属性为下:
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
store.db.user=root
store.db.password=
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
在该配置中我们修改的是数据库的信息。
创建对应的seata数据库和创建对应的表
创建对应的表branch_tableh和global_table
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
通过cmd启动seata服务seata-server.bat
seata服务启动成功
微服务集成seata
1.导入对应的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>$1.4.2</version>
</dependency>
2.在需要使用seata的服务模块中配置对应堵塞seata配置
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默认为空
group: SEATA_GROUP # 分组,默认是DEFAULT_GROUP, 对应seataServer.properties所在的分组
application: seata-tc-server # seata服务名称, registry.conf中的seata服务的名字
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: SH #对应的集群名字
在seata服务中查看结果
总结
微服务如何根据这些配置寻找TC的地址呢?
我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:
-
namespace:命名空间
-
group:分组
-
application:服务名
-
cluster:集群名
以上四个信息,在刚才的yaml文件中都能找到:
namespace默认为public。
XA模式(CP模式解决方案)
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
XA模型
全部事务都完成后才进行才会将总事务进行提交。(强一致性,占用DB锁)
XA模式的优点是什么?
-
事务的强一致性,满足ACID原则。
-
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
-
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
-
依赖关系型数据库实现事务
springboot实现XA模式
在seata中对应的每个服务模块中的application.yml添加seata模式配置
seata:
data-source-proxy-mode: XA #设置seata模式为XA, 在seata事务对用的每个服务模块中进行设置
在开启seata事务的service上添加@GlobalTransactional
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
AT模式 (AP解决方案)
完善XA模式的缺陷。
AT模式模型
通过快照的方式完成数据会滚的操作,每个子事务都会在完成后直接提交,无需等待其他事务,减少事务之间的等待时间,优化性能。
阶段一RM的工作:
-
注册分支事务
-
记录undo-log(数据快照)
-
执行业务sql并提交
-
报告事务状态
阶段二提交时RM的工作:
-
删除undo-log即可
阶段二回滚时RM的工作:
-
根据undo-log恢复数据到更新前
特殊情况:脏读问题
在总事务执行过程中其他操作而导致数据库中对应表中的数据发生改表,而此后总事务中又进行读取快照恢复数据的操作,最终导致在中事务外完成的操作失效的问题,导致脏读。
解决方案:在AT模式中存在两种快照,前快照和后快照(就是在当前子事务中提交更改后的数据),子事务在进行数据恢复的时候会将数据库中的信息和后快照的数据进行对比,如果不一样就说明在总事务外执行了其他的操作,不会直接恢复数据,此时就会通知DBA进行处理。(此情况少出现)
死锁情况:事务一在完成子事务后释放对应的DB锁(全局锁还在事务一中),事务二会获取对应的DB锁,进行相应的操作后事务二要获取全局锁,在此同时事务一要获取DB锁,最终形成依赖循环(死锁),为了防止死锁情况seata设置了获取全局锁的重试时间小于获取DB锁的重试时间,最后事务二会进行回滚,解决死锁情况。
AT模式优缺点
AT模式的优点:
-
一阶段完成直接提交事务,释放数据库资源,性能比较好
-
利用全局锁实现读写隔离
-
没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
-
两阶段之间属于软状态,属于最终一致
-
框架的快照功能会影响性能,但比XA模式要好很多
springboot实现AT模式
创建数据库对应的表lock_table导入到TC服务关联的数据库(seata数据库),undo_log表导入到微服务关联的数据库。
在seata数据库中创建lock_table表
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
在对用项目数据库中创建undo_log表
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
在seata事务关联的每个服务模块中的application.yml中设置seata的模式
seata:
data-source-proxy-mode: AT
在开启seata事务的service上添加@GlobalTransactional
@Override
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。(需要我们手动实现)需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
例子:检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30 。
try阶段:完成对应使得操作,在将对应改变的值使用冻结数据记录下来。(TCC适合在修改操作时使用)
冻结金额使用独立的表记录。
Confirm阶段:Confirm也就是try阶段成功,就是进行持久化操作。(清除冻结表中对应的冻结数据)
Cancel阶段:也就是try阶段失败了,进行回滚的操作。(更改冻结表中对应表的数据, 回滚try时的数据库的操作)
TCC模型
TTC优缺点
TCC的优点是什么?
-
一阶段完成直接提交事务,释放数据库资源,性能好
-
相比AT模型,无需生成快照,无需使用全局锁,性能最强
-
不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
-
有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
-
软状态,事务是最终一致
-
需要考虑Confirm和Cancel的失败情况,做好幂等处理
事务悬挂和空回滚
空回滚:在执行Try阶段时发生堵塞,在没执行Try进行执行了Cancel,也就执行了空回滚的操作,为了防止Cancel中的操作导致数据库异常,我们需要进行空回滚操作,不需要执行真正的回滚操作,只需要记录回滚标致。
解决方案:执行Cancel操作时,应当判断Try是否已经执行,如果尚未执行,则应该空回滚。
业务悬挂:在执行空回滚操作后,阻塞恢复后,执行try操作,但是在空回滚时我们做了回滚的标志(最终会一直无法完成try阶段的操作就无法进入下个阶段),导致try无法执行也就无法进行Confirm和Cancel操作,最终导致业务悬挂。
解决方案:执行Try操作时,应当判断Cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂 。
实现TCC模式
在项目中创建用于存储冻结数据的表account_freeze_tbl
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(180) NOT NULL COMMENT '冻结数据的id',
`user_id` varchar(180) DEFAULT NULL COMMENT '用户的id',
`freeze_money` int DEFAULT '0' COMMENT '冻结的金额',
`state` int NOT NULL COMMENT '事务状态 0:try;1:confirm;2:cancel',
PRIMARY KEY (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中:
-
xid:是全局事务id
-
freeze_money:用来记录用户冻结金额
-
state:用来记录事务状态
编写Try, Confirm, Cancel对应的service接口
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
//TCC接口
@LocalTCC
public interface AccountTCCService {
//该注解就是表明对应的方法为try阶段的方法
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
//try方法中的参数需要使用注解@BusinessActionContextParameter标识, paramName表示该数据在上下文中的名字
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") int money);
boolean confirm(BusinessActionContext context); //形参表示事务上下文,用于获取全局的数据
boolean cancel(BusinessActionContext context); //形参表示事务上下文,用于获取全局的数据
}
编写对应的实现类,实现步骤为下:
-
Try业务:
-
记录冻结金额和事务状态到account_freeze表
-
扣减account表可用金额
-
-
Confirm业务
-
根据xid删除account_freeze表的冻结记录
-
-
Cancel业务
-
修改account_freeze表,冻结金额为0,state为2
-
修改account表,恢复可用金额
-
-
如何判断是否空回滚?
-
cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
-
-
如何避免业务悬挂?
-
try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
-
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class AccountTCCImpl implements AccountTCCService {
@Resource
AccountFreezeMapper accountFreezeMapper;
@Resource
AccountMapper accountMapper;
@Override
public void deduct(String userId, int money) {
//获取事务id,这个事务id是系统生成的,直接获取即可
String Xid = RootContext.getXID().toString();
AccountFreeze oldAccountFreeze = accountFreezeMapper.selectById(Xid);
//为了避免事务悬挂,我们需要判断如果冻结数据存在时是否已经被回滚了,如果回滚则不进行try,防止事务悬挂
if(oldAccountFreeze != null && oldAccountFreeze.getState() == AccountFreeze.State.CANCEL) {
return ;
}
//记录冻结金额和事务状态到account_freeze表中
AccountFreeze accountFreeze = new AccountFreeze();
//设置事务id
accountFreeze.setXid(Xid);
//设置冻结金额
accountFreeze.setFreezeMoney(money);
//设置此冻结数据的用户id
accountFreeze.setUserId(userId);
//设置此冻结数据的状态
accountFreeze.setState(AccountFreeze.State.TRY);
accountFreezeMapper.insert(accountFreeze);
//扣减account表中的可用金
accountMapper.deduct(userId, money);
}
@Override
//BusinessActionContext表示事务上下文
public boolean confirm(BusinessActionContext context) {
//此时try阶段的操作都成功了,所以我们需要删除account_freeze表中对应的冻结数据,
// 完成此删除操作才能使得下次进行try阶段的操作是不会出现事务悬挂的情况
//获取事务id
String Xid = context.getXid().toString();
int result = accountFreezeMapper.deleteById(Xid);
return result == 1;
}
@Override
//BusinessActionContext表示事务上下文
public boolean cancel(BusinessActionContext context) {
//此时说明try阶段的操作失败了,需要进行回滚
//判断对应的冻结数据是否存在,如果不存在就说明try阶段没有执行,进行空回滚
//获取Xid,通过Xid从数据库中获取对应的冻结信息
String Xid = context.getXid().toString();
//将account中对应的数据还原
if(accountFreezeMapper.selectById(Xid) == null) {
//空回滚
AccountFreeze accountFreeze = new AccountFreeze();
accountFreeze.setXid(Xid);
accountFreeze.setState(AccountFreeze.State.CANCEL);
accountFreezeMapper.updateById(accountFreeze);
}
//获取用户的id
String userId = context.getActionContext("userId").toString();
//获取对应的冻结金额
Integer money = (Integer) context.getActionContext("money");
accountMapper.refund(userId, money);
//设置account_freeze表中冻结数据的状态
AccountFreeze accountFreeze = new AccountFreeze();
accountFreeze.setXid(Xid);
accountFreeze.setFreezeMoney(0);
accountFreeze.setState(AccountFreeze.State.CANCEL);
//将冻结的数据的金额是设置为0,状态设置为回滚
int result = accountFreezeMapper.updateById(accountFreeze);
return result == 1;
}
}
进行测试
购买总价格大于用户金额无法购买,进行回滚
购买数量大于库存最终报错。
Saga模式
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。(如果某个子事务需要i回滚时就进行反向操作从而进行回滚)
Saga也分为两个阶段:
-
一阶段:直接提交本地事务
-
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚(进行相反操作)
Saga的优缺点
优点:
-
事务参与者可以基于事件驱动实现异步调用,吞吐高
-
一阶段直接提交事务,无锁,性能好
-
不用编写TCC中的三个阶段,实现简单
缺点:
-
软状态持续时间不确定,时效性差
-
没有锁,没有事务隔离,会有脏写
四种模式对比
高可用
配置seata集群步骤为下:
1.启动多个seata
节点名称 | ip地址 | 端口号 | 集群名称 |
---|---|---|---|
seata | 127.0.0.1 | 8091 | SH |
seata2 | 127.0.0.1 | 8092 | HZ |
配置seata的集群名称(修改registry.conf中的集群名称)
启动seata2
# -p 加上对应的端口
seata-server.bat -p 8092
集群效果为下:
2.实现seata集群热配置更新
创建配置文件client.properties
client.properties的配置为下:
# 事务组映射关系,也就是生效的集群名称
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
在seata对的每个服务模块中修改seata的配置(服务模块读取seata在nacos中的配置)
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties
3. 进行测试
使用SH集群中的seata的终端信息为下:
在nacos中client.properties的配置更改集群名称,HZ集群中的seata终端信息为下: