微服务介绍

需要学习哪些微服务知识?

image-20221229170121015

image-20221229170558080

认识微服务

服务架构

单体架构

image-20221229170908372

分布式架构

image-20221229170945526

image-20221229171046395

微服务

image-20221229171140907

总结

image-20221229171529382

微服务技术对比

image-20221229171714000

image-20221229171757880

企业需求

image-20221229171901412

SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
  • SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验.

image-20221229172020386

image-20221229172102618

服务拆分及远程调用

服务拆分

image-20221229172241196

远程调用

远程调用方式

订单模块可以像浏览器一样给用户模块发送一个得到用户信息的请求, 用户模块收到请求, 将用户信息响应回给订单模块.

基于RestTemplate发起的http请求实现远程调用http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。

image-20221229172444536

远程调用步骤

  1. 注册RestTemplate

    因为是订单模块要向用户信息模块发请求, 所以在order-serviceOrderApplication中注册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中的OrderServicequeryOrderById方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);

    //2. 发送获得用户信息请求, 并获得用户信息
    String url = "http://localhost:8081/user/" + order.getUserId();
    //getForObject是发送get请求, postForObject是post
    //第一个参数是url, 第二个参数将的到的json转化成什么类型
    User user = restTemplate.getForObject(url, User.class);

    //3. 将用户信息写入order
    order.setUser(user);

    // 4.返回订单信息
    return order;
    }

Eureka注册中心

Eureka的作用

服务调用出现的问题

  • 服务消费者该如何获取服务提供者的地址信息?
  • 如果有多个服务提供者,消费者该如何选择?
  • 消费者如何得知服务提供者的健康状态?

image-20221229173951281

当服务提供者的其中一个端口停止向注册中心心跳续约, 注册中心将会将该端口从服务列表中剔除.

  • 消费者该如何获取服务提供者具体信息?

    • 服务提供者启动时向eureka注册自己的信息
    • eureka保存这些信息
    • 消费者根据服务名称向eureka拉取提供者信息
  • 如果有多个服务提供者,消费者该如何选择?

    • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  • 消费者如何感知服务提供者健康状态?

    • 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
    • eureka会更新记录服务列表信息,心跳不正常会被剔除
    • 消费者就可以拉取到最新的信息

搭建EurekaServer

搭建EurekaServer服务步骤如下:

  1. 创建maven项目,引入spring-cloud-starter-netflix-eureka-server的依赖

    1
    2
    3
    4
    5
    <!--Eureka服务端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. 编写启动类,添加@EnableEurekaServer注解

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaServer //Eureka服务的开关
    public class EurekaApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
    }
    }
  3. 添加application.yml文件,编写下面的配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 10089
    spring:
    application:
    name: eurekaserver
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10089/eureka/

注册client端

将user-service服务注册到EurekaServer步骤如下:

  1. user-service项目引入spring-cloud-starter-netflix-eureka-client的依赖

    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: userserver //服务名称
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10089/eureka/

其他服务注册类似, 只需要将yml文件中的服务名称更改即可.

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

image-20221229175642055

Eureka界面

启动所有服务后, 打开http://localhost:10089, 出现以下界面

image-20221229180224377

红框里的就是服务列表.

服务拉取

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

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

    1
    2
    3
    //2. 发送获得用户信息请求, 并获得用户信息
    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
  2. 在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解

    1
    2
    3
    4
    5
    @Bean
    @LoadBalanced //负载均衡注解
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }

Ribbon负载均衡

负载均衡流程

  1. 订单模块发起获得用户信息请求.
  2. 请求被负载均衡拦截器拦截, RibbonLoadBanlancerClient获取url中的服务id交给DynamicServerListLoadBalancer
  3. DynamicServerListLoadBalancereureka-server拉取userservice列表信息.
  4. eureka-server将userservice的服务列表返回给DynamicServerListLoadBalancer.
  5. DynamicServerListLoadBalancer将服务列表交给IRule, IRule根据规则(这里是负载均衡)将某个服务返回给RibbonLoadBanlancerClient.
  6. RibbonLoadBanlancerClient修改一开始获得url, 然后发起请求.

image-20221229181637549

负载均衡策略

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

默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询

image-20221229184628312

image-20221229184650495

修改策略

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

  1. 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule

    1
    2
    3
    4
    @Bean
    public IRule randomRule() {
    return new RandomRule();
    }
  2. 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则

    1
    2
    3
    userservice:    //被指定规则的服务名
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

然后访问”http://localhost:8080/order/不同的订单id“ 后查看user-service的控制台日志可以发现确实改变了规则.

对比

  1. 代码方式: 配置灵活,但修改时需要重新打包发布.
  2. 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置.

饥饿加载

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

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

Nacos注册中心

安装Nacos(Linux)

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

安装JDK

Nacos依赖于JDK运行,所以Linux上也需要安装JDK才行。

/usr/local中, 新建一个java目录, 将jdk安装包上传到/usr/local/java中, 然后cd到java目录中解压缩:

1
tar -xvf jdk-8u144-linux-x64.tar.gz

随后cd到根目录, 输入vim /etc/profile.d/java.sh配置环境变量:

1
2
export JAVA_HOME=/usr/local/java/jdk1.8.0_144
export PATH=$PATH:$JAVA_HOME/bin

设置环境变量:

1
source /etc/profile

输入jps, 出现数字表示配置环境变量成功.

安装Nacos

/usr/local中, 新建一个nacos目录, 将Nacos安装包上传到/usr/local/nacos中, 然后cd到nacos目录中解压缩

1
tar -xvf nacos-server-1.4.1.tar.gz

然后删除安装包:

1
rm -rf nacos-server-1.4.1.tar.gz

cd到/usr/local/nacos/bin目录中,输入命令启动Nacos:

1
sh startup.sh -m standalone

启动日志如下

1
2
nacos is starting with standalone
nacos is starting, you can check the /usr/local/nacos/nacos/logs/start.out

服务注册到Nacos

  1. 在父工程中添加spring-cloud-alilbaba的管理依赖

    1
    2
    3
    4
    5
    6
    7
    8
    <!--nacos的管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
  2. 注释掉order-service和user-service中原有的eureka依赖

  3. 添加nacos的客户端依赖

    1
    2
    3
    4
    5
    6
    <!-- nacos客户端依赖包 -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2.2.6.RELEASE</version>
    </dependency>
  4. 修改user-service&order-service中的application.yml文件,注释eureka地址,添加nacos地址

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

image-20221229214632241

Nacos服务分级存储模型

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高, 本地集群不可访问时,再去访问其它集群

image-20221229214217927

设置服务集群属性

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

    1
    2
    3
    4
    5
    cloud:
    nacos:
    server-addr: LinuxIp:8848 # nacos 服务端地址
    discovery:
    cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州
  2. 在Nacos控制台可以看到集群变化

    image-20221229215157560

根据集群负载均衡

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

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

根据权重负载均衡

实际部署中会出现这样的场景:服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求.

Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高, 权重设置为0则完全不会被访问

image-20221229215930538

环境隔离 - namespace

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

image-20221229220052265

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

    image-20221229220120952

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

    image-20221229220201535

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

    image-20221229220527134

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

    1
    2
    3
    4
    5
    6
    7
    spring:
    cloud:
    nacos:
    server-addr: LinuxIp:8848 # nacos 服务端地址
    discovery:
    cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州
    namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID, 切记不是名字dev
  5. 重启order-service后,再来查看控制台

    image-20221229220959932

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

    image-20221229221009219

临时实例和非临时实例

服务注册到Nacos时,可以选择注册为临时或非临时实例,临时实例宕机时,会从nacos的服务列表中剔除,而非临时实例则不会.

通过下面的配置来设置

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: 129.211.8.47:8848 # nacos 服务端地址
discovery:
ephemeral: false # 设置为非临时实例

image-20221229221835546

Nacos与eureka比较

  1. Nacos与eureka的共同点

    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测
  2. Nacos与Eureka的区别

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

Nacos配置管理

统一配置管理

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。

Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。

image-20221230132343404

配置获取步骤

项目启动后读取bootstrap.yml文件, 根据bootstrap.yml文件中的配置得到需要在Nacos中读取的配置文件名称, 读取到Nacos中的配置文件后再去读取本地配置文件application.yml文件, 接着创建spring容器, 加载bean.

image-20221230142856216

Nacos配置设置步骤

注意: 不是所有的配置都适合放到配置中心,维护起来比较麻烦建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置.

  1. 在Nacos中添加配置信息

    image-20221230132712975

  2. 在弹出表单中填写配置信息(注意缩进!!!)

    image-20221230133623497

  3. 在user-service服务中,引入nacos-config的客户端依赖

    1
    2
    3
    4
    5
    6
    <!--nacos配置管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    <version>2.2.6.RELEASE</version>
    </dependency>
  4. 然后,在user-service中添加一个bootstrap.yaml文件(这里的配置需要根据Nacos配置文件名填写),内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    application:
    name: userservice # 服务名称
    profiles:
    active: dev #开发环境,这里是dev
    cloud:
    nacos:
    server-addr: LinuxIP:8848 # Nacos地址
    config:
    file-extension: yaml # 文件后缀名

    这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。

    本例中,就是去读取userservice-dev.yaml

  5. 删除application.yml中和bootstrap.yml重复的配置, 如服务名称、nacos地址等

  6. 在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Slf4j
    @RestController
    @RequestMapping("/user")
    @RefreshScope //注意要加这个注释
    public class UserController {

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("now")
    public String now(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
    }

配置自动刷新

Nacos中的配置文件变更后,微服务无需重启就可以感知。

实现自动刷新有两种方法(推荐使用第二种):

  • 方法一: 在@Value注入的变量所在类上添加注解@RefreshScope

image-20221230143556624

  • 方式二:新建一个PatternPorperties类, 在类中定义配置文件中的属性为成员变量, 并在类上使用@ConfigurationProperties注解. 在需要调用Nacos配置文件的属性时可以使用@Autowired注释来获得PatternPorperties类对象, 使用get方法获得对象.

    1. 新建PatternProperties类

      1
      2
      3
      4
      5
      6
      @Data
      @Component
      @ConfigurationProperties(prefix = "pattern")
      public class PatternProperties {
      private String dateformat;
      }
    2. 在UserController类中自动装配获得类对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class UserController {
      @Autowired
      private PatternProperties properties;

      @GetMapping("now")
      public String now(){
      return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
      }
      }

多环境配置共享

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

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

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

添加多环境配置步骤

  1. 在Nacos页面新建一个userservice.yml配置文件, 加入属性

    image-20221230152029256

  2. 更新PatternProperties类的成员变量

    1
    2
    3
    4
    5
    6
    7
    @Data
    @Component
    @ConfigurationProperties(prefix = "pattern")
    public class PatternProperties {
    private String dateformat;
    private String envShareValue;
    }
  3. 在UserController中新建接口

    1
    2
    3
    4
    5
    6
    7
    @Autowired
    private PatternProperties properties;

    @GetMapping("prop")
    public PatternProperties properties() {
    return properties;
    }
  4. 先启动一个环境是dev的userservice, 然后对另一个未启动的userserver按下 “shift+F4”, 在最下面的”Active profiles”中填入”test”修改环境, 然后运行测试.

    最后发现共享环境对两个userservice都生效, dev环境的配置对test环境的userservice不生效.

多种配置的优先级

[服务名]-[环境].yaml >[服务名].yaml > 本地配置

image-20221230150431815

共享配置文件

不同微服务之间可以共享配置文件,通过下面的两种方式来指定

  1. 方式一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    application:
    name: userservice # 服务名称
    profiles:
    active: dev # 环境,
    cloud:
    nacos:
    server-addr: LinuxIP:8848 # Nacos地址
    config:
    file-extension: yaml # 文件后缀名
    shared-configs: # 多微服务间共享的配置列
    - dataId: common.yaml # 要共享的配置文件id
  2. 方式二

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    application:
    name: userservice # 服务名称
    profiles:
    active: dev # 环境,
    cloud:
    nacos:
    server-addr: LinuxIP:8848 # Nacos地址
    config:
    file-extension: yaml # 文件后缀名
    extends-configs: # 多微服务间共享的配置列表
    - dataId: extend.yaml # 要共享的配置文件id

Nacos集群搭建

Nacos生产环境下一定要部署为集群状态, 搭建集群的基本步骤:

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

初始化数据库

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

官方推荐的最佳实践是使用带有主从的高可用数据库集群, 这里我们以单点的数据库为例来讲解。

image-20221230181151615

首先新建一个数据库,命名为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

进入到nacos文件中, 其中有以下两个重要文件

  • bin:启动脚本
  • conf:配置文件

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

#example下面修改添加自己的Nacos地址和端口:

1
2
3
4
#example
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=123

启动

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

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

nacos1:

1
server.port=8845

nacos2:

1
server.port=8846

nacos3:

1
server.port=8847

然后在三个Nacos文件的并目录下分别启动三个nacos节点:

1
startup.cmd

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://localhost/nacos即可。

代码中application.yml文件配置如下:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址

优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.

  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

http客户端Feign

Feign的介绍

RestTemplate方式调用存在的问题

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

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

存在下面的问题:

  • 代码可读性差,编程体验不统一
  • 当遇到参数复杂的URL时难以维护

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

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

定义和使用Feign客户端

使用Feign的步骤如下

  1. 在order-service服务的pom文件中引入feign的依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 在order-service的启动类添加注解开启Feign的功能

    image-20221230160448584

  3. 在order-service中新建一个client包, 在包内新建一个接口,内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package cn.itcast.order.client;

    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") //注意该服务名称是在Nacos中注册的服务名
    public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }

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

    • 服务名称:@FeignClient("userservice"), 注意该服务名称是在Nacos中注册的服务名
    • 请求方式:@GetMapping
    • 请求路径:/user/{id}
    • 请求参数:@PathVariable("id") Long id
    • 返回值类型:User

    这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。

  4. 在OrderService中调用UserClient接口来完成远程调用

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

    @Autowired
    private UserCilent userCilent;

    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    //2. 使用Feign远程调用, 获得用户信息
    User user = userCilent.findById(order.getUserId());
    //3. 将用户信息写入order
    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的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

下面以日志为例来演示如何自定义配置。

配置Feign日志

配置Feign日志有两种方式

  1. 方式一: 配置文件方式

    ①全局生效

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

    ②针对单个服务

    1
    2
    3
    4
    5
    feign:  
    client:
    config:
    userservice: # 针对某个微服务名, 服务名是在Nacos中注册的服务名
    loggerLevel: FULL # 日志级别
  2. 方式二:java代码方式

    1. 在orderservice中新建一个config包, 在包中新建一个FeignConfiguration类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      package cn.itcast.order.config;

      import feign.Logger;
      import org.springframework.context.annotation.Bean;

      public class FeignConfiguration {
      @Bean
      public Logger.Level feignLogLevel(){
      return Logger.Level.BASIC; //日志的级别
      }
      }
    2. 而后如果是全局配置,则把它放到orderservice启动类的@EnableFeignClients这个注解中

      1
      @EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

      如果是局部配置,则把它放到接口的@FeignClient注解中

      1
      @FeignClient(value = "userservice", configuration = FeignClientConfiguration.class) 

Feign的性能优化

Feign底层的客户端实现:

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

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

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

连接池配置

这里以Feign添加HttpClient的支持为例

  1. 在order-service的pom文件中引入Apache的HttpClient依赖

    1
    2
    3
    4
    5
    <!--httpClient的依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 在order-service的application.yml中添加配置

    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的最佳实践

仔细观察可以发现,Feign的客户端与服务提供者的controller代码非常相似.

有以下两种方法简化重复的代码编写.

方式一(继承)

image-20221230231857964

给消费者的FeignClient和提供者的controller定义统一的父接口作为标准, 一样的代码可以通过继承来共享.

实现步骤如下.

  1. 新建一个Module, 命名为feign-api

  2. 定义一个API接口,定义返回类型是泛型的方法,并基于SpringMVC注解做声明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package cn.itcast.feign.api;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;

    @RequestMapping("/user")
    public interface UserAPI<T> {
    @GetMapping("/{id}")
    T findById(@PathVariable("id") Long id);
    }
  3. 为了避免类型不统一的情况, **orderservice(消费者)应删除userservice(提供者)**的User(相关pojo)类, 并在pom文件中引入userservice的依赖, 将order类中的User类更改成userservice中的User类.

  4. 在userservice的pom文件中引入feign-api依赖, 然后Controller类直接实现UserAPI<User>接口(泛型一定要指定), 只需要加@RestController注解. 除了重写的方法不指定请求的类型, 非重写的方法仍要写请求类型.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Slf4j
    @RestController
    public class UserController implements UserAPI<User> {

    @Autowired
    private UserService userService;

    @Override
    public User findById(@PathVariable("id") Long id) {
    return userService.queryById(id);
    }

    @GetMapping("prop")
    public PatternProperties properties() {
    return properties;
    }
    }
  5. orderservice的UserCilent接口直接继承UserAPI<User>, 并加上@FeignClient(value = "userservice")注解, 注意泛型一定要指定.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package cn.itcast.order.clients;

    import cn.itcast.feign.api.UserAPI;
    import cn.itcast.user.pojo.User;
    import org.springframework.cloud.openfeign.FeignClient;

    @FeignClient("userservice")
    public interface UserCilent extends UserAPI<User> {
    }
  6. orderservice调用UserCilent接口.

    1
    2
    //2. 使用Feign远程调用, 获得用户信息
    User user = userCilent.findById(order.getUserId());

方式一因为课程里并没有讲怎么具体实现, 以上步骤是由写者自行实现的, 所以可能有些处理不好的地方, 但确实是可以运行的.

优点:

  • 简单
  • 实现了代码共享

缺点:

  • 服务提供方、服务消费方紧耦合

  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

方式二(抽取)

image-20221230231816896

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

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用

抽取

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

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

然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

在order-service中使用feign-api

首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。

在order-service的pom文件中中引入feign-api的依赖:

1
2
3
4
5
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>

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

解决扫描包问题

此时因为UserClient现在在cn.itcast.feign.clients包下,而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。解决该问题有如下两种方法, 推荐使用第二种.

  1. 方式一:指定Feign应该扫描的包(@EnableFeignClients(basePackages = "cn.itcast.feign.clients"))

    1
    2
    3
    4
    5
    6
    7
    8
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    @MapperScan("cn.itcast.order.mapper")
    @SpringBootApplication
    public class OrderApplication {
    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }
    }
  2. 方式二:指定需要加载的Client接口(@EnableFeignClients(clients = {UserCilent.class})) (推荐)

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

Gateway服务网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

网关介绍

image-20221230232333919

网关功能

  • 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

  • 路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。

  • 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

网关技术

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

  • gateway
  • zuul

Zuul是基于Servlet的实现,属于阻塞式编程。

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

搭建网关服务

搭建网关的基本路由步骤如下

  1. 新建一个名为gateway的Module,引入如下依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--网关-->
    <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. 编写启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package cn.itcast.gateway;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class GatewayApplication {

    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
    }
    }
  3. 创建application.yml文件,内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    server:
    port: 10010 # 网关端口
    spring:
    application:
    name: gateway # 服务名称
    cloud:
    nacos:
    server-addr: LinuxIP: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/**

路由配置包括:

  1. 路由id:路由的唯一标示

  2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡

  3. 路由断言(predicates):判断路由的规则,

  4. 路由过滤器(filters):对请求或响应做处理

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。

本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。

image-20221230235653077

启动网关启动类,访问http://localhost:10010/user/1时,符合`/user/**`规则,请求转发到uri:http://userservice/user/1,得到了结果

断言工厂

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

例如Path=/user/**是按照路径匹配,这个规则是由

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来

处理的,像这样的断言工厂在SpringCloudGateway还有十几个:

名称 说明 示例
After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17: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 权重处理

我们只需要掌握Path这种路由工程就可以了。

例如: 在断言中加入After

1
2
3
4
5
6
7
8
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] #在上海时间2031年4月后才能访问

我们重启网关服务, 发现http://localhost:10010/user/1已经访问不了了, 因为我们在断言中加入After2031, 而现在是2022年.

过滤器工厂

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

image-20221231001128093

路由过滤器的种类

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

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

请求头过滤器

下面我们以AddRequestHeader 为例来讲解。

需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

只需要修改gateway服务的application.yml文件,添加路由过滤即可:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, Lucy is belong to me! # 添加请求头

然后在UserController中修改方法获取下请求头, 注意UserAPI也要改方法

1
2
3
4
5
6
@Override
public User findById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println(truth);
return userService.queryById(id);
}

当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。

默认过滤器

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, Lucy is belong to me!

总结

过滤器的作用是什么?

① 对路由的请求或响应做加工处理,比如添加请求头

② 配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

① 对所有路由都生效的过滤器

全局过滤器

网关提供的31种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现, 此时就可以使用全局过滤器。

全局过滤器作用

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与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);
}

在filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

自定义全局过滤器

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

  • 参数中是否有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
23
24
25
26
27
28
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}

设置自定义拦截器优先级

设置自定义拦截器的优先级有两种方法

  1. 在自定义类上加上@Order()注解, 注解括号内填[-2147483647, 2147483647]的整数, 数字越小优先级越高

    1
    2
    3
    @Order(-1)
    @Component
    public class AuthorizeFilter implements GlobalFilter {... }
  2. 自定义类再实现一个名为Ordered的接口, 实现接口内的getOrder方法, 返回一个int类型, 数字越小优先级越高

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    public class AuthorizeFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
    return -1;
    }
    ...
    }

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、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,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html

解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 # 这次跨域检测的有效期