SpringCloud
SpringCloud学习笔记,内容包括:认识微服务及SpringCloud、Eureka注册中心、Nacos注册和配置中心、Feign远程调用服务和Gateway网关
1.初识微服务
1.1 单体架构 和 分布式架构
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
1.2 微服务
可以认为微服务是一种经过良好架构设计的分布式架构方案。
微服务的架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
- 自治:团队独立、技术独立、数据独立,独立部署和交付
- 面向服务:服务提供统一标准的接口,与语言和技术无关
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
微服务拆分原则:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
1.3 SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,具体可以查看官方文档,截至2023年11月,兼容关系如下:
1.4 入门案例
项目结构如下:
在微服务架构中整个项目由多个实现不同业务的模块组成,比如上图管理订单的是order-service模块,管理用户的是user-service。
每个模块间相互独立,所在的运行端口也不同,仅通过其对外暴露的接口互相调用
1.5 远程调用案例
在order-service服务中,有一个根据id查询订单的接口:
根据id查询订单,返回值是Order对象,如图:
其中的user为null
在user-service中有一个根据id查询用户的接口:
查询的结果如图:
1.5.1 案例需求
要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回
1.5.2 实现步骤
首先,我们在order-service服务中的OrderApplication启动类中,注册RestTemplate实例:
1 |
|
然后需要修改OrderService类中的queryOrderById方法
注入restTemplate用于发送Http请求,使用**getForObject()**通过restTemplate调用userService的接口来获取User对象
2.Eureka注册中心
在上面的案例中,url直接用 ip+端口号 写死了
- 假如有多个user-service实例地址,此时url应该怎么写,怎样才能均衡地请求不同地址
- 假如其中一个地址宕机了,固定的 url 必然导致程序无法运行下去
因此我们需要使用 注册中心 来解决这些问题,最广为人知的注册中心就是Eureka,其结构如下:
2.1 搭建eureka-server
2.1.1 创建eureka-server服务
在工程目录下创建一个新模块,使用maven模块即可,这里名字设为eureka-service
2.1.2 引入eureka-server依赖
1 | <dependency> |
2.1.3 编写启动类
给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能:
1 |
|
3.2.4.编写配置文件
编写一个application.yml文件,内容如下:
1 | server: |
3.2.5.启动服务
启动微服务,然后在浏览器访问:http://127.0.0.1:10086 . 红框中的是已注册的服务,这里是eureka-server将自己注册进来了,以后可能eureka-server不止一个。
2.2 服务注册
接下来将user-service和order-service注册到eureka-server中去。
2.2.1 引入依赖
在user-service和order-service的pom文件中,引入下面的eureka-client依赖:
1 | <dependency> |
2.2.2 配置文件
在user-service和order-service中,修改application.yml文件,添加服务名称、eureka地址:
1 | spring: |
1 | spring: |
2.2.3 实现负载均衡
给RestTemplate这个Bean添加一个**@LoadBalanced**注解
2.2.4 拓展:服务多开
启动多个user-service实例
为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service。
首先,复制原来的user-service启动配置:
然后,在弹出的窗口中,填写信息:
现在,SpringBoot窗口会出现两个user-service启动配置:
不过,第一个是8081端口,第二个是8082端口。
启动两个user-service实例:
查看eureka-server管理页面:
2.3 服务发现
在服务拉取过程中,用 服务名 代替 IP+端口
spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。
3.Nacos注册中心
3.1 安装、启动和配置Nacos
3.1.1 安装
在官网下载压缩包解压到任意非中文目录下
3.1.2 启动和配置
- bin 目录下有启动命令,双击startup.cmd 或者 进入cmd窗口输入以下命令即可启动Nacos
startup.cmd -m standalone
- conf 目录下的application.properties是Nacos的配置信息,可以在这里面设置运行端口,默认为8848
然后在浏览器输入地址:http://127.0.0.1:8848/nacos即可访问,**默认的账号密码都是nacos**
3.2 服务注册
3.2.1 引入依赖
在cloud-demo父工程的pom文件中的<dependencyManagement>中引入SpringCloudAlibaba的依赖:
1 | <dependency> |
然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:
1 | <dependency> |
3.2.2 配置nacos地址
在user-service和order-service的application.yml中添加nacos地址:
1 | spring: |
注意配置完成后,仍然需要注意是否有给RestTemplate这个Bean添加一个**@LoadBalanced**注解,否则拉取服务时会报错
3.2.3 启动微服务
启动微服务后可以看到管理面板中服务列表已更新,成功识别到了几个服务
通过访问接口可以发现,前面Eureka完成的服务拉取和负载均衡,现在也能正常完成
3.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是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。
3.3.1 配置集群属性
修改user-service的application.yml文件,添加集群配置:
1 | spring: |
重启一个user-service实例后,我们可以在nacos控制台看到下面结果:
重启的那个userservice所在集群已经从DEFAULT变成HangZhou
3.3.2 同集群优先的负载均衡
@LoadBalanced默认使用的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。
(1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
1 | spring: |
(2)修改负载均衡规则
修改order-service的application.yml文件,修改负载均衡规则:
1 | userservice: #要做配置的微服务名称 |
这样配置完成后,orderservice在调用userservice时就会首先从同集群中调用,其次才会调用其他集群的服务
3.3.3 对比Nacos和Eureka
- Nacos与eureka的共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
- Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式。临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
- Nacos除了可以是注册中心,也可以是配置中心
4.Nacos配置中心
4.1 统一配置管理
4.1.1 在面板中添加配置
注意命名规则必须严格按照以下方式命名,否则拉取不到配置内容。如果不写 -[profile] ,那么这个配置文件就会是 多环境共享 的配置文件
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
4.1.2 微服务拉取配置
引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:
1 | <!--nacos配置管理依赖--> |
添加bootstrap.yaml
然后,在user-service中添加一个bootstrap.yaml文件,内容如下(application.yaml中重复的内容可以删除):
1 | spring: |
在读取配置时,会根据 “ 服务名称-开发环境.文件后缀名 ”拼接而成的文件名来寻找配置
比如本例中,就是去读取userservice-dev.yaml
读取nacos配置
使用**@Value("${}")**可以读取配置项,试着引用刚才在面板中配置的pattern.dateformat
启动服务后访问接口,可以看到配置内容已经成功被拉取到了
4.1.3 实现热更新
按以上操作完成发现,如果在nacos面板中修改的配置内容,而服务仍在运行中,此时刷新页面,会发现新的配置内容还是没有被读取到,仍然是老的配置内容。
这是因为服务没有实现热更新,我们只需要在**@Value注入的变量所在类上添加注解@RefreshScope**
这样,无需重启服务也可以读取到新的配置内容了
5.Feign远程调用
5.1 Feign替代RestTemplate
5.1.1 导入依赖
1 | <!--Feign--> |
5.1.2 添加注解
给需要远程调用的服务添加注解**@EnableFeignClients**
5.1.3 编写Client接口
新建一个包clients专门存放client,编写UserClient,内容与RestFul风格的接口类似,如下:
当调用userClient.getById(id)时,Feign就会自动向http://userservice/user/id发送get请求
调用方式:
5.2 Feign性能优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
URLConnection:默认实现,不支持连接池
Apache HttpClient :支持连接池
OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
此外还可以通过配置较低的日志级别等手段来优化性能
引入依赖
1 | <!--httpClient的依赖 --> |
配置连接池
在order-service的application.yml中添加配置:
1 | feign: |
5.3 最佳实践-抽取
由于项目中的每一个模块都在调用远程服务时,都需要编写Client、POJO,而且这些内容在每个模块中都一模一样,当模块数量增多时,每次都要复制相同的内容到新模块中,不仅麻烦而且浪费。
因此,可以把这些共同需要的Client、POJO等专门放在一个新模块中,而其他模块只需要导入依赖即可使用
抽取
首先创建一个名为feign-api的新模块
在feign-api中然后引入feign的starter依赖
1 | <dependency> |
然后,order-service中编写的UserClient、User都复制到feign-api项目中,其他项目中的相同内容可以删除
其他模块导入feign-api
1 | <dependency> |
修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包
解决包扫描问题
1 | @Autowired |
由于@Autowired只会在扫描启动类所在的包,所以处于另一个包中的UserClient无法被扫描到并创建对象存入IOC容器,所以会报错找不到UserClient。以下是两种解决方法,在启动类上配置Feign需要的包或接口。方式一火力覆盖、方式二精准打击。
方式一:
指定Feign应该扫描的包:
1 |
方式二:
指定需要加载的Client接口:
1 |
6.Gateway网关
6.1 搭建网关
6.1.1 新建网关模块
6.1.2 导入依赖,编写启动类
引入网关和nacos服务发现依赖
1 | <!--网关--> |
编写启动类
1 |
|
6.1.3 配置文件
创建application.yml文件,内容如下:
1 | server: |
我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。
本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
6.1.4 测试
访问10010端口,成功访问到了服务
6.2 过滤器工厂
6.2.1.路由过滤器的种类
Spring提供了31种不同的路由过滤器工厂。例如:
| 名称 | 说明 |
|---|---|
| AddRequestHeader | 给当前请求添加一个请求头 |
| RemoveRequestHeader | 移除请求中的一个请求头 |
| AddResponseHeader | 给响应结果中添加一个响应头 |
| RemoveResponseHeader | 从响应结果中移除有一个响应头 |
| RequestRateLimiter | 限制请求的流量 |
更多过滤器参考官网文档:Spring Cloud Gateway
6.2.2 添加过滤器
以AddRequestHeader为例
只需要修改gateway服务的application.yml文件,添加路由过滤器即可:
1 | spring: |
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。如果需要对所有微服务都起作用,可以用默认过滤器default-filters,使用方式如下:
1 | spring: |
这样配置好之后,就可以在接口处调用这个参数测试一下能否拿到 Message:
可以看到使用@RequestHeader成功拿到了Message
1 |
|
6.2.3 全局过滤器
GatewayFilter,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。
GlobalFilter与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
案例:
假如需要拦截请求并判断用户身份,放行条件为请求中带有参数authorization,值为admin
实现:
在gateway中定义一个类,实现GlobalFilter接口:
1 | /** |
可以看到传递了authorization=admin才能访问
6.3 解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
1 | spring: |