微服务框架课程体系

image-20230315221733615

认识微服务

单体架构

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

image-20210713202807818

单体架构的优缺点如下:

优点:

  • 架构简单
  • 部署成本低

缺点:

  • 耦合度高(维护困难、升级困难)

分布式架构

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

image-20210713203124797

分布式架构的优缺点:

优点:

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

缺点:

  • 服务调用关系错综复杂

分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:

  • 服务拆分的粒度如何界定?
  • 服务之间如何调用?
  • 服务的调用关系如何管理?

人们需要制定一套行之有效的标准来约束分布式架构。

微服务

微服务的架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

image-20210713203753373

微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。

因此,可以认为微服务是一种经过良好架构设计的分布式架构方案

但方案该怎么落地?选用什么样的技术栈?全球的互联网公司都在积极尝试自己的微服务落地方案。

其中在Java领域最引人注目的就是SpringCloud提供的方案了。

SpringCloud

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

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

其中常见的组件包括:

image-20210713204155887

总结

  • 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统

  • 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝

  • 微服务:一种良好的分布式架构方案

    ①优点:拆分粒度更小、服务更独立、耦合度更低

    ②缺点:架构非常复杂,运维、监控、部署难度提高

  • SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件

Eureka注册中心

Eureka的结构和作用

这些问题都需要利用SpringCloud中的注册中心来解决,其中最广为人知的注册中心就是Eureka,其结构如下:

image-20210713220104956

回答之前的各个问题。

  1. 问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

  • user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
  • eureka-server保存服务名称到服务实例地址列表的映射关系
  • order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取
  1. 问题2:order-service如何从多个user-service实例中选择具体的实例?
  • order-service从实例列表中利用负载均衡算法选中一个实例地址
  • 向该实例地址发起远程调用
  1. 问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
  • user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
  • 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
  • order-service拉取服务时,就能将故障实例排除了

注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端

因此,接下来我们动手实践的步骤包括:

image-20210713220509769

搭建eureka-server

首先大家注册中心服务端:eureka-server,这必须是一个独立的微服务

创建eureka-server服务

  1. 在cloud-demo父工程下,创建一个子模块:

  2. 填写模块信息:

  3. 然后填写服务信息:

引入eureka依赖

引入SpringCloud为eureka提供的starter依赖:

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

编写启动类

给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能:

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}

编写配置文件

编写一个application.yml文件,内容如下:

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

启动服务

启动微服务,然后在浏览器访问:http://127.0.0.1:10086

看到下面结果应该是成功了:

image-20210713222157190

服务注册

下面,我们将user-service注册到eureka-server中去。

引入依赖

在user-service的pom文件中,引入下面的eureka-client依赖:

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

配置文件

在user-service中,修改application.yml文件,添加服务名称、eureka地址:

1
2
3
4
5
6
7
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka

启动多个user-service实例

为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service。

首先,复制原来的user-service启动配置:

image-20210713222656562

然后,在弹出的窗口中,填写信息:

image-20210713222757702

现在,SpringBoot窗口会出现两个user-service启动配置:

image-20210713222841951

不过,第一个是8081端口,第二个是8082端口。

启动两个user-service实例:

image-20210713223041491

查看eureka-server管理页面:

image-20210713223150650

总结

  1. 服务注册
  • 引入eureka-client依赖
  • 在application.yml 中配置eureka地址

服务发现

下面,我们将order-service的逻辑修改:向eureka-server拉取user-service的信息,实现服务发现。

引入依赖

之前说过,服务发现、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致。

在order-service的pom文件中,引入下面的eureka-client依赖:

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

配置文件

服务发现也需要知道eureka地址,因此第二步与服务注册一致,都是配置eureka信息:

在order-service中,修改application.yml文件,添加服务名称、eureka地址:

1
2
3
4
5
6
7
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka

服务拉取和负载均衡

最后,我们要去eureka-server中拉取user-service服务的实例列表,并且实现负载均衡。

不过这些动作不用我们去做,只需要添加一些注解即可。

在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解:

image-20210713224049419

修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径,用服务名代替ip、端口:

image-20210713224245731

spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。

总结

搭建EurekaServer

  1. 引入eureka-server依赖
  2. 添加@EnableEurekaServer注解
  3. 在application.yml中配置eureka地址

服务注册

  1. 引入eureka-client依赖
  2. 在application.yml中配置eureka地址

服务发现

  1. 引入eureka-client依赖
  2. 在application.yml中配置eureka地址
  3. 给RestTemplate添加@LoadBalanced注解
  4. 用服务提供者的服务名称远程调用

Ribbon负载均衡

上一节中,我们添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?

负载均衡原理

SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。

image-20210713224517686

那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢?

总结

SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:

image-20210713224724673

基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1
  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

负载均衡策略

负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:

image-20210713225653000

不同规则的含义如下:

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

默认的实现就是ZoneAvoidanceRule,是一种轮询方案

自定义负载均衡策略

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

  1. 代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:
  • 注意:这种方式是全局配置
1
2
3
4
@Bean
public IRule randomRule(){
return new RandomRule();
}
  1. 配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
  • 注意:此方式可单独为不同的微服务配置
1
2
3
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

注意,一般用默认的负载均衡规则,不做修改。

饥饿加载

  • Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。

  • 饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
5
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #指定饥饿加载的服务名称
- user-service

Nacos注册中心

国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。

认识和安装Nacos

Windows安装

下载安装包

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

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

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

如图:

image-20210402161102887

解压

将这个包解压到任意非中文目录下,如图:

image-20210402161843337

目录说明:

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

端口配置

Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。

如果无法关闭占用8848端口的进程,也可以进入 nacosconf 目录,修改配置文件中的端口:

修改其中的内容:

image-20210402162251093

启动

启动非常简单,进入bin目录,结构如下:

然后执行命令即可:

  • windows命令:

    1
    startup.cmd -m standalone

执行后的效果如图:

image-20210402162526774

访问

image-20210402162709515

Linux安装

Linux或者Mac安装方式与Windows类似。

安装JDK

  1. Nacos依赖于JDK运行,索引Linux上也需要安装JDK才行。

  2. 上传jdk安装包:

  3. 上传到某个目录,例如:/usr/local/

  4. 然后解压缩:

1
tar -xvf jdk-8u144-linux-x64.tar.gz
  1. 然后重命名为java

  2. 配置环境变量:

1
2
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin
  1. 设置环境变量:
1
source /etc/profile

上传安装包

如图:

image-20210402161102887

上传到Linux服务器的某个目录,例如/usr/local/src目录下:

解压

命令解压缩安装包:

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

然后删除安装包:

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

目录中最终样式:

image-20210402163858429

目录内部:

image-20210402164414827

端口配置

与windows中类似

启动

在nacos/bin目录中,输入命令启动Nacos:

1
sh startup.sh -m standalone

Nacos的依赖

父工程:

1
2
3
4
5
6
7
<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>

客户端:

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

服务注册到nacos

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。

主要差异在于:

  • 依赖不同
  • 服务地址不同

引入依赖

在cloud-demo父工程的pom文件中的<dependencyManagement>中引入SpringCloudAlibaba的依赖:

1
2
3
4
5
6
7
<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>

然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

注意:不要忘了注释掉eureka的依赖。

配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:8848

注意:不要忘了注释掉eureka的地址

重启

重启微服务后,登录nacos管理页面,可以看到微服务信息:

image-20210713231439607

服务分级存储模型

一个服务可以有多个实例,例如我们的user-service,可以有:

  • 127.0.0.1:8081
  • 127.0.0.1:8082
  • 127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例 划分为一个集群

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

image-20210713232522531

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

image-20210713232658928

杭州机房内的order-service应该优先访问同机房的user-service。

给user-service配置集群

  1. 修改user-service的application.yml文件,添加集群配置:
1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
  1. 重启两个user-service实例后,我们可以在nacos控制台看到下面结果:

image-20210713232916215

  1. 我们再次复制一个user-service启动配置,添加属性:
1
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
  1. 启动UserApplication3后再次查看nacos控制台:

image-20210713233727923

同集群优先的负载均衡

默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。

因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。随机负载均衡

  1. 给order-service配置集群信息

修改order-service的application.yml文件,添加集群配置:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
  1. 修改负载均衡规则

修改order-service的application.yml文件,修改负载均衡规则:

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

权重配置

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

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

  • 但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。

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

  • 在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:

image-20210713235133225

在弹出的编辑窗口,修改权重:(0-1)之间

image-20210713235235219

注意:如果权重修改为0,则该实例永远不会被访问

环境隔离

Nacos提供了namespace来实现环境隔离功能。

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

image-20210714000101516

创建namespace

默认情况下,所有service、data、group都在同一个namespace,名为public:

image-20210714000414781

我们可以点击页面新增按钮,添加一个namespace:

image-20210714000440143

然后,填写表单:

image-20210714000505928

就能在页面看到一个新的namespace:

image-20210714000522913

给微服务配置namespace

给微服务配置namespace只能通过修改配置来实现。

例如,修改order-service的application.yml文件:

1
2
3
4
5
6
7
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID

重启order-service后,访问控制台,可以看到下面的结果:

image-20210714000830703

image-20210714000837140

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

image-20210714000941256

总结

Nacos环境隔离

  1. namespace 用来做环境隔离
  2. 每个namespace都有唯一id
  3. 不同namespace下的服务不可见

Nacos与Eureka的区别

Nacos的服务实例分为两种l类型:

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。

  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例:

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

Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:

image-20210714001728017

  • Nacos与eureka的共同点

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

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

Nacos配置管理

Nacos除了可以做注册中心,同样可以做配置管理来使用。

统一配置管理

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

image-20210714164426792

  • Nacos一方面可以将配置集中管理,

  • 另一方可以在配置变更时,

  • 及时通知微服务,

  • 实现配置的热更新。

image-20230307210030013

在nacos中添加配置文件(在public下配置)

如何在nacos中管理配置呢?

image-20210714164742924

然后在弹出的表单中,填写配置信息:

image-20210714164856664

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

从微服务拉取配置

微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。

但如果尚未读取application.yml,又如何得知nacos地址呢?

因此spring引入了一种新的配置文件:bootstrap.yaml 文件,会在 application.yml 之前被读取,流程如下:

img

  1. 引入nacos-config依赖

首先,在user-service服务中,引入nacos-config的客户端依赖:

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

然后,在user-service中添加一个bootstrap.yaml文件(优先级高于application.yml),内容如下:

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost: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

image-20210714170845901

  1. 读取nacos配置

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

image-20210714170337448

完整代码:

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
package cn.itcast.user.web;

import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

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

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

在页面访问,可以看到效果:

image-20210714170449612

配置热更新

我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新

要实现配置热更新,可以使用两种方式:

方式一

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

image-20210714171036335

方式二

使用 @ConfigurationProperties 注解代替@Value注解。

在user-service服务中,添加一个类,读取patterrn.dateformat属性:

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

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}

在UserController中使用这个类代替@Value:

image-20210714171316124

完整代码:

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
package cn.itcast.user.web;

import cn.itcast.user.config.PatternProperties;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
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;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

@Autowired
private PatternProperties patternProperties;

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

// 略
}

配置共享

其实微服务启动时,会去nacos读取多个配置文件,例如:

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

  • [spring.application.name].yaml,例如:userservice.yaml

[spring.application.name].yaml不包含环境,因此可以被多个环境共享。

下面我们通过案例来测试配置共享

添加一个环境共享配置

我们在nacos中添加一个userservice.yaml文件:

image-20210714173233650

在user-service中读取共享配置

在user-service服务中,修改PatternProperties类,读取新添加的属性:

image-20210714173324231

在user-service服务中,修改UserController,添加一个方法:

image-20210714173721309

运行两个UserApplication,使用不同的profile

修改UserApplication2这个启动项,改变其profile值:

image-20210714173538538

image-20210714173519963

这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。

启动UserApplication和UserApplication2

image-20210714174313344

image-20210714174424818

可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。

配置共享的优先级

当nacos、服务本地同时出现相同属性时,优先级有高低之分:

image-20210714174623557

Nacos集群搭建

集群结构图

官方给出的Nacos集群图:

image-20210409210621117

其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。

我们计划的集群结构:

image-20210409211355037

三个nacos节点的地址:

节点 ip port
nacos1 192.168.150.1 8845
nacos2 192.168.150.1 8846
nacos3 192.168.150.1 8847

搭建集群

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载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

nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。

本例中才用1.4.1版本:

image-20210409212119411

配置Nacos

将这个包解压到任意非中文目录下,如图:

image-20210402161843337

目录说明:

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

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

image-20210409212459292

然后添加内容:

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=123

启动

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

image-20210409213335538

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

nacos1:

1
server.port=8845

nacos2:

1
server.port=8846

nacos3:

1
server.port=8847

然后分别启动三个nacos节点:

1
startup.cmd

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;
}
}
1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址

优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.
  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

总结

集群搭建步骤:

  1. 搭建MySql集群并初始化数据库表
  2. 下载解压nacos
  3. 修改集群配置(节点信息)、数据库配置
  4. 分别启动多个nacos节点
  5. nginx反向代理

Feign远程调用

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

image-20210714174814204

存在下面的问题:

  • 代码可读性差,编程体验不统一

  • 参数复杂URL难以维护

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

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

Feign替代RestTemplate

Fegin的使用步骤如下:

引入依赖

我们在order-service服务的pom文件中引入feign的依赖:

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

添加注解

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

image-20210714175102524

编写Feign的客户端

在order-service中新建一个接口,内容如下:

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

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

修改配置

  1. 注释掉 namespace,无需集群
  2. 修改nacos端口为 8848

测试

修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

image-20210714175415087

是不是看起来优雅多了。

总结

使用Feign的步骤:

① 引入依赖

② 添加@EnableFeignClients注解

③ 编写FeignClient接口

④ 使用FeignClient中定义的方法代替RestTemplate

自定义配置

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的日志级别可以针对单个服务:

1
2
3
4
5
feign:  
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别

也可以针对所有服务:

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

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Java代码方式

也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

1
2
3
4
5
6
public class DefaultFeignConfiguration  {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

如果是局部生效,则把它放到对应的@FeignClient这个注解中:

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

Feign使用优化

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

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

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

这里我们用Apache的HttpClient来演示。

  1. 引入依赖

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

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
  1. 配置连接池

在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 # 每个路径的最大连接数

接下来,在FeignClientFactoryBean中的loadBalance方法中打断点:

image-20210714185925910

Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:

image-20210714190041542

总结,Feign的优化:

1.日志级别尽量用basic

2.使用HttpClient或OKHttp代替URLConnection

① 引入feign-httpClient依赖

② 配置文件开启httpClient功能,设置连接池参数

最佳实践

所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。

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

feign客户端:

image-20210714190542730

UserController:

image-20210714190528450

有没有一种办法简化这种重复的代码编写呢?

继承方式

一样的代码可以通过继承来共享:

  1. 定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

  2. Feign客户端和Controller都集成改接口

image-20210714190640857

优点:

  • 简单
  • 实现了代码共享

缺点:

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

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

抽取方式

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

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

image-20210714214041796

实现基于抽取的最佳实践

抽取

首先创建一个module,命名为feign-api:

image-20210714204557771

项目结构:

image-20210714204656214

在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项目中

image-20210714205221970

在order-service中使用feign-api

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

  2. 在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>
  1. 修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包

重启测试

重启后,发现服务报错了:

image-20210714205623048

这是因为UserClient现在在cn.itcast.feign.clients包下,

而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。

解决扫描包问题

方式一:

指定Feign应该扫描的包:

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

方式二:

指定需要加载的Client接口:

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

Gateway服务网关

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

为什么需要网关

Gateway网关是我们服务的守门神,所有微服务的统一入口。

网关的核心功能特性

  • 请求路由
  • 权限控制
  • 限流

架构图:

image-20210714210131152

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

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

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

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

  • gateway
  • zuul

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

gateway快速入门

下面,我们就演示下网关的基本路由功能。基本步骤如下:

  1. 创建SpringBoot工程gateway,引入网关依赖
  2. 编写启动类
  3. 编写基础配置和路由规则
  4. 启动网关服务进行测试

创建gateway服务,引入依赖

创建服务:

image-20210714210919458

引入依赖:

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>

编写启动类

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);
}
}

编写基础配置和路由规则

创建application.yml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

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

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

重启测试

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

image-20210714211908341

网关路由的流程图

整个访问的流程如下:

image-20210714211742956

总结:

网关搭建步骤:

  1. 创建项目,引入nacos服务发现和gateway依赖

  2. 配置application.yml,包括服务基本信息、nacos地址、路由

路由配置包括:

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

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

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

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

接下来,就重点来学习路由断言和路由过滤器的详细知识

断言工厂

  • 我们在配置文件中写的断言规则只是字符串,这些字符串会被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这种路由工程就可以了。

过滤器工厂

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

image-20210714212312871

路由过滤器的种类

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, Itcast is freaking awesome! # 添加请求头

当前过滤器写在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, Itcast is freaking awesome!

总结

过滤器的作用是什么?

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

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

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

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

实现:

在gateway中定义一个过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Order(-1)
@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. 实现GlobalFilter接口
  2. 添加@Order注解或实现Ordered接口
  3. 编写处理逻辑

全局过滤器的作用

  1. 对所有路由都生效的过滤器,并且可以自定义处理逻辑

过滤器执行顺序

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

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

image-20210714214228409

排序的规则是什么呢?

  • 每一个过滤器都必须指定一个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排序,组织过滤器链

总结(路由过滤器、defaultFilter、全局过滤器的执行顺序)

  1. order 值越小,优先级越高
  2. 当order值一样时,顺序是 defaultFilter > 路由过滤器 > GlobalFilter

跨域问题

什么是跨域问题

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

  • 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com

  • 域名相同,端口不同:localhost:8080和localhost8081

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

解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html

模拟跨域问题

找到课前资料的页面文件:

image-20210714215713563

放入tomcat或者nginx这样的web服务器中,启动并访问。

可以在浏览器控制台看到下面的错误:

image-20210714215832675

从localhost:8090访问localhost:10010,端口不同,显然是跨域的请求。

解决跨域问题

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

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:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期