springcloud


微服务技术栈

微服务治理

认识微服务

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署

优点

  • 架构简单
  • 部署成本低

缺点:

  • 耦合度高
分布式架构

分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,就称为一个服务。

优点:

  • 降低服务耦合度
  • 有利于服务升级拓展

服务治理

分布式架构要考虑的问题:

  • 服务拆分粒度如何?
  • 服务集群地址如何维护?
  • 服务之间如何实现远程调用?
  • 服务健康状态如何感知?

微服务

微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:微服务对外暴露业务接口
  • 自治:团队独立、技术独立、数据独立、部署独立
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

微服务结构

微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是 SpringCloud 和 阿里巴巴的 Dubbo。


  • 注册中心
  • 配置中心
  • 服务集群
  • 服务网关

微服务技术对比

Dubbo SpringCloud SpringCloudAlibaba
注册中心 zookeeper、Redis Eureka、Consul Nacos、Eureka
服务远程调用 Dubbo 协议 Feign(http 协议) Dubbo、Fegin
配置中心 SpringCloudConfig SpringCloudConfig、Nacos
服务网关 SpringCloudGateway、Zuul SpringCloudGateway、Zuul
服务监控和保护 dubbo-admin、功能弱 Hystrix Sentinel

企业需求

  • SpringCloud + Fegin
    • 使用 SpringCloud 技术栈
    • 服务接口使用 Restful 风格
    • 服务调用采用 Fegin 方式
  • SpringCloudAlibaba + Dubbo
    • 使用 SpringCloudAlibaba 技术栈
    • 服务接口采用 Dubbo 协议标准
    • 服务调用采用 Dubbo 方式
  • SpringCloudAlibaba + Fegin
    • 使用 SpringCloudAlibaba 技术栈
    • 服务接口采用 Restful 风格
    • 服务调用采用 Fegin 方式
  • Dubbo 原始模式
    • 基于 Dubbo 老旧技术体系
    • 服务接口采用 Dubbo 协议标准
    • 服务调用采用 Dubbo 方式

SpringCloud

  • SpringCloud 是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。

  • SpringCloud集成了各种微服务功能组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验。


  • SpringCloud 与 SpringBoot 的版本兼容关系如下:
Release Train Boot Version
2020.0.x aka Ilford 2.4.x
Hoxton 2.2.x,2.3.x(Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x
  • 本次使用的版本是 Hoxton.SR10,因此对应的 SpringBoot 版本是 2.3.x 版本。

服务拆分与远程调用

服务拆分注意事项

  • 1、不同微服务,不要重复开发相同业务
  • 2、微服务数据独立,不要访问其他微服务的数据库
  • 3、微服务可以将自己的业务暴露为接口,供其他微服务调用
导入服务拆分 Demo
  • 项目结构
    • cloud-demo
      • order-service(根据 id 查询订单)
      • user-service(根据 id 查询用户)
案例:根据订单 id 查询订单功能

需求:根据订单 id 查询订单的同时,把订单所属的用户信息一起返回

远程调用方式分析

1、注册 RestTemplate

在 order-service 的 OrderAppplication 中注册 RestTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

2、服务远程调用 RestTemplate

修改 order-service 中的 OrderService 中的 queryOrderById 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class OrderService {

@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用 RestTemplate 发起 http 请求,查询用户
// 2.1 url 路径
String url = "http://localhost:8081/user/" + order.getUserId();
// 2.2 发送 http 请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
// 3.封装 user 信息
order.setUser(user);
// 4.返回
return order;
}
}

Eureka

提供者与消费者
  • 服务提供者:一次约业务中,被其他微服务调用的服务。(提供接口给其他微服务)
  • 服务消费者:一次业务中,调用其他微服务的服务。(调用其他微服务提供的接口)

思考:服务 A 调用 B,服务 B 调用服务 C,那么服务 B 是什么角色?(答:相对而言,业务不同身份不同)

Eureka 注册中心

服务调用出现的问题:http://localhost:8081/user/" + order.getUserId();

  • 服务消费者该如何获取服务者提供的地址信息?
  • 如果有多个服务者,消费者该如何选择?
  • 消费者如何得知服务提供者的健康状态?
Eureka 的作用
  • 消费者如何获取服务提供者的具体信息?
    • 服务提供者启动时向 Eureka 注册自己的信息
    • Eureka 保存这些信息
    • 消费者根据服务名称向 Eureka 拉取提供者信息
  • 如果有多个服务者,消费者该如何选择?
    • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  • 消费者如何得知服务提供者的健康状态?
    • 服务提供者会每隔 30 秒向 EurekaServer 发送心跳请求,报告健康状态
    • Eureka 会更新记录到服务列表信息,心跳不正常会被删除
    • 消费者就可以拉取到最新的信息
动手实践
搭建 EurekaServer

搭建 EurekaServer 服务步骤如下:

1、创建项目,引入 spring-cloud-starter-netfix-eureka-server 的依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netfix-eureka-server</artifactId>
</dependency>

2、编写启动类,添加 @EnableEurekaServer 注解

3、添加 application.yml 文件,编写下面配置:

1
2
3
4
5
6
7
8
9
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver # eureka 的服务名称
eureka:
client:
service-url: # eureka 的地址信息
defaultZone: http://127.0.0.1:10086/eureka

image-20221021163552326

注册 user-service

将 user-service 项目引入到 EurekaServer 步骤如下:

1、在 user-service 项目中引入 spring-cloud-starter-netfix-eureka-client 的依赖

注意:记得导入 spring-boot-starter-web 包

1
2
3
4
5
<!-- eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2、在 application.yml 文件,编写下面的配置:

1
2
3
4
5
6
7
spring:
application:
name: userservice # user 服务器名称
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka

另外,我们可以将 user-service 多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口配置

image-20221025183507437

在 order-service 完成服务拉取

服务器拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡

1、修改 OrderService 的代码,修改访问的 url 路径,用服务名代替 ip、端口:

1
String url = "http://userserice/user" + order.getUserId();

2、在 order-service 项目的启动类 OrderApplication 中的 RestTemplate 添加负载均衡注解:

1
2
3
4
5
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

Ribbon 负载均衡

负载均衡流程

image-20221025191630703

image-20221025192145321

负载均衡策略

Ribbon 的负载均衡规则是一个叫做 IRule 的接口来定义的,每一个子接口都是一种规则:

image-20221025192341277

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是 Ribbon 默认的负载均衡规则
AvailabilityFilteringRule 对以下两种服务器进行忽略:(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“”短路“状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级的增加。(2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了 AvailabilityFilteringRule 规则的客户端也会将其忽略。并发连接数的上线,可以由客户端的clientName.clientConfigNamespace.ActiveConnextionsLimit 属性进行配置
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用 Zone 对服务器进行分类,这个 Zone 可以理解为一个机房、一个机架等。然后再对 Zone 内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器
RetryRule 重试机制的选择逻辑

通过定义 IRule 实现可以修改负载均衡规则,有两种方式:

1、代码方式(全局):在 order-service 中的 OrderApplication 类中,定义一个新的 Rule:

1
2
3
4
5
@Bean
public IRule randomRule() {
// return 一个要实现的方式
return new RandomRule();
}

2、配置文件方式(局部):在 order-service 的 application.yml 文件中,添加新的配置也可以修改规则:

1
2
3
userservice: # 针对某个服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
饥饿加载

Ribbon 默认是采用懒加载,即第一次访问时才会去创建 LoadBalancerClient,请求的时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
ribbon:
eager-load:
enable: true # 开启饥饿加载
clients: userservice # 指定对 userservice 这个服务饥饿加载

Nacos 注册中心

Nacos 是阿里巴巴的产品,现在是 SpringCloud 中的一个组件。相比 Eureka 功能更加丰富,在国内受欢迎的程度较高。

Nacos 安装指南
Windows 安装

开发阶段采用单机安装即可。

1.1、下载安装包

在 Nacos 的 github 页面,提供有下载连接,可以下载编译好的 Nacos 服务端或者源码

github 主页:https://github.com/alibaba/nacos

github 的 release 下载页:https://github.com/alibaba/nacos/releases

1.2、解压

1.3、端口配置

打开 conf 文件夹下的 application.properties 文件找到 server.port 即可更改端口,默认端口为 8848

1.4、启动

启动非常简单,进入 bin 目录,执行命令:startup.cmd -m standalone 即可

注意:nacos 默认账户和密码都为 nacos

服务注册到 Nacos

1、在父工程中添加 spring-cloud-alibaba 的管理依赖:

1
2
3
4
5
6
7
8
<!--nacos的管理依赖-->
<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>

2、注释掉所有 eureka 依赖

3、添加 nacos 的客户端依赖

1
2
3
4
5
<!-- nacos 客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

4、修改项目配置文件(application.yml),注释掉 eureka 地址,添加 nacos 地址:\

1
2
3
4
spring: 
cloud:
nacos:
server-addr: localhost:8848 # nacos 服务地址

5、启动并测试

Nacos 服务分级存储模型

image-20221026194001296

服务跨集群调用问题

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高

本地集群不可访问时,再去访问其他集群

服务集群属性

1、修改 application.yml,添加如下内容:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos 服务地址
discovery:
cluster-name: HZ # 配置集群的名称,也就是机房位置,例如:HZ 杭州

2、在 Nacos 控制台可以看到集群变化:

image-20221026194607991

根据集群负载均衡

1、修改 order-service 中的 application.yml,设置集群为 HZ:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848 # nacos 服务地址
discovery:
cluster-name: HZ # 配置集群的名称,也就是机房位置,例如:HZ 杭州

2、然后在 order-service 中设置负载均衡的 IRule 为 NacosRule,这个规则会优先寻找与自己同集群的服务:

1
2
3
userservice:
ribbion:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.NacosRule # 负载均衡规则

3、注意将 user-service 的权重都设置为 1

根据权重负载均衡

实际部署中会出现这样的场景:

  • 服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求

Nacos 提供了权重配置来控制访问频率,权重越大则访问频率越高、


1、在 Nacos 控制台可以设置实例的权重值,首先选中实例后面的编辑按钮

2、将权重设置为 0.1,测试可以发现 8081 被访问到的频率大大降低(权重为 0 时不会被访问)

环境隔离-namespace

Nacos 中服务存储和数据存储的最外层都是一个名为 namespace 的东西,用来做最外层隔离

image-20221026200337920

1、在 Nacos 控制台可以创建 namespace,用来隔离不同环境

2、然后填写一个新的命名空间信息

3、保存后会在控制台看到这个命名空间的 id

4、修改 order-service 的 application.yml,添加 namespace

1
2
3
4
5
6
7
spring: 
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SH
namespace: 33741fd3-501d-496d-a1cd-02b086f9c789 # 命名空间,填 ID

5、重启 order-service 后,再来查看控制台

6、此时访问 order-service,因为 namespace 不同,会导致找不到 userservice,控制台会报错

Nacos 注册中心细节分析

image-20221026201357963

临时实例和非临时实例

服务注册到 Nacos 时,可以选择注册为临时或非临时实例,通过下面的配置来设置:

1
2
3
4
5
spring: 
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例

总结:

1、Nacos 与 Euerka 的共同点

  • 都支持服务注册和服务拉取
  • 都支持服务提供者心跳方式做健康检测

2、Nacos 与 Eureka 的区别

  • Nacos 支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
  • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
  • Nacos 支持服务列表变更的消息推送模式,服务列表更新及时
  • Nacos 集群默认采用 AP 方式,当集群中存在非临时实例时,采用 CP 模式;Eureka 采用 AP 方式

Nacos 配置管理

统一配置管理
  • 配置更改热更新

image-20221028115620807

在 Nacos 中添加配置信息:

image-20221028120212384

在弹出表单中填写配置信息:

image-20221028120311235

注意:配置内容不要无脑填,要填那些将来会发生变化的内容

微服务配置拉取

image-20221028120636298

1、引入 Nacos 的配置管理端客户依赖:

1
2
3
4
5
<!-- nacos 配置管理依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

2、在 userservice 中的 resource 目录添加一个 bootstrap.yml 文件,这个文件是引导文件,优先级高于 application.yml:

1
2
3
4
5
6
7
8
9
10
spring: 
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 #nacos 地址
config:
file-extension: yaml # 文件后缀名

3、测试

我们在 user-service 中将 pattern.dateformat 这个属性注入到 UserController 中做测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {
// 注入 nacos 中的配置属性
@Value("${pattern.dateformat}")
private String dateformat;

// 编写 Controller,通过日期格式化器来格式化现在时间并返回
@GetMapping("/now")
public String now() {
return LocalDate.now().format(DateTimeFormatter.ofPattern(dateformat, Locale.CHINA));
}
}
配置热更新

Nacos 中的配置文件变更后,微服务无需添加就可以感知。不过需要通过下面两种配置实现:

  • 方式一:在 @Value 注入的变量所在类上添加注解 @RefreshScope
1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
// 注入 nacos 中的配置属性
@Value("${pattern.dateformat}")
private String dateformat;
}
  • 方式二:使用 @ConfigurationProperties 注解
1
2
3
4
5
6
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
多环境配置共享

微服务启动时会从 nacos 读取多个配置文件:

  • [spring.application.name]-[spring-profiles.active].yaml,例如:userservice-dev.yaml
  • [spring-application.name].yaml,例如:userservice.yaml

无论 profile 如何变化,[spring.application.name].yaml 这个文件一定会加载,因此多环境共享配置可以写入这个文件


多种配置的优先级:

  • 服务名-profile.yaml > 服务名称.yaml > 本地配置
Nacos 集群搭建

Nacos 生产环境下一定要部署为集群状态

集群结构图

image-20221028124737015

搭建集群

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载 nacos 安装包
  • 配置 nacos
  • 启动 nacos 集群
  • nginx 反向代理

初始化数据库

Nacos默认数据存储在内嵌数据库 Derby 中,不属于生产可用的数据库。

官方推荐的最佳实践时使用带有主从的高可用数据库集群。

这里我们以单点的数据库为例来讲解。

首先新建一个数据库,命名为 nacos,然后导入下面的 sql:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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:

然后添加内容:

1
2
3
127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847

然后修改 application.properties

1
2
3
4
5
6
7
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root

将 nacos 文件夹复制 3 份,分别命名为:nacos1、nacos2、nacos3

然后分别修改三个文件夹中的 application.properties

nacos1:

1
server.port:8845

nacos2:

1
server.port:8846

nacos3:

1
server.port:8847

然后分别启动 nacos(注意!!!文件目录有中文可能会报错)

Nginx 反向代理

解压 Nginx 安装包到任意非中文目录下:

修改 conf/nginx.conf 文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream nacos-cluster {
server 127.0.0.1:8845
server 127.0.0.1:8846
server 127.0.0.1:8847
}

server {
listen 80;
server_name localhost;

location /nacos {
proxy_pass http://nacos-cluster;
}
}

http 客户端 Feign

RestTemplate 方式调用存在的问题

先看看我们以前利用 RestTemplate 发起远程调用的代码:

1
2
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);

存在下面的问题:

  • 代码可读性差,编程体验不统一
  • 参数复杂 URL 难以维护
Feign 的介绍

Feign 是一个声明式的 http 客户端,官方地址:https://github.com/OpenFeign/feign

其作用就是帮助我们优雅的实现 http 请求发送,解决上面提到的问题

定义和使用 Feign 客户端

使用 Feign 的步骤如下:

1、引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、在 order-service 的启动类添加注解开启 Feign 的功能:

1
2
3
4
5
6
7
8
@EnableFeignClients
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}

3、编写 Feign 客户端:

1
2
3
4
5
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}

主要是基于 SpringMVC 的注解来声明远程调用的信息,比如:

  • 服务名称:userservice
  • 请求方式:GET
  • 请求路径:/user/{id}
  • 请求参数: Long id
  • 返回值类型:User

4、用 Feign 客户端代替 RestTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private UserClient userClient;

public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用 Feign 发起 http 请求,查询用户
User user = userClient.findById(order.getUserId());
// 3.封装 user 信息
order.setUser(user);
// 4.返回
return order;
}
自定义 Feign 的配置

Feign 运行自定义配置来覆盖默认配置,可以修改的配置如下:

类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json 字符串为 java 对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过 http 请求发送
feign.Contract 支持的注解格式 默认是 SpringMVC 的注解
feign.Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用 Ribbon 的重试

一般我们需要配置的就是日志级别


配置 Feign 日志有两种方式:

方式一:配置文件方式

1、全局生效:

1
2
3
4
5
feign: 
client:
config:
default: # 这里用 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别

2、局部生效

1
2
3
4
5
feign: 
client:
config:
userservice: # 这里用 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别

方式二:java 代码方式,需要先声明一个 Bean:

1
2
3
4
5
6
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevle() {
return Logger.Level.BASIC;
}
}

1、如果是全局配置,则把它放到 @EnableFeignClients 这个注解中:

1
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

2、如果是局部配置,则把它放到 @FeignClient 这个注解中:

1
@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)
Feign 的性能优化

Feign 底层的客户端实现:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

因此优化 Feign 的性能主要包括:

  • 1、使用连接池代替默认的 URLConnection
  • 2、日志级别,最好用 basic 或 none

Feign 的性能优化-连接池配置

引入依赖:

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactedId>feign-httpclient</artifactedId>
</dependency>

配置连接池:

1
2
3
4
5
6
7
8
9
feign: 
client:
config:
default: # default 全局的配置
loggerLevel: BASIC # 日志级别,BASIC 就是基本的请求和响应信息
httpclient:
enabled: true # 开启 feign 对 httpClient 的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
Feign 的实践

方法一(继承):给消费者的 FeignClient 和 提供者的 Controller 定义统一的 父接口作为标准。

  • 服务紧耦合
  • 父接口参数列表中的映射不会被继承

image-20221028155659086

方式二(抽取):将 FeignClient抽取为独立模块,并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用

image-20221028160149752

抽取 FeignClient

实现最佳实践方式二的步骤如下:

1、首先创建一个 module,命名为 feign-api,然后引入 feign 的 starter 依赖

2、将 order-service 中编写的 UserClient、User、DefaultFeignConfiguration 都复制到 feign-api 项目中

3、在 order-service 中引入 feign-api 的依赖

4、修改 order-service 中的所有与上述三个组件有关的 import 部分,改成导入 feign-api 中的包

5、重启测试


当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围内时,这些 FeignClient 无法使用。有两种方法解决。

方式一:指定 FeignClient 所在包

1
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")

方式二:指定 FeignClient 字节码

1
@EnableFeignClients(clients = {userClients.class})

统一网关 Gateway

为什么需要网关

网关功能:

  • 身份认证和权限校验
  • 服务路由、负载均衡
  • 请求限流
网关的技术实现

在 SpringCloud 中网关的实现包括两种:

  • gateway
  • zuul

zuul 是基于 Servlet 的实现,属于阻塞式编程。而 SpringCloudGateway 则是基于 Spring5 中提供的 WebFlux,属于响应式编程的实现,具备更好的性能。

搭建网关服务

搭建网关服务的步骤:
1、创建新的 module,引入 SpringCloudGateway 的依赖和 nacos 的服务发现依赖:

1
2
3
4
5
6
7
8
9
10
11
<!-- 网关依赖 -->
<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>

2、编写路由配置及 nacos 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 10010
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos 地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以 /user 开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
路由断言工厂 Route Predicate Factory

网关路由可以配置的内容包括:

  • 路由 id:路由唯一标识
  • uri:路由目的地,支持 lb(loadbalanced) 和 http 两种
  • predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
  • filters:路由过滤器,处理请求或响应

我们在配置文件中写的断言规则只是字符串,这些字符串会被 Predicate Factory 读取并处理,转变为路由判断的条件

例如 Path=/user/** 是按照路径匹配,这个规则是由 org.springframework.cloud.gateway.handler.predicate.PathRouterPredicateFactory 类来处理的

像这样的断言工厂在 SpringCloudGateway 还有十几个

名称 说明 示例
After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 - Before=2037-01-20T17:42:47.789-07:00[America/Denver]
Between 是某两个时间点之间的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver],2037-02-20T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些 Cookie - Cookie=chocolate,ch.p
Header 请求必须包含某些 header - Header=X-Request-Id,\d+
Host 请求必须是访问某个 host(域名) - Host=**.somehost.org,**.anotherhost.org
Method 请求方式必须是指定方式 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数 - Query=name,Jack 或者 - Query=name
RemoteAddr 请求者的 ip 必须是指定范围 - RemoteAddr=192.168.1.1/24
Weight 权重处理
路由过滤器 GatewayFilter

GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:

image-20221105153418207

Spring 提供了 31 种不同的路由过滤器工厂。例如:

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 从响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除一个响应头
RequestRateLimiter 限制请求的流量
全局过滤器 GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样。

区别在于 GatewayFilter 通过配置定义,处理逻辑是固定的。而 GlobalFilter 的逻辑需要自己写代码实现。

定义方式是实现 GlobalFilter 接口。

1
2
3
4
5
6
7
8
9
10
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过 {@link GatewayFilterChain} 将请求交给下一个过滤器处理

@param exchange 请求上下文,里面可以获取 Request、Response 等信息
@param chain 用来把请求委托给下一个过滤器
@return {@code Mono<Void>} 返回标示当前过滤器业务结束
**/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
案例-定义全局过滤器,拦截并判断用户身份

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有 authorization
  • authorization 参数值是否为 admin

如果同时满足则放行,否则拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取参数中的 authorization 参数
String auth = params.getFirst("authorization");
// 3.判断参数值是否等于 admin
if("admin".equals(auth)) {
// 4.是,放行
return chain.filter(exchange);
}
// 5.否,拦截
// 5.1.设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2.拦截请求
return exchange.getResponse().setComplete();
}
}
过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和 DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器


  • 每一个过滤器都必须指定一个 int 类型的 order 值,order 值越小,优先级越高,执行顺序越靠前
  • GlobalFilter 通过实现 Ordered 接口,或者添加 @Order 注解来指定 order 值,由我们自己指定
  • 路由过滤器和 defaultFilter 的 order 由 Spring 指定,默认是按照声明顺序从 1 递增
  • 当过滤器的 order 值一样是,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行

可以参考下面几个类的源码来查看:


org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters() 方法是先加载 defaultFilters,然后再加载某个 route 的 filters,然后合并。

org.springframework.cloud.gateway.handler.FilteringWebHandler#handle() 方法会加载全局过滤器,与前面的过滤器合并后根据 order 排序,组织过滤器链

跨域问题处理

跨域:域名不一致就是跨域,主要包括:

跨域问题:浏览器禁止请求的发起者与服务端发生跨域 ajax 请求,请求被浏览器拦截的问题

解决方案:CORS


网关处理跨域采用的同样是 CORS 方案,并且只需要简单配置即可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring: 
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决 options 请求被拦截问题
corsConfigurations:
'[/**]':
allwoedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许跨域 ajax 的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带 cookie
maxAge: 360000 # 这次跨域检测的有效期

Docker

异步通信

分布式搜索

微服务保护

分布式事务

分布式缓存

多级缓存

可靠消息服务

Nacos 源码

Sentinel 源码


文章作者: Water monster
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Water monster !
评论
  目录