本文使用的资料我都放在百度网盘中,如链接过期请留言告知作者:点此下载,提取码:ookk。
Eureka注册中心
Eureka可以解决的问题
图片中的问题都可以利用Eureka注册中心来解决
Eureka的作用
搭建EurekaServer
注册Eureka服务端
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
PS: eureka自己也是一个微服务,启动的时候会把自己也注册到eureka上,所以这里要配置eureka的地址信息(defaultZone)
新建maven项目,pom文件:
server:
port: 10086 #服务端口
spring:
application:
name: eurekaserver #eureka的服务名称
eureka:
client:
service-url: #eureka地址信息,eureka自己也是一个微服务,会把自己注册到eureka上
defaultZone: http://127.0.0.1:10086/eureka
register-with-eureka: false #设置成false表示自己就是注册中心,不用注册自己
fetch-registry: false #设置成false表示自己就是注册中心,不用去注册中心获取其他服务地址
#在eureka单机版配置中,register-with-eureka和fetch-registry 这两个选项是false,但是集群版的eureka是利用服务发现来实现的,所以需要改成true来向服务端注册自己并获取客户端信息
以上就是Eureka的服务端配置
注册Eureka客户端
客户端的配置跟服务端差不多,引用maven依赖的时候把server变成client
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
然后再yml文件中加上eureka的配置,并设置eureka的服务名称, service-url 地址就是服务端的地址信息
spring:
application:
name: eurekaclientserver #eureka的客户端名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
eureka服务端的配置中register-with-eureka和fetch-registry默认为true的情况下,可以看到这里注册了三个服务,一个eureka服务端,两个客户端:user和order。
而eureka服务端的配置中register-with-eureka和fetch-registry为false的情况下,这里注册了两个服务:user和order。
小总结:
这里还有个操作:{%label 复制一个服务 %}用于之后的配置负载均衡
例如我复制这里的:userApplication
右键复制配置:
这个地方配置下端口,否则会端口冲突:
配置好后确定然后运行
配置负载均衡(服务发现)
前置步骤中复制出了一个相同的服务,现在我们就用这两个服务实现负载均衡。
第一步
给启动类中注册的restTemplate方法加上负载均衡注解@LoadBalanced
@Bean
@LoadBalanced
public RestTemplate restTemplate()
{
return new RestTemplate();
}
第二步
修改调用这个服务的实现类接口:
将调用的url接口从localhost:8081改成userservice,用服务名调用
@Autowired
RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//String url = "http://localhost:8081/user/"+order.getUserId();
String url = "http://userservice/user/"+order.getUserId();
User user = restTemplate.getForObject(url,User.class);
order.setUser(user);
// 4.返回
return order;
}
改完之后重启order服务,然后调用接口:http://localhost:8088/order/101 和 http://localhost:8088/order/102,注意这里查询的ID不同(101,102),说明调用了两次user服务。
查看刚才复制的userApplication服务:
这两个服务各自调用了一次,说明负载均衡配置成功
Eureka总结
Ribbon负载均衡
Ribbon实现原理
这个地方的请求地址不能直接拿到浏览器中直接使用
因为这不是一个真正的IP或者域名,所以不能直接访问,而Ribbon做的事情就是拦截下这个请求然后去找到eureka服务(eureka里面记录了所有的服务信息)从eureka中拉取服务信息,之后做一些处理找到对应的接口,如果有两个userservice的服务Ribbon就会利用轮询实现负载均衡(如下图)
请求会被@LoadBalanced注解拦截下来,然后Ribbon通过服务名称(userservice)拿到真实的IP地址列表(localhost:8081,localhost:8082),之后通过轮询规则实现负载均衡。
常见的负载均衡规则
常见的负载均衡规则
怎样修改负载均衡策略
第一种(代码方式)
这种方式修改的负载均衡策略是全局的,在order-service中调用的任何接口都是随机策略的。修改完之后需要重新打包发布。
{%label 使用代码方式配置之后会忽略配置文件的配置 %}
@Bean
public IRule randomRule(){
return new RandomRule();
}
重新运行并访问四次接口后发现:user服务的8081端口被访问了三次,user服务的8082端口被访问了一次
第二种(修改配置文件application.yml)
修改配置文件这种负载均衡配置方式只对{%label 某一单独的服务 %}起作用,修改完之后不需要重新打包发布。
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡
注释OrderApplication启动类中的代码并重新启动order-service,调用了8次接口后:user的8081执行三次,user的8082执行五次。
Ribbon的饥饿加载
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: userservice #指定对userservice这个服务进行饥饿加载
这里的clients是一个集合,要写多个的话可以用下面的方式
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients:
- userservice #指定对userservice这个服务进行饥饿加载
- XXXservice
Ribbon总结
Nacos注册中心
Nacos是阿里巴巴的产品,现在是springCloud的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度比较高。
Nacos安装指南
Windows安装
开发阶段采用单机安装即可。
下载安装包
在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:
GitHub主页:https://github.com/alibaba/nacos
GitHub的Release下载页:https://github.com/alibaba/nacos/releases
如图:
本课程采用1.4.1.版本的Nacos,课前资料已经准备了安装包:
windows版本使用nacos-server-1.4.1.zip
包即可。
解压
将这个包解压到任意非中文目录下,如图:
目录说明:
- bin:启动脚本
- conf:配置文件
端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口:
修改其中的内容:
启动
启动非常简单,进入bin目录,结构如下:
然后执行命令即可:
windows命令:
#这里的 -m 指的是模式 standalone指的是单机模式,除此之外还有集群模式 startup.cmd -m standalone
执行后的效果如图:
访问
在浏览器输入地址:http://127.0.0.1:8848/nacos即可:
默认的账号和密码都是nacos,进入后:
Linux安装
Linux或者Mac安装方式与Windows类似。
安装JDK
Nacos依赖于JDK运行,索引Linux上也需要安装JDK才行。
上传jdk安装包:
上传到某个目录,例如:/usr/local/
然后解压缩:
tar -xvf jdk-8u144-linux-x64.tar.gz
然后重命名为java
配置环境变量:
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin
设置环境变量:
source /etc/profile
上传安装包
如图:
也可以直接使用下载好的tar.gz:
上传到Linux服务器的某个目录,例如/usr/local/src
目录下:
解压
命令解压缩安装包:
tar -xvf nacos-server-1.4.1.tar.gz
然后删除安装包:
rm -rf nacos-server-1.4.1.tar.gz
目录中最终样式:
目录内部:
端口配置
与windows中类似
启动
在nacos/bin目录中,输入命令启动Nacos:
sh startup.sh -m standalone
启动时出现的异常
在运行“startup.cmd”文件时出现 db.num is null 异常。
1.修改 Nacos 下的 conf/application.properties 文件,注意需要修改的文件是application.properties,不是application.properties.example。
2.新建本地数据库 Nacos,右击选择“运行SQL文件”,执行 conf/nacos-mysql.sql 文件
3、在startup.cmd文件中修改配置,把set MODE = "cluster" 修改为“standalone”(默认集群启动,修改为单机启动)
再访问 http://127.0.0.1:8848/nacos/ 即可成功!
Nacos的使用
引入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>
客户端依赖:
<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
先把eureka的依赖包注释掉,然后引入nacos客户端的依赖包,这里分别为我的两个模块(order和user)引入
服务注册到Nacos
修改user-service&order-service中的application.yml文件,记得将eureka地址注释掉,然后添加nacos地址,如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice #order的服务名称
cloud:
nacos:
server-addr: localhost:8848 #nacos服务地址
Nacos分级储存模型
在之前,我们就有了服务的概念,这个服务下有多个实例,这就是简单的两层概念:
随着业务规模越来越大,我们就不能吧所有的实例都部署在一个机房里,这就像所有的鸡蛋不能放在同一个篮子里一样,篮子翻了鸡蛋就没了,而机房出故障了服务就停了,所以我们要把这些实例放在不同的机房里,这样一个机房故障还有好几个机房可以用,这就叫做容灾,一个机房中的多个实例被称为集群,在nacos的服务分级中,一级是服务,二级是集群,三级是实例。
服务跨集群调用问题
Nacos引入集群概念就是为了尽可能的防止跨集群访问,避免服务出现跨集群调用。
配置集群属性
我们点开服务的控制台可以看到现在的集群是默认的,接下来就要去配置集群了
为了更好的实现集群的概念,这里我又复制了一个user的实例,端口改成8083,如图:
接下来打开user服务的application.yml文件,在nacos的服务地址(server-addr)下添加:cluster-name 属性(集群名称),直接输入cluster-name会有提示。
discovery:
cluster-name: HZ #自定义名称,表示杭州
接下来运行user的前两个实例,将这两个实例放到HZ集群中
现在我们要把user的第三个实例(UserApplication(2))放到上海集群中去,只需要修改配置文件中的cluster-name:SH,然后启动实例即可。注意不要重启前两个实例,否则前两个实例的集群都会变成SH。
启动完成后查看Nacos的服务列表,这里可以看到我们有两个集群。
到这里我们就完成了nacos的服务分级存储模型
NacosRule负载均衡
在配置负载均衡之前,我们先把order服务也配置一个集群属性,我们把它放到HZ集群中去:
下面我们可以测试一下order服务调用user服务会不会优先从本集群调用,依次访问order服务,传入101,102,103调用三次。然后打开idea看下user实例输出日志。
user实例1:
user实例2:
user实例3:
可以看到三个实例各自都被调用了一遍,表示order服务发起远程调用的时候没有优先选择本集群的服务,而是采用轮询方案。想要有限调用本地集群的服务,就要配置负载均衡的策略。
现在我们如下修改配置文件的ribbon属性,注意两个user的实例权重要相同:
userservice: #userservice的服务名application.name
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡
配置好后我们重启并调用order服务,分别传入参数101,102,103,104,105,调用五次order服务之后之后点开idea看下user的三个实例在控制台的输出。
user实例1:
user实例2:
user实例3:
这里可以看到同属HZ集群的user实例1和user实例2中都各有输出日志,而user实例3是空白的,算是实现了本集群调用,这也证明了我们的NacosRule具备优先选择本地集群且随机访问的功能。
接下来我们把本集群的user实例1和user实例2停掉,看下order服务在无法调用本地集群服务的情况下会发生什么,
可以看到现在HZ集群中的user服务都被停掉了,现在调用order服务然后看下idea的控制台。
我们可以看到SH集群的user实例3被调用到了,而order实例却出现了警告,告诉我们发生跨集群调用。
根据权重负载均衡
怎样去配置权重呢?
权重一般在0~1之间,数值越小被访问的几率越低,设置完后运行效果很明显,这里就不截图演示了。权重设置为0的时候这个实例就不会被访问到,可以用于丝滑的服务升级迭代:服务升级迭代完成后可以先设置低权重,让用户访问这个升级后的服务,等用户反馈良好,业务测试通过之后再设置对等的权重,这样就可以实现用户无感知的服务升级。
环境隔离namespace
Nacos不仅是一个注册中心,他还是一个数据中心,所以nacos为了做数据和服务的管理他会有一个隔离的概念,首当其中的就是namespace,如图:
可以把namespace环境看作单独的控件,这个空间中又会有组(group),组下面就是我们的服务或者数据,服务下面就是集群,再往下就是实例,我们的环境隔离其实就是在对服务做隔离,所以不同环境的服务不能互相访问。服务划分,实例划分其实是基于业务或者地域做的划分,有的时候我们还会有开发环境,测试环境,生产环境的变化,所以我们也会基于这些环境做划分,我们可以把一些业务相关性比较高的放在同一个组里,例如:订单和支付。在设计中可以用这个,也可以不用。
现在打开nacos的控制中心,点开命名空间,可以看到这里有一个public的默认命名空间。
然后我们点开服务列表,可以看到我们的服务就在public环境下面的默认分组中。
接下来我们创建一个新的命名空间:
这时候我们点开服务列表就会发现出现了两个命名空间但是dev下面什么都没有,这是因为我们没有给dev配置任何服务。
那我们怎样给服务配置命名空间呢?只需要在cluster-name下面加上namespace就行了,namespace的值就是命名空间的ID。
namespace: e7d9b62f-c038-45b9-aadb-b3e18353cc51 #命名空间ID
这里我给order服务配置了命名空间,配置完成后重启下order服务,然后打开nacos控制中心查看dev的服务列表,如图:
现在再去调用order服务就会报错:
虽然user的三个实例都在运行,但是order的控制台依然提示没有可用的userservice实例,因为他们的环境不同,他们是两个世界的实例。
总结
Nacos注册中心原理分析
Nacos注册中心细节分析
如下图,不管是什么样的注册中心,服务提供者在启动的时候都会把服务信息提供给注册中心,注册中心会把这些信息保存下来,这就是服务注册。
如下图,在服务消费者要用的时候就会向 注册中心去定时拉取服务的信息,这可以叫服务拉取,也叫服务发现。
服务消费者不是每次都会向注册中心拉取服务信息,这样的话注册中心的压力就会很大,所以消费者在服务拉取的时候会把服务信息缓存到一个列表当中,这个列表每隔30秒就会更新一次,然后消费者再通过服务信息进行远程调用。
以上内容在Nacos和Eureka中都是相同的,不同点则在于nacos的临时实例与非临时实例
临时实例:
服务提供者定时向注册中心发送请求,告诉注册中心这个实例可用。如果不发请求的话注册中心就会移除这个临时实例,这一点跟eureka完全一致,只不过eureka发送频率慢一些。
非临时实例:
非临时实例在nacos中不会做心跳检测,而是由注册中心主动发请求去问非临时实例还在吗?而且nacos不会吧非临时实例剔除掉,而是标记处它不健康了。
在eureka中,消费者每隔30秒拉取一次服务信息,如果在30秒内某个服务挂掉了,那消费者还是不知道,这个时候调用就会出问题,所以eureka的更新效率不太好。所以nacos的注册中心会多一步操作:主动推送服务信息,一旦有服务挂掉了,nacos会立刻通知服务消费者变更,这样效率就比较高,服务消费者获取到的服务信息也比较准确。
实力配置方式:
ephemeral: false #是否是临时实例,默认true
当一个实例被设置成非临时实例之后,即便是服务停止,服务列表依旧会保存服务信息,如下图列表会变成红色:
临时实例则是会被剔出,不在服务列表中。
总结
Nacos配置管理
统一配置管理
当微服务的数量少的时候,想修改某些服务的配置可以直接修改,但如果有几十上百上千的服务,这样挨个修改配置再重新启动服务就非常的困难了,所以我们现在就需要把这些配置文件进行统一管理,而且修改完配置之后可以不用启动服务实现配置的热更新。想要实现这样的功能我们就需要一个配置管理服务来统一管理配置。配置管理服务跟微服务中的配置结合使用 ,当配置管理服务有改动的时候就会立刻通知服务同步修改,实现热更新。
下面我们就开始使用配置管理,操作也比较简单:
1.点开配置管理,然后点加号,这里不要选错命名空间了,不然后面的读取操作可能会出错
2.依次填入各项参数,Data ID的命名尽量以服务名-环境名.后缀全称命名,我这里用的就是:userservice-dev.yaml,配置内容就填写user和order等公共的服务需要改动和实现热更新的部分,例如某个开关,某种模板。配置完后点发布就行了。
读取统一配置
配置获取步骤:我们的项目启动之后会先读取nacos中的配置文件,然后跟本地服务的配置合并之后才会进行下一步的操作。所以我们要先知道nacos的相关地址才能先读取nacos的配置,像以前那样在application.yml文件中配置naco地址信息就行不通了。因此,spring提供了一个bootstrap.yml的配置文件,它的读取优先级比application.yml高很多,所以项目启动之后spring会先读取bootstrap.yml文件,然后再跟本地配置文件结合。我们可以把所有与nacos相关的配置都放到bootstrap.yml里面。
下面就开始配置读取步骤:
1.引入客户端依赖:
<!--nacos统一配置管理--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
2.在userservice的resources下创建配置文件bootstrap.yml,并填入以下nacos相关配置,然后把application.yml中重复的配置删除。
spring: application: name: userservice profiles: active: dev #环境 cloud: nacos: server-addr: localhost:8848 #nacos地址 config: file-extension: yaml #文件后缀名
接下来就测试下我们有没有读取到nacos配置中心里的文件:
我们可以在userController中添加以下代码测试:
@Value("${pattern.format}")
private String format;
@GetMapping("now")
public String now() {
System.out.println(format);
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(format));
}
出错的话可以检查自己的命名空间是不是对的上,或者nacos配置中的格式是否正确。
总结
配置自动更新
在使用自动更新配置前,我们先把nacos中的配置文件修改一下,把yy-MM-dd HH:mm:ss改成yy年MM月dd日 HH:mm:ss
现在修改完成之后,调用服务并不会自动更新,我们还需要配置一些东西:
方式一,注解
@RefreshScope
添加完成之后重启服务可以看到配置生效了
当然这也可能因为我们重启服务的缘故,所以我们现在再把nacos配置改回去:
重新调用我们的服务,出现这样的情况就说明我们的配置热更新成功了:
idea控制台也会输出日志告知我们配置文件被修改了
方式二,配置文件
我们在user服务中新建config包和配置类PatternProperties:
- @Data 为属性提供get,set方法
- @Component 将类注册为Bean供其它类使用
- @ConfigurationProperties(prefix = "xxxxx") 指定前缀,可以直接为前缀下的同名属性注入值
然后我们去userController中,将@RefreshScope注解和@Value等代码注释掉,然后定义PatternProperties类和Bean注入
@Autowired private PatternProperties patternProperties;
具体的效果这里就不展示了,步骤正确的话是可以实现配置的自动刷新:
所以这里推荐大家使用配置文件的方式实现配置的自动刷新
总结
多环境配置共享
如果有一个配置属性,在开发,测试等环境下是一样的,这样的配置就在每个配置文件中都写上,想要修改的时候也不方便,这显然就不合适了,所以就需要有一个地方可以统一管理这个配置,这就是一个多环境配置共享的需求。如下如,微服务启动的时候会从nacos读取多个配置文件,但无论profile(userservice-dev.yaml中的dev)怎么变化,userservice.yaml一定会加载(我的理解是这类配置文件没有指定环境,所以可以被所有的环境共用),因此多环境共享的配置可以写入到这个文件中去。
现在我们来动手配置一下多环境配置共享:
打开nacos控制台,新增配置文件,文件名就叫userservice.yaml,如下图
然后我们打开idea,修改PatternProperties类,将envShareValue属性加上去,并多谢两个接口用来测试
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String format;
private String envSharedValue;
}
@GetMapping("/env")
public String env() {
System.out.println(patternProperties.getEnvSharedValue());
return patternProperties.getEnvSharedValue();
}
@GetMapping("/prop")
public PatternProperties prop() {
return patternProperties;
}
代码写完之后就可以重启服务了,我们再弄一个test环境用来测试我们的共享配置是否生效,按照以下方法配置后重启这个服务。
重启后依次访问8081和8082的prop接口,出现以下效果
8081:
8082:
可以看到8082相比于8081,format属性是null,这说明我们的多环境共享配置就成功了。
这里还有个问题,当这三种环境(nacos共享环境,nacos指定环境,本地环境)有一个共同的属性时,谁的优先级更高?经过测试我发现,线上环境配置 > 本地环境配置,在两种线上环境配置中,指定环境(带环境名的) > 共享环境。
总结
Nacos集群搭建
为了实现程序的高可用,在企业的开发中nacos要部署成集群状态,如下图:
假设这里有3个nacos节点,要实现程序的高可用,我们第一个要实现的就是数据共享,因为不管有几个节点,数据不共享,大家各干各的最后就出问题了,所以我们一般会搭建一个MySQL集群,让多个nacos都来访问和操作这个MySQL集群实现数据共享,我们可以用Nginx来做请求的负载均衡。接下来我们就开始搭建一个nacos集群:
在本地搭建3个nacos节点,因为是本地搭建所以IP相同,只是端口不同。
下载Nacos
这里从网上下载并解压就好了
初始化数据库
搭建MySQL集群,这里先用单个的数据库做实例,把nacos安装包中的sql语句放到数据库中执行以下就可以初始化数据库:
配置Nacos集群
把下载下来的nacos解压后配置,配置完成后将整个nacos文件夹复制成三份
打开cluster.conf文件,添加代码:
这里是本地的测试环境,正式场景应该是三台不同服务器的IP和端口
127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847
然后就去配置MySQL的信息,打开conf下的application.properties文件,然后修改MySQL的连接配置:
启动Nacos集群
修改完成之后就可以依次启动了,注意存放nacos集群的文件夹不要有中文路径,否则可能会报错
Nginx反向代理
打开nginx的配置文件夹,并打开nginx.conf文件
#粘贴到http内部就行
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;
}
}
如果启动不了,出现报错或者404的话,可以去查一下自己的80端口是否被占用,我这里就是被占用了所以自己改了端口。如果配置出错也可以使用这一套代码(下图),把localhost换成127.0.0.1,然后把下面的server中的监听端口和localhost也换成对应nacos的,这样我们访问8888端口的时候nginx就可以随机请求到8845,8846,8847三个nacos中的一个了:
配置完成后双击nginx.exe即可启动:
然后通过127.0.0.1:8888/nacos就可以访问了(因为浏览器默认是80端口,我这里改成了8888所以要带端口),访问成功!
注意:localhost如果启动出错可以试试 127.0.0.1,至于什么原理我也没搞清楚,这是我尝试出来的解决方案。
在代码中配置
代码中配置也非常简单,只需要将bootstrap.yml文件中的对应nacos的ip和端口修改成被Nginx代理的就可以了,这里我还是修改的user服务的bootstrap.yml文件,修改完成后启动user服务的两个实例。
spring:
application:
name: userservice
profiles:
active: dev #后缀
cloud:
nacos:
server-addr: 127.0.0.1:8888 #nacos地址
config:
file-extension: yaml #文件后缀名
打开服务列表发现已经配置并启动成功了:
之后我们打开配置管理,新建一个userservice.yaml配置文件,保存完之后查看nacos数据库的config_info表中有无数据,如果有我们刚才建的那个配置文件的数据就表示我们的数据已经存到MySQL中,完成持久化了。
到这nacos集群就搭建完成了。
总结
Http客户端Feign
在说Feign之前,我们先回顾下以前用到的RestTemplate发送请求,以及RestTemplate方式调用存在的问题。
RestTemplate是通过url指明要访问的服务名称,请求路径以及参数信息再由RestTemplate调用方法发送请求指定返回值类型来获取对应数据,这种方式已经是再ribbon的基础上优化过的,但还是存在一些问题。
- 代码的可读性差,编程体验不统一。这段代码是一个http请求的代码,有http请求路径,又有复杂的请求方式,对于一个从来没接触过但要远程调用的人来说体验非常不好。
- 参数复杂的Url难以维护,参数多了,或者参数有变化的情况下就非常难维护。
所以我们要学习一种更加优雅的http客户端:Feign
Feign的介绍
Feign是一种声明式的Http客户端,官方地址:https://github.com/OpenFeign/feign。其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。这就像在spring boot没出现的时候,我们每次使用事务都要手动开启关闭,非常麻烦,而有了springboot之后只需要在配置文件中声明一下就可以直接调用了,优雅而便捷。
基于Feign的远程调用
定义和使用Feign客户端
使用Feign的步骤如下:
引入依赖:
注意要在服务消费者的pom配置文件中添加依赖,例如我这里是order服务调用user服务,所以我就在order的pom文件中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加启用注解:
在服务消费者的启动类上加上 @EnableFeignClients 注解开启Feign功能
编写Feign客户端:
首先我们新建一个客户端的接口(UserClient),然后加上注解@FeignClient("userservice")表示这个接口用来存放userservice服务相关的接口,然后照着Controller层的写法去写就行了。
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
然后将我们之前在orderService中restTemplate调用user服务的代码注释掉,如图:
接着改造代码,如下图:
//用bean注入的方式声明和调用userClient接口
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
User user = userClient.findById(order.getUserId());
order.setUser(user);
// 4.返回
return order;
}
做完这些我们就可以去访问order接口看看效果了,出现数据表示我们的配置和调用成功!:
如果报错的话就去看看orderservice和userservice是不是在同一个环境下,下图是正确运行后的效果:
我这里启动了了3个user服务的实例,在调用order服务的时候这三个user实例的控制台都出现了日志,这说明Feign本身就继承了ribbon的负载均衡,并自动为我们实现了负载均衡的功能。
代码到这里就写完了,对比之前我们用restTemplate调用接口的方式来说简单,便捷,优雅了很多,Feign针不戳。
总结:
Feign自定义配置
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
feign.Logger.Level
- NONE:没有任何日志,默认就是这样
- BASIC:当你发起依次http请求的时候,记录什么时候发的,耗时多久这些基本信息
- HEADERS:除了记录基本信息之外,还会记录请求头信息
- FULL:记录所有信息
feign.codec.Decoder
当feign发起一次远程调用的时候,比如拿到一个用户,其实拿到的是一个json,最终我们要转成json对象,就可以通过这个Decoder去转
feign.codec.Ecoder
发请求的时候,我们传递的参数可以是各种格式,我们真正去发请求的时候我们要把这些格式转换成请求体,这个动作就是Ecoder去完成的
feign.Contract
注解,也可以说是一种契约,默认的就是SpringMVC注解,一般不动它
feign.Retryer
失败重试机制,默认是没有重试机制的,feign底层是依赖Ribbon的,Ribbon是有重试机制的,所以会使用Ribbon的重试,比如我发一次请求去查用户,第一次请求到了8081,但是8081可能因为网络或什么导致查询一直查不到,等超时之后Ribbon就会去请求8082,一直尝试直到拿到结果为止。这就是重试机制。
方式一:配置文件
全局生效(不指定服务名称,用default)
feign: client: config: default: #这里用default就是全局配置,如果是写服务名就是针对某个微服务的配置 loggerLevel: FULL #日志级别
局部生效(指定服务名称)
feign: client: config: userservice: #这里用default就是全局配置,如果是写服务名就是针对某个微服务的配置 loggerLevel: FULL #日志级别
下面就是配置了FULL日志级别之后控制台输出的日志,非常的详细:
方式二:代码配置
我们先创建一个Feign的配置文件 DefaultFeignCondiguration,如下图:
package cn.itcast.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignCondiguration
{
@Bean
public Logger.Level logLevel() {
return Logger.Level.BASIC;
}
}
如果是在服务消费者(order)的Feign的客户端(userClient)指定configuration属性为DefaultFeignCondiguration,那么这个配置就只会在当前这个服务生效
如果在服务消费者的启动类中指定DefaultFeignCondiguration配置文件,就会全局生效
我们这里配置了BASIC级别,发送一次请求过后,控制台输出以下信息,相比FULL少了很多:
一般来说在调试的时候用FULL输出日志,在生产环境中用BASIC或者NONE。
总结:
Feign的性能优化
连接池可以减少连接创建和销毁,所以可以提升性能。减少日志输出也可以提升性能。
操作步骤如下:
1.引入依赖:
在服务消费者的pom文件中引入,我这里的服务消费者是orderservice
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2.配置httpclient:
在application.yml中配置
feign:
httpclient:
enabled: true #支持 httpclient 的开关
max-connections: 200 #最大连接数
max-connections-per-route: 50 #单个请求路径的最大连接数
一般要根据实际的业务需求或者压力测试对参数进行配置
总结:
Feign的最佳实践
继承(紧耦合)
继承,给消费者的FeignClient和提供者的controller定义统一的父接口作为标准,实现一种面向契约的编程思想。
spring官方给的说明:一般情况下不推介使用共享接口,因为这会造成紧耦合,而且这种继承方案对spring mvc不起作用,方法参数是无法继承的。
这种使用方式虽然有弊端,但是也有好处,所以一般企业中使用的也挺多。
抽取(低耦合)
抽取,将FeignClient抽取为独立模块,并且把接口相关的POJO,默认的Feign配置都放到这个模块中,提供给所有消费者使用
第一步
新建模块
引入依赖
第二步
新建包并将原先在服务消费者中创建好的feign客户端(userClient)还有feign的配置类(DefaultFeignConfiguration)以及服务提供者(User)的实体类复制到feign模块中。
第三步
复制完成之后就可以把order-service中的类删掉了,删除之后order服务会报错,先在order-service(服务消费者)的pom文件中引入feign-api模块的依赖,然后把报错的地方逐个引入即可。
第四步
把报错的import改成feign-api的路径就行
报错
因为我们把userClient放到feign中了,spring默认扫描不到这个包,所以会报错
解决方案
第一种方式是把所有的客户端都引入进来,所以推荐使用第二种,使用哪个就引入哪个。
总结
GateWay统一网关
为什么需要网关
这是我们之前的服务结构,我们有很多个不同的微服务,每个微服务都需要去访问数据库来实现自己的业务。这些微服务都可以到nacos中完成服务的注册,配置的管理,当微服务内部有调用关系的时候就可以通过Feign去调用,而外部的请求直接调用就可以了。
这个时候就出现了一个问题:现在的业务都是对外公开的,而很多敏感的接口和数据不能被外部人员访问,我们要对用户的身份进行验证,所以我们使用gateway来做这个身份的认证和校验。
在身份认证通过之后,用户的请求就放行到微服务中去,这个时候问题又来了,比如要做一个用户的查询功能,而网关不能处理业务相关的功能,所以要把请求转发到对应的用户查询服务中去,而网关需要判断这个请求是用来干什么的(例如查询用户或者查询订单),如果搞错了就会出问题,这个动作我们称之为服务路由,而一个服务可能有多个实例,所以还要有一个负载均衡,这些都是gateway网关的功能。除此之外网关还有请求限流的功能,比如一个游乐场最大游客数量只有200个,而现在有300个人,这个时候门口的保安或者检票处就会限制多出来的游客进入,等出来一部分人了再允许其他人进去。
网关的作用非常重要,是对整个微服务的一种保护。
Gateway是比较新的,而zuul是最早的网管实现方式,我们一般使用Gateway,性能更好,吞吐能力更强。
总结
搭建网关服务
搭建网关的步骤:
第一步
创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖。
创建gateway启动类
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);
}
}
第二步
编写路由配置及nacos地址
id: 路由的id,自定义,只要保持id唯一就行。
uri: 路由的地址,划红线的是url其中一种写法通过具体的ip和端口进行路由,这样就是直接写死,请求只往这个地址发,所以无法使用负载均衡,不推荐使用这种方式。我们一般使用划黑线的部分,以lb://服务名这种方式进行配置。
predicates: 断言,判断请求的路径是否符合路由规则的条件
下面就是实际配置的代码
先再resources文件夹下创建application.yml的配置文件:
然后添加对应的配置代码:
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/**
最后直接启动
启动成功后我们访问对应服务,我这里的访问路径是:http://localhost:10010/user/1 最后的数字就是user的id,我这里一共启动了userservice服务的三个实例,请求了4次,每个user实例的控制台都有输出日志,证明网关配置成功并且实现了负载均衡。再发送order服务的请求也是一样的:http://localhost:10010/order/101 控制台也可以输出日志。
以下就是整个网关运行的流程:
总结
路由断言工厂
下面我们就对After这个路由工厂做个演示:
直接在配置文件中添加这句代码(如果路由不符合这两个条件的话就会直接跳过,浏览器会出现404错误):
配置完成后重新启动Gateway服务,然后对order发送请求:http://localhost:10010/order/101,因为当前时间是2022年,而路由匹配的时间是2031-04之后,所以浏览器会出现404错误,这就说明After路由配置成功了:
这11种路由不需要全都记住,眼熟就行,用到哪个就去官网查一下用法。
路由过滤器GatewayFilter
GatewayFilter是网关种提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
问:我们现在有很多的微服务,用户想要访问这些微服务不能直接访问,必须通过网关,那么问题来了,当用户向网关发起请求的时候到底经历了什么?
答:请求进入网关一定要先做路由,我们会有个断言工厂,它会基于我们配置的规则,完成请求并判断请求应该到哪个微服务
问:路由之后是不是就可以立即向微服务发起请求了呢?
答:不是,因为在网关里面我们还可以给路由配置各种各样的过滤器,这些过滤器会形成一个过滤器链,请求一定要经过这些过滤器链才会进入到微服务中。在这个过程当中过滤器就可以对请求参数做处理,例如请求头,请求参数。
除此之外,服务返回的结果也会先经过过滤器来逐层处理响应结果,最终才会返回给用户。在这个过程中可以把响应头拿出来改一改,把响应体改一改,这就是过滤器的功能。
过滤器工厂GatewayFilterFactory
具体使用哪一个还要看实际的业务需求,不需要全都记住,眼熟就行。
接下来在demo里配置一下,为所有user请求添加一个请求头:
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开头,如果是则符合规则
filters:
- AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
注意:这里的Truth,Itcast is.....中的 “,” 是 “=” 号的意思
然后改造下userservice服务UserController中的方法,打印请求头信息:
改造完成后重启GatewayApplication和userservice服务,然后通过Gateway访问user的接口:http://localhost:10010/user/1,如果控制台输出以下数据就说明配置成功了。
全局过滤器default-filters
如果需要为所有的路由请求都添加一个共同的过滤器,那就添加一个默认过滤器:default-filters,这个过滤器跟routes是同级的。
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开头,如果是则符合规则
# filters:
# - AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
default-filters: #默认过滤器 对所有请求都生效
- AddRequestHeader=Truth,Itcast is freaking awesome! #添加请求头
配置完成后重启GatewayApplication服务,然后通过Gateway访问user的接口:http://localhost:10010/user/1,控制台输出对应的日志就说明配置成功了。
总结
全局过滤器GlobalFilter
GlobalFilter接口
filter方法中的两个参数:
exchange:表示请求上下文,这个上下文指的是从进入网关开始,一直到结束为止整个流程中都可以共享exchange对象,里面可以获取Request,Response等信息,还可以向这个对象中取东西,存东西。
chain:表示过滤器链,这个链条上除了自己本身的过滤器外还有其他的过滤器,它的作用就是放行,我调用这个过滤器链让它往后走表示我这里的逻辑处理完了,之后给别人处理。
现在我们来实现这样一个需求:
实现接口
第一步
在gateway服务中新建过滤器类起名为:AuthorizeFilter,并且实现接口 GlobalFilter
第二步
改造方法体:
//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
//2.获取参数中的authorization参数
String auth = queryParams.getFirst("authorization");
//3.判断参数值是否等于 admin
if ("admin".equals(auth)) {
//4.如果等于,则放行
return chain.filter(exchange);
}
//5.如果不等于,则拒绝
//5.1.设置状态码 401未登录
exchange.getResponse().setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
//5.2.拦截请求
return exchange.getResponse().setComplete();
第5步中设置状态码是为了让用户体验更友好,如果直接拦截请求的话不会返回提示给用户。
第三步
为了让这个过滤器生效,我们要为这个类加上两个注解:
@Component:让这个过滤器类注入到spring容器中作为一个bean,使这个过滤器生效。
@Order:这是一个顺序注解,数值越小,执行的优先级最高。除了这个注解之外还可以实现Ordered接口来设置执行顺序
第四步
重启Gateway服务,再次访问user服务的请求:http://localhost:10010/user/1,发现无法访问。
然后我们添加参数authorization: http://localhost:10010/user/1?authorization=admin,再次访问,成功!
到这里我们就实现了一个简单的网关过滤器。
总结
过滤器执行顺序
路由过滤器和DefaultFilter过滤器这两个过滤器本质上都是同一种过滤器(GatewayFilter),只是作用的范围不一样,而GlobalFilter过滤器跟GatewayFilter是两种不同的过滤器,所以只能通过GlobalFilterAdapter适配器将GlobalFilter转换成GatewayFilter,然后再添加到同一个过滤器链中排序。
路由过滤器,DefaultFilter,GlobalFilter这三种过滤器的各自顺序是独立的,所以这三种过滤器的order值出现相同的时候就会按照defaultFilter > 路由过滤器 >GlobalFilter的顺序执行。
总结
跨域问题处理
为什么会出现跨域问题
跨域问题一般发生在服务端和客户端之间,解决方案就是CORS,所以服务器一般要配置很多信息来允许跨域。
在网关中处理跨域
- add-to-simple-url-handler-mapping: true :这是Gateway网关中独有的,因为弯管处理跨域采用的也是CORS方案,由浏览器向服务器发送options请求来询问是否允许跨域,而一般的options请求会被默认拦截,通过这个地方的配置可以解决options请求被拦截问题。
- allowedOrigins:允许哪些网站的跨域请求
- allowedMethods:允许的跨越与ajax请求方式,例如:GET,POST等等
- allowedHeaders:允许在请求中携带的头信息
- allowCredentials:true:是否允许携带cookie
- maxAge:每次发送ajax请求的时候浏览器都会询问服务器是否允许跨域请求,所以跨域的CORS解决方案对服务器的性能有损耗,为了减少这种损耗我们可以给跨域请求设置有效期,有效期内浏览器不再询问,直接放行。
corsConfigurations下的 ’[/**]‘ 意为拦截一切请求,都做跨域处理。
globalcors:
add-to-simple-url-handler-mapping: true #全局的跨域处理
cors-configurations:
'[/**]':
allowed-origins: #允许跨域的域名
- 'localhost:8080'
- 'www.handsomejg.com'
allowed-methods: #允许跨域的方法
- 'GET'
- 'POST'
- 'PUT'
- 'DELETE'
- 'OPTIONS'
allowed-headers: #允许跨域的请求头
- '*'
allow-credentials: true #是否允许跨域携带cookie
maxAge: 360000 #跨域的有效时间(秒)
本文的完整demo:点此下载,提取码:isok
以上就是Spring Cloud Alibaba最常用的一些知识,至此本文结束,后续还会有Spring Cloud Alibaba(二),Spring Cloud Alibaba(三)......
3 条评论
幽默外壳包裹严肃内核,寓教于乐。
zhen chang
很详细的,到时候可以直接拿来用