文章目录
Spring Cloud
Spring Cloud 集成了各种微服务组件,基于 Spring Boot 实现了这些组件的自动装配,从而提供了开箱即用的效果。
Spring Cloud 与 Spring Boot 版本对应:
cloud | boot |
---|---|
2020.0.x aka Ilford | 2.4.x, 2.5.x (Starting with 2020.0.3) |
Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
Dalston
、Edgware
、Finchley
、Greenwich
等四个版本已经到达生命的最后阶段不再长期支持。
一、核心组件
Spring Cloud Netfix 系列
组件名称 | 作用 |
---|---|
Eureka | 服务注册、服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。 |
Ribbon | 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。 |
Feign | Feign是一种声明式、模板化的 HTTP 客户端,用于远程服务调用 |
Hystrix | 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。 |
Zuul | API 服务网关,提供动态路由,监控,弹性,安全等边缘服务的框架 |
Spring Cloud Alibaba 系列
组件名称 | 作用 |
---|---|
Nacos | 服务注册、服务发现、服务配置 |
Sentinel | 客户端容错保护,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 |
Spring Cloud 原生及其他核心组件
组件名称 | 作用 |
---|---|
Consul | 服务注册中心,是一个服务发现与配置工具,与Docker容器可以无缝集成。 |
Config | 分布式配置中心,可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git以及Subversion。 |
Gateway | API 服务网关,旨在提供一种简单而有效的方式来路由到 API 并为它们提供交叉关注点,例如:安全性、监控/指标和弹性。 |
Sleuth/Zipkin | 提供分布式链路追踪功能,可以监控一条访问链中各个节点的状态 |
以上是 spring cloud 的部分核心组件,大家应该有点印象,然后再深入学习。
Spring Cloud 微服务项目结构:
- 上图由 Spring 官方提供。
- 从图中可以看出,API网关是客户端与服务端连接的桥梁,所有的访问客户需要经过统一的 API 网关才能访问对应的服务。
- 针对每个微服务本身,我们需要考虑的服务注册、服务配置、服务状态监控、链路追踪与问题定位等等问题都可以使用 Spring Cloud 来完成。
- Spring Cloud是一系列框架的有序集合。
二、入门案例
以商品订单系统为例:
- 系统中包含两个服务:订单服务和商品服务。
- 在订单服务中会调用商品服务的接口。
- 然后在订单服务端配置调用商品服务的负载均衡和断路器。
- 最后配置一个统一的 API 网关,供客户调用。
项目已经上传至 gitee,建议下载后与后文对照验证,项目地址:Spring Cloud 入门案例
2.1 创建父工程
创建一个普通的 Maven 项目spring-cloud-demo
,修改 pom 文件如下:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!--父工程-->
<packaging>pom</packaging>
<!--继承 Spring Boot,后续子模块都是基于 sprinig boot 进行开发-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<!--定义全局属性-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--boot 2.3.x 对应的 cloud 版本为 Hoxton-->
<spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>
<!--公共依赖-->
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!--管理 spring cloud 版本-->
<dependencyManagement>
<dependencies>
<!--spring cloud依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring could alibaba,后续用到的 sentinel 属于 spring cloud alibaba -->
<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>
</dependencies>
</dependencyManagement>
</project>
2.2 创建 eureka 注册中心
在父工程下创建子模块:eureka-server
,pom 文件如下:
<?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>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<!--引入 eureka 服务端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
启动类开启 eureka 服务:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // 启动 eureka 服务端
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
配置文件配置 eureka 服务端信息:
server:
port: 9000
spring:
application:
name: eureka-server
# 配置 eureka server
eureka:
instance: # 实例主机名
hostname: localhost
client:
register-with-eureka: false # 是否将当前服务注册到服务中心以供其他服务发现,默认 true
fetch-registry: false # 是否从 eureka 中获取注册信息
service-url: # 配置暴露给 eureka client 的请求地址
defaultZone: "https://${eureka.instance.hostname}:${server.port}/eureka/"
2.3 创建 order 服务
创建一个子项目order-service
,pom 文件如下:
<?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>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<!--Eureka Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
启动类开启 eureka 客户端:
@SpringBootApplication
@EnableEurekaClient // 开启 eureka 客户端,可省略
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
eureka 客户端配置文件:
server:
port: 9001
spring:
application:
name: order-service # 为当前服务命名
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/ # 配置服务注册地址,与 eureka-server 中暴露地址保持一致
instance:
prefer-ip-address: true
instance-id: order-service # 实例 id,服务的唯一标识
# instance-id: ${spring.cloud.client.ip-address}:${server.port} # 如果想在控制页面看到服务地址与端口,可以将 instance-id 这样配置
2.4 创建 product 服务
创建一个子项目:product-service
,修改 pom 文件如下:
<?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>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>product-service</artifactId>
<dependencies>
<!--Eureka Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
启动类:
@SpringBootApplication
@EnableEurekaClient // 开启 eureka 客户端,可省略
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
配置文件:
server:
port: 9002
spring:
application:
name: product-service # 为当前服务命名
eureka:
client:
service-url: # 配置服务注册地址,与 eureka-server 中暴露地址保持一致
defaultZone: http://localhost:9000/eureka
instance:
prefer-ip-address: true # 是否使用 IP 地址注册,默认 false
instance-id: product-service # 实例 id,服务的唯一标识
# instance-id: ${spring.cloud.client.ip-address}:${server.port} # 如果想在控制页面看到服务地址与端口,可以将 instance-id 这样配置
lease-renewal-interval-in-seconds: 5 # 发送心跳的间隔,单位秒,默认 30
lease-expiration-duration-in-seconds: 10 # 续约到期时间,单位秒,默认90
2.5 服务注册测试
首先启动注册中心服务:eureka-server
,然后启动订单服务order-service
与商品服务product-servie
启动成功后,访问注册中心地址:localhost:9000
,可以看到两个服务都注册到了注册中心:
后面编写订单服务与商品服务的接口,进行后续的远程服务调用、负载均衡和断路器配置等操作。
2.6 商品服务接口
在商品服务项目中添加 Product
、ProductController
// 商品实体类
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
}
// controller 类
@RestController
@RequestMapping("/product")
public class ProductController {
@Value("${server.port}")
private String port;
@Value("${spring.cloud.client.ip-address}")
private String ip;
@GetMapping("/{id}")
public Product findById(@PathVariable Long id) {
Product product = new Product();
product.setId(id);
// 后面需要测试负载均衡,所以返回 ip 地址及端口号
product.setName("当前访问服务地址:" + ip + ":" + port);
return product;
}
}
2.7 订单服务接口配置远程服务调用 Feign
由于订单服务中会调用商品服务中的接口,所以需要在order-service
项目中引入远程调用组件 Feign,添加依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在启动类上激活 feign
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients // 启动 feign
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
订单服务调用商品服务接口后会返回一个商品对象,所以在接收端订单服务中也需要声明一个商品对象:
@Data
public class Product {
private Long id;
private String name;
private BigDecimal price;
}
编写 Feign 接口,用于调用远程商品服务的接口
@Component // 让 spring 可以识别,不加也行,但是在注入的时候 IDEA 会报错,不会影响运行,但有条红线让自己不舒服
@FeignClient("product-service")
public interface ProductFeign {
@GetMapping(value = "/product/{id}")
Product findById(@PathVariable Long id);
}
编写订单服务接口:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
ProductFeign productFeign;
@GetMapping("/buy/{id}")
public Product buy(@PathVariable Long id) {
Product product = productFeign.findById(id);
return product;
}
}
2.8 远程调用测试
编写好两个项目的接口后,重启两个项目,访问订单服务接口,测试能否远程调用商品服务。
访问地址:http://localhost:9001/order/buy/1
,如果返回的 JSON 数据如下,说明访问成功:
{
"id": 1,
"name": "当前访问服务地址:192.168.0.106:9002",
"price": null
}
2.9 配置 Ribbon 负载均衡
ribbon 是客户端负载均衡器,在商品、订单服务中,订单服务是商品服务的客户端,所以需要在 order-service
中配置 ribbon。
由于之前引入的 spring-cloud-starter-openfeign
依赖中集成了 ribbon,所以不需要额外引入依赖,直接在配置文件中添加如下配置:
product-service: # 服务名
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 选择负载均衡策略,默认为轮询方式,当前配置为随机方式
ConnectTimeout: 250 # 连接超时时间
ReadTimeout: 1000 # ribbon 读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
负载均衡策略补充:
- ribbon 内置了多种负载均衡策略,顶级接口为:
com.netflix.loadbalancer.IRule
com.netflix.loadbalancer.RoundRobinRule
:以轮询方式进行负载均衡,默认方式com.netflix.loadbalancer.RandomRule
:随机策略com.netflix.loadbalancer.RetryRule
:请求重试策略com.netflix.loadbalancer.WeightedResponseTimeRule
:权重策略,权重越高,被调用可能性越大com.netflix.loadbalancer.BestAvailableRule
:最佳策略,遍历所有的服务实例,过滤掉故障实例,并返回请求数最小的实例com.netflix.loadbalancer.AvailabilityFilteringRule
:可用过滤策略。过滤掉故障和请求数超过阈值的服务实例,再从剩下的实例中轮询调用。
将 product-service
的启动配置复制2分,然后分别修改端口为 9003
和 9004
继续启动,模拟部署三个商品服务


启动成功后,刷新注册中心,查看是否注册成功:
测试负载均衡是否配置成功,
访问订单服务地址:http://localhost:9001/order/buy/1
,根据端口号判断是否成功。上面负载均衡策略为随机策略,多次访问可以看到返回的 json 值中的端口号是随机的,更换负载均衡策略,只需修改配置文件中的product-service.NFLoadBalancerRuleClassName:
配置项即可。
2.10 配置容错 Hystrix
在微服务架构中,一个请求需要调用多个服务是非常常见的。试想以下场景:
- 如客户端访问 A 服务,而 A服务 需要调用 B 服务,B 服务需要调用 C 服务。
- 由于网络原因或者自身的原因,如果 B 服务或者 C 服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。
- 此时若有大量的请求涌入,容器的线程资源会被消耗完毕导致服务瘫痪。
- 由于服务与服务之间的依赖性,所以故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果。
上面场景描述的就是服务故障的“雪崩"效应,为了解决雪崩效应,就需要断路器入场了,篇幅有限,就不展开介绍断路器了。
由于前面引入的 spring-cloud-starter-openfeign
依赖中已经集成了 feign-hystrix
,有了对 Hystrix 的支持,所以不需要额外引入依赖项。
首先需要在 order-service
项目中编写 ProductFeign
的实现类,子类中重写的方法就是熔断降级方法,可以在子方法中编写熔断降级逻辑。
@Component // 交由 IoC 管理,否则会报找不到 fallback instance
public class ProductFeignFallback implements ProductFeign {
/**
* 熔断降级方法,编写熔断逻辑
*/
@Override
public Product findById(Long id) {
Product product = new Product();
product.setId(id);
product.setName("触发了熔断降级方法");
return product;
}
}
然后在 ProductFeign
接口中指定熔断降级的回调类。
@Component
@FeignClient(value = "product-service", fallback = ProductFeignFallback.class)
public interface ProductFeign {
@GetMapping(value = "/product/{id}")
Product findById(@PathVariable Long id);
}
最后配置熔断降级策略,在 application.yml
中添加如下配置:
# 配置 feign
feign:
hystrix:
enabled: true # 开启断路器
# 配置 hystrix
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE # 隔离策略,SEMAPHORE=信号量隔离;THREAD=线程池隔离
semaphore:
maxConcurrentRequests: 1 # 设置最大并发请求数,默认 10,只有隔离策略为 SEMAPHORE 时才生效
thread:
timeoutInMilliseconds: 2000 # 调用超时时间(ms),默认 1000 ms,超时则触发熔断降级逻辑
为了验证信号量触发熔断,这里将最大并发请求数设为 1,然后在 product-service
项目的接口方法中添加延时,演示网络波动。
@GetMapping("/{id}")
public Product findById(@PathVariable Long id) {
// 模拟网络波动
try {
Thread.sleep(500); // 测试并发触发熔断
// Thread.sleep(3000); // 测试超时触发熔断
} catch (InterruptedException e) {
e.printStackTrace();
}
Product product = new Product();
product.setId(id);
// 后面需要测试负载均衡,所以返回 ip 地址及端口号
product.setName("当前访问服务地址:" + ip + ":" + port);
return product;
}
测试并发触发熔断时,需要快速刷新2次,http://localhost:9001/order/buy/1
地址的页面,第二次就会返回以下 JSON:
{
"id": 1,
"name": "触发了熔断降级方法",
"price": null
}
测试超时触发熔断时,只需要将延时的 500ms 改成 2000ms 即可。
2.11 配置统一 API 网关 Gateway
创建一个子模块:api-gateway-server
作为网关服务,pom 文件如下:
<?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>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway-server</artifactId>
<dependencies>
<!-- 将 gateway 注册到 eureka 后,可以根据服务名进行路由 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入网关 gateway
spring cloud gateway 内部通过 netty + webflux 实现
webflux 与 spring mvc 存在冲突
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>
网关配置文件:
server:
port: 8080
spring:
application:
name: api-gateway-server
cloud:
gateway:
routes:
- id: product-service # 路由 id,唯一标识
uri: http://localhost:9002
predicates:
- Path=/product/** # 断言,路由匹配条件,匹配 /product 开头的所有 api
- id: order-service
uri: lb://order-service # 微服务名称,lb:// 表示根据微服务名称从注册中心拉去服务请求路径
predicates:
- Path=/order/** # 断言,路由匹配条件,匹配 /order 开头的所有 api
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
启动类:
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
- 注意:当存在
spring-boot-starter-web
依赖时,api-gateway-server
项目不能启动成功,会报以下错误:
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
spring cloud gateway 内部通过 netty + webflux 实现,webflux 与 spring mvc 存在冲突。所以需要将之前在父 pom 中的 spring-boot-starter-web
删除,然后在需要这个依赖的其他子模块中单独添加。
成功启动后,访问网关地址:http://localhost:8080/order/buy/1
,可以发现与访问 http://localhost:9001/order/buy/1
是同样的效果。
添加统一身份验证,可以在网关项目中添加全局过滤器,然后配置认证逻辑即可。
@Component
public class LoginFilter implements GlobalFilter, Ordered {
/**
* 过滤器业务逻辑
*
* @param exchange 可以获取 Request 对象和 Response 对象
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行了全局过滤器");
String token = exchange.getRequest().getHeaders().getFirst("token");
if (token == null) { // 认证失败
System.out.println("认证失败");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 请求结束
}
// 放行
return chain.filter(exchange);
}
/**
* 过滤器的执行顺序,返回值越小,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
重启网关项目后,再次访问网关,则会出现 HTTP ERROR 401
的提示。
好了,入门案例到此结束,希望对你有所帮助~