Spring Cloud 介绍及入门案例

Spring Cloud

Spring Cloud 集成了各种微服务组件,基于 Spring Boot 实现了这些组件的自动装配,从而提供了开箱即用的效果。

Spring Cloud 与 Spring Boot 版本对应:

cloudboot
2020.0.x aka Ilford2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton2.2.x, 2.3.x (Starting with SR5)
Greenwich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.5.x
  • DalstonEdgwareFinchleyGreenwich 等四个版本已经到达生命的最后阶段不再长期支持。

一、核心组件

Spring Cloud Netfix 系列

组件名称作用
Eureka服务注册、服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。
Ribbon提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。
FeignFeign是一种声明式、模板化的 HTTP 客户端,用于远程服务调用
Hystrix熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。
ZuulAPI 服务网关,提供动态路由,监控,弹性,安全等边缘服务的框架

Spring Cloud Alibaba 系列

组件名称作用
Nacos服务注册、服务发现、服务配置
Sentinel客户端容错保护,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Spring Cloud 原生及其他核心组件

组件名称作用
Consul服务注册中心,是一个服务发现与配置工具,与Docker容器可以无缝集成。
Config分布式配置中心,可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git以及Subversion。
GatewayAPI 服务网关,旨在提供一种简单而有效的方式来路由到 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,可以看到两个服务都注册到了注册中心:

eureka注册中心

后面编写订单服务与商品服务的接口,进行后续的远程服务调用、负载均衡和断路器配置等操作。

2.6 商品服务接口

在商品服务项目中添加 ProductProductController

// 商品实体类
@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分,然后分别修改端口为 90039004 继续启动,模拟部署三个商品服务

启动成功后,刷新注册中心,查看是否注册成功:

请添加图片描述

测试负载均衡是否配置成功,
访问订单服务地址: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 的提示。

好了,入门案例到此结束,希望对你有所帮助~

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值