1.认识微服务
服务治理
分布式架构的要考虑的问题:
- 服务拆分粒度如何?
- 服务集群地址如何维护?
- 服务之间如何实现远程调用?
- 服务健康状态如何感知?
Springcloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:Spring Cloudhttps://spring.io/projects/spring-cloud
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
2.分布式服务架构案例
1. 服务拆分Demo
服务拆分(也叫项目拆分)注意事项
●不同的微服务,不要重复开发相同的业务
●要求微服务之间的数据独立,不要访问其它微服务的数据库
●微服务可以将自己的业务暴露为接口,供其它微服务调用
首先有一个已经写好的项目工程,为cloud-demo.zip,需要把这个压缩包解压并导入进idea,然后就是根据功能来拆分这个项目。这个项目的原有结构如下:
cloud-demo是父工程,里面有两个模块是order-service、user-service,这俩模块分别的作用是根据id查询订单、根据id查询用户。这俩模块就是所谓的微服务,分是订单服务和用户服务
除了把项目工程解压导入,还有两份sql文件(cloud-order.sql和cloud-user.sql)需要导入自己本地数据库(创建两个不同的database),作为数据层面的分离。订单服务只能查询cloud-order.sql表,用户服务只能查询cloud-user.sql表数据库的具体操作如下:
第一步: 在mysql创建两个数据库,为cloud_order、cloud_user# 创建两个数据库 create database if not exists cloud_order; create database if not exists cloud_user;
第二步: 分别在这两个数据库导入对应的sql文件
第三步: 下载cloud-demo.zip项目工程文件,解压到D盘的springcloud目录,并在idea打开cloud-demo项目工程
第四步: 启动cloud-demo项目工程
下面将在这个Demo进行练习,如何正确使用微服务项目
2. 服务远程调用
案例: 根据订单id查询订单功能
需求: 根据订单id查询订单的同时,把订单所属的用户信息一起返回
难点: 订单表、用户表在两个数据库,不是同一个数据库。订单业务、用户业务在两个项目,不是同一个项目
解决: 服务远程调用
目前这个Demo还没有实现服务远程调用,也就是在查订单表时,无法查到用户信息
分析:
用户项目对外暴露了一个Restful接口,如下
可以在订单项目使用Spring提供的RestTemplate工具,作用是发送http请求,也就是在订单项目向用户项目发送http请求,用户项目就会返回数据给订单项目
具体操作:
第一步: 在订单项目的OrderApplication引导类,添加如下/*** 创建RestTemplate并注入Spring容器* @return*///以后我们都要习惯使用上面这种注释,写法: /**+回车@Beanpublic RestTemplate xxrestTemplate(){return new RestTemplate();}
第二步: 在订单项目的OrderService类,添加如下
@Autowiredprivate RestTemplate kkrestTemplate;/*** 使用RestTemplate,向用户项目发起http请求,查询用户*/String xxurl = "http://localhost:8081/user/"+order.getUserId();User gguser = kkrestTemplate.getForObject(xxurl, User.class);//第一个参数是路径,第二个参数是你想要拿到什么类型的数据order.setUser(gguser);//把向用户项目拿到的数据封装到这个订单项目
第三步: 测试。重新启动用户项目和订单项目,在浏览器输入如下,查看在订单项目是否能获取到用户项目的数据http://localhost:8080/order/101
总结: 跨服务的远程调用其实就是发送一次http请求,首先是在Spring容器里面注入RestTemplate对象,然后在你发送请求的类里面自动装配这个RestTemplate对象,并且在方法里面调用这个RestTemplate对象,第一个参数是路径,第二个参数是你想拿到什么类型的数据
3.eureka注册中心实用篇-Eureka注册中心
读音: yī yōu ruī kǎ
1. 提供者与消费者
服务提供者: 一次业务中,被其它微服务调用的服务。简单来说,服务提供者就是提供接口给其它微服务
服务消费者: 一次业务中,调用其它微服务的服务。简单来说,服务消费者就是调用其它微服务提供的接口
例如上面的案例中,order-service微服务是服务提供者,user-service微服务是服务消费者
思考: 如果服务A调用服务B,服务B调用服务C,那么服务B是什么角色 ?
答案: 一个服务既可以是提供者,也可以是消费者。所以服务B相对于服务A而言,服务B是提供者。服务B相对于服务C而言,服务B是消费者
2. eureka原理分析
还是以上面的案例为例,order-service微服务和user-service微服务之间,服务调用出现的问题,如下
●order-service去向user-service发送请求,使用的是硬编码,也就是 "http://localhost:8081/user/"+order.getUserId();
●硬编码每次修改需要重新打包
●如果user-service微服务(提供者)部署成了多实例,形成集群来应对并发,那么order-service微服务(消费者)硬编码到底是写哪个实例的地址
●即使你知道这些实例的地址,那么如何挑选其中一台实例来使用呢
解决: 下面要学的Eureka注册中心
Eureka的原理:
●Eureka有两个角色,其中一个是eureka-server,叫Eureka的注册中心。作用是记录和管理所有微服务
●Eureka的另一个角色就是我们的所有微服务,也就是消费者/提供者,叫Eureka的客户端EurekaClient。
●可以把注册中心理解为Key,我们的微服务项目理解为Value,Eureka理解为字典
●当我们的微服务项目(例如order-service)启动时,会主动把自己(user-service微服务)的信息注册给注册中心
●多个微服务的话,注册中心就会有多个Value,一个Value就是一个微服务的信息,这些Value会放到一个列表里面
●当其它微服务(例如user-service)要使用某个微服务(例如order-service)时,这个微服务(例如user-service就会向注册中心去拉取对应微服务(例如order-service)的信息
●注册到注册中心的微服务会每隔30秒,向注册中心发起心跳,证明自己还在健康运行
●当微服务没有正常向注册中心发起心跳,此时注册中心就会自动在列表把这个异常的微服务剔除掉
●当注册中心有多个提供者(微服务),那么消费者是通过负载均衡算法,在注册中心的服务列表中挑选一个
●负载均衡: 简单理解就是如果有很多个微服务,且这些服务都是一个相同的请求,看谁现在工作压力小就调用哪个谁
3. 搭建eureka服务
步骤有三步:
1、搭建注册中心。搭建EurekaServer注册中心,也就是创建一个项目(在cloud-demo项目内部创建eureka-server项目),把这个项目做成注册中心
2、服务注册。将user-service(前面导入的服务拆分Demo)、order-service(前面导入的服务拆分Demo)都注册到eureka
3、服务发现。在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
这里只学第一个步骤,也就是搭建注册中心
第一步: 创建一个新的项目,作为独立的微服务,用于搭建Eureka,也就是在cloud-demo工程里面新建eureka-server微服务项目第二步: 在eureka-server微服务的pom.xml添加如下
<!--添加注册中心的依赖坐标--><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies>
第三步: 在eureka-server微服务的src/main/java目录新建cn.huanf.eureka.EurekaApplication类,写入如下
package cn.itcast.eureka;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/*** @Author:豆浆* @name :EurekeApplication* @Date:2024/3/28 11:32*/ @SpringBootApplication @EnableEurekaServer//注册中心的开关 public class EurekeApplication {public static void main(String[] args) {SpringApplication.run(EurekeApplication.class);} }
第四步: 在eureka-server微服务的src/main/resources目录新建File,名字为application.yml,写入如下
第五步: 启动eureka-server微服务。也就是运行EurekaApplication类,浏览器访问 http://localhost:8686
eureka-server微服务的Eureka在启动时,会把自己(eureka-server)也注册到Eureka。所以这就是为什么我们会在eureka-server微服务的application.yml里面写name: eurekaserver,这个其实就是自己的地址,把自己注册到Eureka,注册之后自己的名字就是EUREKASERVER
4. 服务注册
步骤有三步:
1、搭建注册中心。搭建EurekaServer注册中心
2、服务注册。将user-service(前面导入的服务拆分Demo)、order-service(前面导入的服务拆分Demo)都注册到eureka
3、服务发现。在order-service中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
这里只学第二个步骤,也就是服务注册。就是把user-server微服务注册到eureka-server微服务(这个微服务我们已经在上面 '3. 搭建eureka服务' ,做成了注册中心)里面。具体操作如下
第一步: 在user-service微服务的pom.xml添加如下<!--引入Eureka客户端依赖--> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
第二步: 在user-service微服务的application.yml添加如下
spring:# Eureka相关配置application:# user的服务名称。也就是这个user-service注册到Eureka之后,这个user-service会叫什么名字name: UserServiceeureka:client:service-url:# eureka的服务地址。如果有多个的话,逗号隔开。也就是把当前这个user-service微服务注册给哪个EurekadefaultZone: http://localhost:8686/eureka
第三步: 启动user-service。也就是运行UserApplication类,浏览器访问 http://localhost:8686,就可以看到user-service已经添加到Eureka里面
同理,给order-service微服务也注册到Eureka。具体操作跟上面一样
建议:
思考: 如何给user-service微服务启动多个实例呢,也就是启动多次,每次user-service微服务启动的端口都不相同,
如下
具体操作如下图-Dserver.port=8082
5. 服务发现
服务发现也叫服务拉取,我们需要在order-service完成服务拉取。服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡
服务发现是我们学习的重点,负载均衡不是,所以不详细介绍负载均衡,实现负载均衡只需要一个注解
首先回想一下,前面的远程调用,我们实现了在订单项目(也就是现在的order-service微服务)去查询用户项目(也就是现在的user-service微服务)的案例需求,
当时是在order-service里面使用url请求ip地址的方式,去请求user-service,从而获取user-service的用户信息。那么,学习了上面的Eureka之后,并且我们已经把order-service和user-service注册到注册中心(eureka-server)了,所以就可以在order-service里面,通过 '服务发现' 去获取user-service里面的用户信息。
具体操作也非常简单,也是使用url请求的方式,但请求的路径不是ip,而是服务名称,如下:
第一步: 在order-server微服务中的src/main/java/cn.itcast.order/service/OrderService类,修改访问的url路径,用服务名代替ip、端口。修改为如下String url = "http://UserService/user/"+order.getUserId();
第二步: 负载均衡。在order-server微服务中的OrderApplication引导类修改为如下
第三步: 重新启动在order-server微服务的OrderApplication引导类,浏览器输入http://localhost:8080/order/101,并向user-service微服务发送多次请求
4.Ribbon负载均衡原理1. 负载均衡原理
回想一下上面的 '服务发现',order-service微服务向user-service微服务发送请求,但是user-service有两个,也就是开启了两个user-service实例,且端口不同,一个是8081,另一个是8222(对应的是下图的8082),下面我们将详细学一下请求在过程中经历了什么,如下图
其中负载均衡的各种策略是在IRule接口里面,下面将会深入学习这个IRule接口
2. 负载均衡策略
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则,IRule有很多实现类,如下继承关系图每一个实现类都是一种规则,上图只是简单标注一下,下图是详细的
如何修改负载均衡策略。负载均衡策略默认是轮询,如何修改为随机呢。有两种方式如下
第一种: 代码方式(作用于全局)。在order-service中的OrderApplication类中,定义一个新的IRule。简单理解就是在项目的引导类创建一个类型为IRule的bean@Bean //bean的类型必须是IRule。bean的id是方法名,随便写 public IRule randomRule(){//实现类不一定是RandomRule,还可以是其它,如上图那些都可。RandomRule表示把负载均衡策略修改为随机return new RandomRule(); }
第二种: 配置文件方式(只作用于该服务名称)。在order-service的application.yml文件中,添加新的配置也可以修改规则
# UserService是你注册到Eureka时的服务名称。注意顶格写就行,不用写在spring:属性里面 UserService:ribbon:# 负载均衡策略。不一定是RandomRule,还可以是其它,如上图那些都可。RandomRule表示把负载均衡策略修改为随机NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果同时使用上面这两种方式,那么配置文件的优先级比代码方式低,也就是代码方式的优先级高
注意在使用的时候,很简单,就字面意思,不需要改变你其它地方的任何代码,只需要添加如上两种方式提供的代码即可
3. 饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建Ribbon的LoadBalanceClient客户端,请求时间会很长(第一次访问时间长)。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置就可以开启饥饿加载。在order-service微服务的application.yml添加如下# 注意顶格写就行,不用写在spring:属性里面 ribbon:eager-load:# 开启饥饿加载enabled: true# 指定对UserService这个服务开启饥饿加载。UserService是你注册到Eureka时的服务名称。如果有多个服务需要做饥饿加载,就-往下写clients: - UserService- UserService2
5.nacos注册中心国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:
- - 依赖不同
- - 服务地址不同
## 5.1.认识和安装Nacos
[Nacos](https://nacos.io/)是阿里巴巴的产品,现在是[SpringCloud](https://spring.io/projects/spring-cloud)中的一个组件。相比[Eureka](https://github.com/Netflix/eureka)功能更加丰富,在国内受欢迎程度较高。
# Nacos安装指南
# 1.Windows安装
开发阶段采用单机安装即可。
## 1.1.下载安装包
在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`包即可。
## 1.2.解压
将这个包解压到任意非中文目录下,如图:
目录说明:
- bin:启动脚本
- conf:配置文件
## 1.3.端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
**如果无法关闭占用8848端口的进程**,也可以进入nacos的conf目录,修改配置文件中的端口:
修改其中的内容:
## 1.4.启动
启动非常简单,进入bin目录,结构如下:
cmd
然后执行命令即可:
- windows命令:
startup.cmd -m standalone
执行后的效果如图:
## 1.5.访问
在浏览器输入地址:http://127.0.0.1:8848/nacos即可:
默认的账号和密码都是nacos,进入后:
# 2.Linux安装
Linux或者Mac安装方式与Windows类似。
## 2.1.安装JDK
Nacos依赖于JDK运行,索引Linux上也需要安装JDK才行。
上传jdk安装包:
上传到某个目录,例如:`/usr/local/`
然后解压缩:
tar -xvf jdk-8u144-linux-x64.tar.gz
然后重命名为java
mv jdk-8u144-linux-x64 java
配置环境变量:
export JAVA_HOME=/usr/local/javaexport PATH=$PATH:$JAVA_HOME/bin
设置环境变量:
source /etc/profile
## 2.2.上传安装包
也可以直接使用课前资料中的tar.gz:
上传到Linux服务器的某个目录,例如`/usr/local/src`目录下:
## 2.3.解压
命令解压缩安装包:
tar -xvf nacos-server-1.4.1.tar.gz
然后删除安装包:
rm -rf nacos-server-1.4.1.tar.gz
## 2.4.端口配置
与windows中类似
## 2.5.启动
在nacos/bin目录中,输入命令启动Nacos:
sh startup.sh -m standalone
例如:# 3.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>
spring: cloud:nacos:server-addr:localhost:8848
## 5.3.服务分级存储模型
一个**服务**可以有多个**实例**,例如我们的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是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。
当本集群内不可用时,才访问其它集群。例如:
杭州机房内的order-service应该优先访问同机房的user-service。
### 5.3.1.给user-service配置集群
1.修改user-service的application.yml文件,添加集群配置:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ # 集群名称
重启两个user-service实例后,我们可以在nacos控制台看到下面结果:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: SZ # 集群名称
启动UserApplication3后再次查看nacos控制台:
### 5.3.2.同集群优先的负载均衡
默认的`ZoneAvoidanceRule`并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个`NacosRule`的实现,可以优先从同集群中挑选实例。
(同集群间随机)
1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ # 集群名称
2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
userservice:ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
## 5.4.权重配置
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
在弹出的编辑窗口,修改权重:
> **注意**:如果权重修改为0,则该实例永远不会被访问
## 5.5.环境隔离
Nacos提供了namespace来实现环境隔离功能。
- - nacos中可以有多个namespace
- - namespace下可以有group、service等
- - 不同namespace之间相互隔离,例如不同namespace的服务互相不可见
例如,修改order-service的application.yml文件:
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
重启order-service后,访问控制台,可以看到下面的结果:
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:
## 5.6.Nacos与Eureka的区别
Nacos的服务实例分为两种l类型:
- - 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
- - 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring:cloud:nacos:discovery:ephemeral: false # 设置为非临时实例
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
- Nacos与eureka的共同点
- - 都支持服务注册和服务拉取
- - 都支持服务提供者心跳方式做健康检测
- Nacos与Eureka的区别
- - Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- - 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- - Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- - Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
# 1.实用篇-Nacos配置管理
Nacos除了可以做注册中心,同样可以做配置管理来使用。
## 1.1.统一配置管理
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
### 1.1.1.在nacos中添加配置文件
如何在nacos中管理配置呢?
在弹出的表单中,填写配置信息:
> 注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
### 1.1.2.从微服务拉取配置
微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
1)引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:
<!--nacos配置管理依赖--> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
2)添加bootstrap.yaml
然后,在user-service中添加一个bootstrap.yaml文件,内容如下:
spring:application:name: userservice # 服务名称profiles:active: dev #开发环境,这里是devcloud: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`:
3)读取nacos配置
在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:
代码:
@Value("${pattern.dateformat}")private String dateformat;@GetMapping("now")public String now(){return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}
在页面访问,可以看到效果:
## 1.2.配置热更新
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是**配置热更新**。
要实现配置热更新,可以使用两种方式:
### 1.2.1.方式一
在@Value注入的变量所在类(UserController)上添加注解@RefreshScope:
### 1.2.2.方式二
使用@ConfigurationProperties注解代替@Value注解。
在user-service服务中,添加一个类,读取patterrn.dateformat属性:
在UserController中使用这个类代替@Value:
代码:
@Autowiredprivate PatternProperties patternProperties;@GetMapping("now")public String now(){return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));}
## 1.3.配置共享
下面我们通过案例来测试配置共享
### 1)添加一个环境共享配置
我们在nacos中添加一个userservice.yaml文件:
### 2)在user-service中读取共享配置
在user-service服务中,修改PatternProperties类,读取新添加的属性:
在user-service服务中,修改UserController,添加一个方法:
### 3)运行两个UserApplication,使用不同的profile
修改UserApplication2这个启动项,改变其profile值:
这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。
启动UserApplication和UserApplication2
访问http://localhost:8081/user/prop,结果:
访问http://localhost:8082/user/prop,结果:
可以看出来,不管是dev,还是test环境,都读取到了envSharedValue这个属性的值。
### 4)配置共享的优先级
当nacos、服务本地同时出现相同属性时,优先级有高低之分:
## 1.4.搭建Nacos集群
Nacos生产环境下一定要部署为集群状态
nacos集群搭建1
上面我们一直使用的都是单点的nacos,这种方式适合测试,但是在生产环境是不使用单点nacos。原因: 生产环境强调高可用,所以nacos要做成集群
官方Nacos集群图如下。其中包含3个nacos节点,然后一个负载均衡器(例如nginx)来代理3个Nacos服务
我们将要学习的Nacos集群图如下
三个nacos节点的地址:
节点
ip
port
nacos1
127.0.0.1
8845
nacos2
127.0.0.1
8846
nacos3
127.0.0.1
8847
下面的操作是集群配置
第一步: 准备数据库数据。在你数据库执行如下语句create database if not exists nacos; use nacos;
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');
第二步: 在G盘新建nacosGroup文件夹,下载链接里的nacos-server-1.4.1压缩包,并解压到nacosGroup文件夹
第三步: 把解压后的nacos文件夹,里面的cluster.conf.example文件重命名为cluster.conf
第四步: 在cluster.conf文件添加如下
第五步: 把application.properties文件修改为如下
第六步: 把做好的nacos文件夹复制三份,分别命名为nacos1、nacos2、nacos3
第七步: 把这三个nacos文件夹的conf/application.properties文件的端口改一下,分别是8845、8846、8847
nacos集群启动之后,接下来就是安装启动nginx负载均衡器
第一步: 在G盘新建nacosNginx目录,把下载好的nginx-1.18.0压缩包解压到nacosNginx目录
第二步: 把nacosNginx目录的nginx-1.18.0/conf/nginx.conf文件,修改为如下
注意三个server地址要跟你上面做的nacos一致,作用是nginx对谁进行负载均衡,也就是对我们启动的三台nacos节点进行负载均衡
listen的作用是反向代理,简单说就是你浏览器访问nacos时,不需要输入8848端口,直接就是80端口
/nacos表示代理路径,也就是你访问/nacos路径时,实际访问的是 http://nacos-clusternacos 集群路径Nginx复制代码
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;} }
第三步: 启动nginx。在命令行输入如下。然后浏览器访问: http://localhost/nacos 。用户名和密码都是nacosPlain Text复制代码
d: cd D:\nacosNginx\nginx-1.18.0 start nginx.exe
集群搭建成功,看着好像是只访问了一台nacos,实际上已经在三台nacos之间做了一个负载均衡,也就是三台nacos都可被访问
6.Feign远程调用
http客户端Feign
6.1 Feign替代RestTemplate
先来看我们以前利用RestTemplate发起远程调用的代码:
存在下面的问题:
- •代码可读性差,编程体验不统一
- •参数复杂URL难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
Fegin的使用步骤如下:
### 1)引入依赖
我们在order-service服务的pom文件中引入feign的依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
### 2)添加注解
在order-service的启动类添加注解开启Feign的功能:
@EnableFeignClients
### 3)编写Feign的客户端
在order-service中新建一个接口,内容如下:
@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来发送了。
### 4)测试
修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:
是不是看起来优雅多了。
6.2 自定义配置
Feign可以支持很多的自定义配置,如下表所示:
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
下面以日志为例来演示如何自定义配置。
### 2.2.1.配置文件方式
基于配置文件修改feign的日志级别可以针对单个服务:
feign: client:config:userservice: # 针对某个微服务的配置loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign: client:config:default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置loggerLevel: FULL # 日志级别
而日志的级别分为四种:
- - NONE:不记录任何日志信息,这是默认值。
- - BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- - HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- - FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
### 2.2.2.Java代码方式
也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration {@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC; // 日志级别为BASIC}}
如果要**全局生效**,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是**局部生效**,则把它放到对应的@FeignClient这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
6.3 Feign使用优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
- •URLConnection:默认实现,不支持连接池
- •Apache HttpClient :支持连接池
- •OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用**连接池**代替默认的URLConnection。
这里我们用Apache的HttpClient来演示。
1)引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
<!--httpClient的依赖 --><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId>
</dependency>
2)配置连接池
在order-service的application.yml中添加配置:
feign:client:config:default: # default全局的配置loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息httpclient:enabled: true # 开启feign对HttpClient的支持max-connections: 200 # 最大的连接数max-connections-per-route: 50 # 每个路径的最大连接数
总结,Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
6.4 最佳实践
所谓最近实践,就是使用过程中总结的经验,最好的一种使用方式。
自习观察可以发现,Feign的客户端与服务提供者的controller代码非常相似
没有一种办法简化这种重复的代码编写呢?
方式一: 继承。给消费者的FeignClient和提供者的controller定义统一的父接口作为标准
特点: 紧耦合,仅适用于面向契约编程的思想上来使用
操作: 让controller和FeignClient继承同一接口
方式二: 抽取。 将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
特点; 低耦合,但是高冗余
操作: 将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用
演示上面说到的方式二"抽取"的Feign实践,实现步骤:
1、首先创建一个module,命名为feign-api,然后引入feign的starter依赖
2、就可以把order-service中的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
3、要使用怎么办,直接在order-service中引入feign-api的依赖即可
4、然后修改order-service中的所有与"UserClient、User、DefaultFeignConfiguration"有关的Import部分,改成导入feign-api中的包
具体步骤如下:
第一步: 在cloud-demo总项目中新建一个项目,项目名为feign-api
第二步: 把feign-api微服务项目的pom.xml修改为如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>cloud-demo</artifactId><groupId>cn.itcast.demo</groupId><version>1.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>feign-api</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies></project>
第三步: 在feign-api项目的src/main/java目录下新建cn.itcast.feign包,接着把order-service项目的src/main/java/cn.itcast.order/clients目录、config目录、pojo目录复制到feign-api项目的src/main/java/cn.itcast.feign目录里面。注意粘贴后的clients目录的UserClient接口会爆红,改一下import即可
第四步: 以后哪个项目需要使用Feign时,就直接在pom.xml使用刚写好的feign-api项目即可。例如把order-service项目中使用,我们可以把在order-service项目中有关Feign配置的接口和类删掉(此时order-service项目有关Feign的功能类会报错,等下会解决),然后在order-service项目的pom.xml中引入刚写好的feign-api项目依赖,接着在报错的有关Feign功能类里面重新引入一下即可解决报错
<!--引入feign-api项目(自己写的)的依赖-->
<dependency><groupId>cn.itcast.demo</groupId><artifactId>feign-api</artifactId><version>1.0</version>
</dependency>
第五步: 解决bug。当我们启动OrderApplication、UserApplication、UserApplication2服务时,会发现报错了,找不到feign-api项目的UserClient接口。解决方式有两种
第一种解决方式: 在order-service项目的启动类,指定FeignClient所在包,适合于有多个UserClient时,缺点是会把其它用不上的UserClient也加入进来
@EnableFeignClients(basePackage = "cn.itcast.feign.clients")
第二种解决方式: 在order-service项目的启动类,指定FeignClient字节码,也就是直接指定扫描哪个写好的UserClient,也可以指定多个。推荐这种解决方案
@EnableFeignClients(clients = {UserClient.class})
第六步。测试(先确保你的nacos已经启动),重启OrderApplication、UserApplication、UserApplication2服务
浏览器访问: http://localhost:8080/order/103