1.SpringCloud概述
1.1 什么是微服务
1.1.1 单体架构
业务所有功能都打包在一个war包或jar包,这种方式就是单体架构,单体架构的应用就是单体应用。这种架构开发简单,部署简单,一个项目包含所有功能;省去了多个项目之间交互和调用消耗,直接部署在一个服务器即可。
单体面临的问题:
- 后端服务器压力大,负载高
- 业务场景发展变复杂之后,单体应用越来越大,各个业务之间耦合度也会越来越高:某一个功能挂了 ,全挂了 。
解决方案:
横向:添加服务器 -> 集群
纵向:把整块业务划分为不同的模块 -> 分布式
1.1.2 集群和分布式架构
集群:将一个完整的系统部署到多个服务器上,每个服务器都能提供系统的所有服务,多个服务器通过负载均衡调度完成任务,每个服务器称为节点
分布式:将一个系统拆分为多个子系统,多个子系统部署到多个服务器上,多个服务器的子系统完成一个特点任务
集群和分布式的联系:
- 从概念上:集群是多个计算机做相同的事,分布式是多个计算机做不同的事
- 从功能上:集群每个点功能相同和互相替代,分布式的节点业务实现不同
- 从关系上:在实践中,二者互相配合:分布式某一个节点是一个集群;分布式架构建立在集群上,二者通称分布式架构
1.1.3 微服务架构
按照业务拆分的话,会有一些重复功能实现:因此我们可以把通用的,被多个上层服务调用的共享业务,提取成独立的基础服务,组成一个个微小的服务:微服务
简单来说:微服务是很小的服务,小到一个服务只对应一个单一的功能,只做一件事,这个服务可以单独部署运行
微服务之间使用Rest或者RPC通信
分布式与微服务的区分
分布式:服务拆分,拆了就行
微服务:指非常微笑的服务,不能再拆分的服务,更细粒度的垂直拆分
微服务是一种经过良好架构设计的分布式架构方案;
1.2 什么是SpringCloud
SpringCloud是分布式微服务架构的一站式解决方案
注意版本一致
本文将以SpringCloudAlibaba为主进行讲解简单的入门及项目运用
2. SpringCloud运用
2.1 环境与工程搭建
win和linux都需要安装:JDK21+MySQL5.7
Linux安装jdk21:
wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
yum -y install jdk-21_linux-x64_bin.rpm
java -version
2.2 服务拆分
将模拟实现一个demo级别的电商平台,先从服务拆分起手:
问题来了如何划分?一般遵循以下原则:
1.单一职责原则
指的是一个类应该专注于单一功能,不要存在多于一个导致类变更的原因
2.服务自治
自己可以独立自理,每一个服务都可以独立开发构建部署测试...
3.单向依赖
不能存在循环依赖、双向依赖:循环依赖 A调用B,B调用C,C调用A ,双向依赖 A调用B,B调用A。
要注意的是:服务拆分不一定一定要遵循原则,具体情况具体划分以业务为主。
以商品订单为例:
把服务拆分为订单服务、商品服务
订单服务:提供订单ID,获取订单详细信息
商品服务:根据商品ID,返回商品详细信息
2.3 数据准备
订单表商品表
2.4 工程搭建
2.4.1 创建父子工程
创建Maven项目
2.4.2 订单、商品服务实现
简单的controller、service、mapper层实现即可
2.4.3 远程调用
我们想调用订单服务获取订单信息,订单信息中的商品ID在去调用商品服务,获取商品信息。
我们是用封装好的RestTemplate,通过http访问来实现远程调用。
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public OrderInfo selectOrderById(Integer orderId){
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
ProductInfo productInfo = restTemplate.getForObject("http://order/selectProductById?productId=" + orderInfo.getProductId(), ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
}
RestTemplate
Rest:表现层 资源 状态转移
资源:网络上的数据文本图片等
表现层:资源的表现形式 文本的表现形式是txt
状态转移:通过网络访问资源,对资源进行修改(引起资源状态的变化)
简单来说:Rest就是描述网络中client和server的一种交互形式,rest本身不是用,只是一种风格
Restful
满足Rest风格的接口就是Restful API
风格主要特征:
1.资源:资源可以是一个图片,对资源进行处理
2.统一接口:
RestTemplate由Spring提供,封装HTTP调用,并强制使用Resful风格,它会处理HTTP连接和关闭,只需要提供资源地址和参数即可
来看以上的代码出现的问题:
1、url是写死的,url出现变动需要手动修改
2、多机器部署如何处理
3、返回结果如何共用
....
SpringCloud将解决以上等等问题...
3.Eureka
注册中心三个角色:
服务注册:服务提供者启动时,向Registry注册自己的服务,并且向Registry定期发送心跳包汇报存活状态
服务发现:服务消费者从注册中心查询服务提供者的地址,并通过该地址去调用提供者的接口,服务发现一大重要作用:提供给服务消费者一个可用的服务列表
3.1 CAP理论
一致性
强一致性:主库从库无论何时,对外提供的服务都是一致的
弱一致性:随着时间的推移,最终达到一致性
可用性
对所有请求都有响应:这个请求可能是错误的也要响应
分区容错性
在网络分区的情况下,系统依然可用对外提供服务
举一个例子:
在银行中,所有的银行工作人员对客户,讲的利率都是一致的,这就是一致性
无论何时,银行人员对客户利率查询请求,都是有答案的,这个答案可能是旧的,这就是可用性
如果一个工作人员请假,银行依旧运行
P一定做到(分区容错性)
保证一致性的情况下,主从库同步期间不对外同步,因此无法保证可用性
保证可用性,数据同步的时候无法保证强一致性
只能二选一
3.2 常见的注册中心
1. zookeeper
Zookeeper的官方并没有说它是一个注册中心,但是国内Java体系,大部分的集群环境都是依赖Zookeeper来完成注册中心的功能.
2. Eureka
Eureka是Netflix开发的基于REST的服务发现框架,主要用于服务注册,管理,负载均衡和服务故障转移.官方声明在Eureka2.0版本停止维护,不建议使用.但是Eureka是SpringCloud服务注册/发现的默认实现,所以目前还是有很多公司在使用.
3. Nacos
Nacos是Spring Cloud Alibaba架构中重要的组件,除了服务注册,服务发现功能之外, Nacos还支持配置管理,流量管理,DNS,动态DNS等多种特性.
3.3 Eureka搭建注册中心
已淘汰,相关组件学习请看其他资料,下文有Nacos
3.负载均衡
负载均衡(LB)是高并发、高可用不可缺少的关键组件
当服务流量增大时,通常会采用增加机器的方式进行扩容,负载均衡就是在多个机器或其他资源中,按照一定的规则(不一定是平均)进行合理的分配
负载均衡分为客户端负载均衡和服务端负载均衡,二者的区分顾名思义就是负载均衡器在哪里实现,常见的服务端负载均衡器Nginx,常见的客户端负载均衡器为SpringCloudLoadBalancer
3.1 SpringCloudBalancer
3.1.1 简单入门
添加注解、修改远程调用代码,把ip和端口号改成服务名
3.1.2 负载均衡策略
只支持轮询和随机选择:
轮询:服务器轮流的处理请求
随机选择:随机选择一个后端的服务器处理请求
自定义负载均衡策略
要注意:该自定义类不需要@Configuration注释,在组件扫描范围内
使用自定义负载均衡策略,name:该负载均衡器对哪个服务生效;config:该负载均衡策略用哪个策略来实现(图内是自己实现的自定义负载均衡策略)
3.2 原理
LoadBalancer的实现,主要是LoadBalancerInterceptor
拿到url,从url拿到host获取服务名称,判断不能为空
第一步就是根据服务名称获取负载均衡策略(像上文提到的的自定义均衡策略注解中name属性),如果负载均衡策略为空就返回null,不为空就调用loadBalancerResponse.getServer方法
轮询的策略:
4. Nacos
在最初开源时, Nacos选择进⾏内部三个产品合并统⼀开源(Configserver ⾮持久注册中⼼,VIPServer 持久化注册中⼼,Diamond 配置中⼼). 定位为:⼀个更易于构建云原⽣应⽤的动态服务发现, 配置管理 和服务管理平台. 所以Nacos是⼀个注册中⼼组件, 但它⼜不仅仅是注册中⼼组件.
4.1 Nacos安装
Release 2.2.3 (May 25th, 2023) · alibaba/nacos · GitHub
下载之后解压:
Nacos启动默认是集群模式,自我学习需要修改
以文本编辑器打开start.cmd 进行修改
修改端口号是在conf下application.properties修改port
4.2 快速入门
4.2.1 环境搭建
父子项目都需要添加相对应的依赖,loadBalancer依赖也需加入。要注意的是SpringBoot和Nacos版本有要求。
配置文件
4.2.2 远程调用
4.2.3 服务下线与权重配置
Nacos在控制台上对某一个机器进行下线,IDEA上看这个机器实际还在运行,消费者发送请求(url,restTemplate)不会请求到已下线的机器;
Nacos控制台还可以配置节点的权重
不过要注意的是,如果加了LoadBalancer的话是没办法使用到Nacos权重的:
配置文件修改即可
4.2.4 同集群优先访问
在分布式架构中,一个应用会有多个实例,如何部署?
Nacos把同⼀个机房内的实例, 划分为⼀个集群. 所以同集群优先访问, 在⼀定程度上也可以理解为同机 房优先访问.
因此微服务访问时, 应尽量访问同机房的实例. 当本机房内实例不可⽤时, 才访问其他机房的实例
⽐如order-service 在上海机房, product-service 在北京和上海机房都有实例, 那我们希望可以优先访 问上海机房, 如果上海机房没有实例, 或者实例不可⽤, 再访问北京机房的实例. 通常情况下, 因为同⼀个 机房的机器属于⼀个局域⽹, 局域⽹访问速度更快⼀点!
需要开启Nacos的负载均衡策略
Nacos负载均衡总结
1.支持服务上下线
2.支持权重的配置(非严格)
3.同集群优先访问
2、3前提是需要开启Nacos负载均衡
4.3 健康检查
Nacos作为注册中心,需要感知服务的健康状态
Nacos有俩种健康检查机制
客户端主动上报机制
客户端主动以心跳上报机制告知服务器健康状态,默认心跳间隔5s
nacos会在超过15s未收到心跳后将实例设置为不健康状态,超过30s自动删除
服务器端反向探测机制
nacos主动探知客户端健康状态,默认间隔20s
健康检查失败后,标记为不健康,不会删除
但要注意的是这俩机制不能自主设置, 这俩机制与Nacos的服务实例类型有关
Nacos服务实例类型
Nacos的服务实例(注册的节点)分为临时实例和⾮临时实例.
• 临时实例: 如果实例宕机超过⼀定时间, 会从服务列表剔除, 默认类型.
• ⾮临时实例: 如果实例宕机, 不会从服务列表剔除, 也可以叫永久实例
修改非临时:配置文件中修改即可
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为⾮临时实例
总结:
4.4 环境隔离
配置文件中
spring.cloud.nacos.discovery.namespace:填写命名空间的ID
环境不同的服务是无法进行调用的
4.5 配置中心
除了注册中⼼和负载均衡之外, Nacos还是⼀个配置中⼼, 具备配置管理的功能
为什么需要配置中心?
当前项⽬的配置都在代码中, 会存在以下问题:
1. 配置⽂件修改时, 服务需要重新部署. 微服务架构中, ⼀个服务可能有成百个实例, 挨个部署⽐较⿇ 烦, 且容易出错.
2. 多⼈开发时, 配置⽂件可能需要经常修改, 使⽤同⼀个配置⽂件容易冲突
流程
1.服务启动时, 从配置中⼼读取配置项的内容, 进⾏初始化.
2. 配置项修改时, 通知微服务, 实现配置的更新加载
使用
1、添加配置、引入依赖
2、修改配置文件
配置管理的命名空间 不是 服务管理(不同环境)的命名空间,二者并无关系只是叫法一样
添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud 2020.*之后版本需要引⼊bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
再创建一个配置文件bootstrap.yml
微服务启动前, 需要先获取nacos中配置, 并与application.yml配置合并. 在微服务运⾏之前, Nacos要求 必须使⽤ bootstrap.properties 配置⽂件来配置Nacos Server 地址
spring:
application:
name: 服务名称
cloud:
nacos:
config:
server-addr: IP地址
配置中心和注册中心是隔离的
配置中心详解
设置命名空间
修改bootstrap配置文件
spring.cloud.nacos.config.namespace=配置中心的命名空间ID
Data ID
官方文档
prefix 默认为 spring.application.name 的值, 也可以通过配置项 spring.cloud.nacos.config.prefix 来配置.
• spring.profiles.active 即为当前环境对应的 profile. 当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${fileextension}
• file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。
⽬前只⽀持 properties 和 yaml 类型.
默认为properties. 微服务启动时, 会从Nacos读取多个配置⽂件:
1. ${prefix}-${spring.profiles.active}.${file-extension} 如: product-servicedev.properties
2. ${prefix}.${file-extension} , 如: product-service.properties
3. ${prefix} 如product-service
三个⽂件的优先级为: product-service-dev.properties > product-service.properties > productservice
5.服务部署
1. 修改数据库, Nacos等相关配置
2. 对两个服务进⾏打包
3. 上传jar到Linux服务器
4. 启动Nacos 启动前最好把data数据删除掉
5. 启动服务 nohup
Nacos与Eureka的区别
共同点:
都⽀持服务注册和服务拉取
区别:
1.功能 :Nacos除了服务发现和注册之外, 还提供了配置中⼼, 流量管理和DNS服务等功能.
2. CAP理论
Eureka遵循AP原则, Nacos可以切换AP和CP模式,默认AP.
Nacos 根据配置识别CP或者AP模式.
如果注册Nacos的Client的节点是临时节点, 那么Nacos对这个 Client节点的效果就是AP, 反之是CP. AP和CP可以同时混合存在.
3. 服务发现
Eureka:基于拉模式. Eureka Client会定期从Server拉取服务信息, 有缓存, 默认每30秒拉取⼀次. Nacos:基于推送模式. 服务列表有变化时实时推送给订阅者, 服务端和客⼾端保持⼼跳连接.
6. openFeign
前面我们使用RestTempalte会带来几个问题:url拼接复杂,代码可读性差,风格不统一
微服务之间通讯方式通常有俩种:RPC和HTTP
SpringCloud默认使用HTTP:RestTemplate和openFeign
RPC(Remote Procedure Call)远程过程调⽤,是⼀种通过⽹络从远程计算机上请求服务,⽽不需 要了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信, 如HTTP、TCP、UDP等, 并且在 TCP/IP⽹络四层模型中跨越了传输层和应⽤层。简⾔之RPC就是像调⽤本地⽅法⼀样调⽤远程⽅法
6.1 介绍openFeign
OpenFeign 是⼀个声明式的 Web Service 客⼾端. 它让微服务之间的调⽤变得更简单, 类似controller 调⽤service, 只需要创建⼀个接⼝,然后添加注解即可使⽤OpenFeign
可以简单理解为 Netflix Feign 是OpenFeign的祖先, 或者说OpenFeign 是Netflix Feign的升级版. OpenFeign 是Feign的⼀个更强⼤更灵活的实现
Spring Cloud Feign 是 Spring 对 Feign 的封装, 将 Feign 项⽬集成到 Spring Cloud ⽣态系统中.
受 Feign 更名影响,
Spring Cloud Feign 也有两个 starter
• spring-cloud-starter-feign
• spring-cloud-starter-openfeign (使用这个)
6.2 快速上手
Feign客户端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
}
@FeignClient 注解作⽤在接⼝上, 参数说明:
• name/value:指定FeignClient的名称, 也就是微服务的名称, ⽤于服务发现, Feign底层会使⽤ Spring Cloud LoadBalance进⾏负载均衡. 也可以使⽤ url 属性指定⼀个具体的url.
• path: 定义当前FeignClient的统⼀前缀
6.3 参数传递
6.4 最佳实践
通过观察, 我们也能看出来, Feign的客⼾端与服务器端⾮常相似
继承
我们可以定义好⼀个接⼝, 服务提供⽅实现这个接⼝, 服务消费⽅编写Feign 接⼝的时候, 直接继承这个 接⼝
具体参考: Spring Cloud OpenFeign Features :: Spring Cloud Openfeign
分为几步操作:
在创建一个module:product-api模块
复制 ProductApi, ProductInfo 到product-api模块中
public interface ProductInterface {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
@RequestMapping("/p2")
String p2(@RequestParam("id")Integer id,@RequestParam("name")String name);
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo);
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
}
目录结构:
maven将此模块打包进本地仓库中,供其他模块调用(加依赖即可调用)
服务提供⽅实现接⼝ ProductInterface
@RequestMapping("/product")
@RestController
public class ProductController implements ProductInterface {
@Autowired
private ProductService productService;
@RequestMapping("/{productId}")
public ProductInfo getProductById(@PathVariable("productId") Integer
productId){
System.out.println("收到请求,Id:"+productId);
return productService.selectProductById(productId);
}
@RequestMapping("/p1")
public String p1(Integer id){
return "p1接收到参数:"+id;
}
@RequestMapping("/p2")
public String p2(Integer id,String name){
return "p2接收到参数,id:"+id +",name:"+name;
}
@RequestMapping("/p3")
public String p3(ProductInfo productInfo){
return "接收到对象, productInfo:"+productInfo;
}
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo){
return "接收到对象, productInfo:"+productInfo;
}
}
Feign客户端可以删除了
抽取
简单来说创建一个module,把Feign客户端完整的复制过去,加入到Maven仓库,供其他服务使用
客户端(谁调用的)加注解
在启动类中加!!
@EnableFeignClients(basePackages = {"com.bite.api"})
也可以指定需要加载的Feign客⼾端
@EnableFeignClients(clients = {ProductApi.class})
服务部署
确认配置
打包
部署
7. 统一服务入口 Gateway
当前的所有微服务接口都是直接对外暴露的,可以直接通过外部访问,为了保证安全性。服务端实现的微服务接⼝通常都带有⼀定的权限校验机制. 由于使⽤了微服务, 原本⼀个应⽤的的多个 模块拆分成了多个应⽤, 我们不得不实现多次校验逻辑。
如果是多个服务都需要校验一下权限,那也太麻烦了:Gateway应运而生
7.1 什么是API网关
API⽹关(简称⽹关)也是⼀个服务, 通常是后端服务的唯⼀⼊⼝. 它的定义类似设计模式中的Facade模式 (⻔⾯模式, 也称外观模式). 它就类似整个微服务架构的⻔⾯, 所有的外部客⼾端访问, 都需要经过它来进 ⾏调度和过滤
核心功能
权限校验:网关没过,后面的服务访问不到
动态路由:⼀切请求先经过⽹关, 但⽹关不处理业务, ⽽是根据某种规则, 把请求转发到某个微服务
负载均衡:当路由的⽬标服务有多个时, 还需要做负载均衡
限流:请求流量过⾼时, 按照⽹关中配置微服务能够接受的流量进⾏放⾏, 避免服务压⼒过⼤
7.2 快速入门
创建一个服务
引入依赖 网关服务添加配置
eg:
uri:目标服务地址
predicates:路由条件(满足后面的条件,会把这个请求转发到uri的地址上)
7.3 Route Predicate Factories
predicate是java8提供的函数式编程接口,接收一个参数并返回一个布尔值用于条件过滤。
Route Predicate Factories (路由断⾔⼯⼚, 也称为路由谓词⼯⼚, 此处谓词表⽰⼀个函数), 在Spring Cloud Gateway中, Predicate提供了路由规则的匹配机制
我们配置文件中写的断言规则是字符串,这些字符串会被RoutePredicateFactory读取并处理,转变为路由判断条件。
更多的条件详见:Route Predicate Factories :: Spring Cloud Gateway
当有多个路由条件的时候,他们的关系是and
7.4 Gateway Filter Factories(网关过滤工厂)
Predicate是决定了请求由哪一个路由处理,Filter则是在请求处理前后加一些逻辑
分俩种类型Post和Pre(根据作用时间划分的)
从作用范围上:
GatewayFilter(应用到单个路由上或一组路由上)和GlobalFilter(应用到所有的路由上,对所有的请求都生效)
7.4.1 GatewayFilter
例子:
GatewayFilter Factories :: Spring Cloud Gateway
默认过滤器
针对全部的路由生效:spring.cloud.gateway.default-filters这个属性加一个filter列表
7.4.2 GlobalFilter
作用与GatewayFilter相同,只不过的是GlobalFilter是应用所有路由请求
Global Filters :: Spring Cloud Gateway
配置文件进行添加
spring.cloud.gateway:
7.5 过滤器执行顺序
请求路由后,global和gateway Filter合并到一个过滤器链中,并进行排序,依次执行过滤器
每个过滤器都有一个int类型的order值,其值越小优先级越高;
当order值一致时,执行顺序是Derfault > Gateway > Global
7.6 自定义过滤器
7.6.1 自定义GatewayFilter
需要实现GatewayFilterFactory,SpringBoot默认实现的是AbstractGatewayFilterFactory
主体类:
@Slf4j
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {
public CustomGatewayFilterFactory() {
super(CustomConfig.class);
}
@Override
public GatewayFilter apply(CustomConfig config) {
return new GatewayFilter() {
/**
* ServerWebExchange: HTTP 请求-响应交互契约, 提供了对HTTP请求和响应的访问
* GatewayFilterChain: 过滤器链
* Mono: Reactor的核心类, 数据流发布者,Mono最多只能触发一个事件.可以把Mono用在异步完成任务时,发出通知
* chain.filter(exchange) 执行请求
* Mono.fromRunnable() 创建一个包含Runnable元素的数据流
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//Pre类型 执行请求 Post类型
log.info("Pre Filter, config:{} ",config); //Pre类型过滤器代码逻辑
return chain.filter(exchange).then(Mono.fromRunnable(()->{
log.info("Post Filter...."); //Post类型过滤器代码逻辑
}));
}
};
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
配置类
import lombok.Data;
@Data
public class CustomConfig {
private String name;
}
使用配置文件
7.6.2 自定义GlobalFilter
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Pre Global Filter");
return chain.filter(exchange).then(Mono.fromRunnable(()->{
log.info("Post Global Filter...");
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}