SpringCloud

单体架构

我们所实现的web应用都有一个共同的特点,所有的代码最终打包成一个文件(jar包),整个系统的所有功能单元整体部署到同一个进程,这种软件架构的风格,即所谓的”单体架构”

单体架构的扩容

一个单体应用在运行时,会部署在一台云服务器上,但是随着用户体量的增长,一台云服务器上运行的一个单体应用,已经无法承载日益增长的请求量,怎么办呢?我们可以对单体应用实现扩容,即使用单体应用集群

通过使用单体应用的集群,可以一定程度上,很好的应对日益增长的用户请求,但是这样就完美了吗?当然不是

单体架构的优势和弊端

在单体应用的早期,应用程序相对较小,单体架构的好处:

  • 应用的开发很简单
  • 易于对应用程序进行大规模的更改
  • 测试相对直观简单
  • 部署简单明了
  • 横向扩容不费吹灰之力

但是,随着时间的推移,随着单体应用中包含的功能越来越多,应用的”体积”越来越大,单体应用的弊端就会逐渐体现出来。

  • 代码过度复杂且严重耦合,导致难以维护
1. 由于系统本身过于庞大和复杂,以至于任何一个开发者都很难理解它的全部,因此,修复软件中的问题和正确实现新功能就变得困难且耗时
2. 更糟糕的是,这种极度的复杂性,可能会形成一个恶性循环: 由于代码难以理解,因此开发者在更改时更容易出错,每一次更改都会让代码库变得更复杂,更难懂
  • 从代码提交到实际部署的周期很长
从代码完成到运行在生产环境,是一个漫长且费力的过程。
1. 众多开发人员都向同一个代码库提交代码,常常使得代码库的构建结构处于无法交付的状态。当采用了分支来解决这个问题,又必须忍受漫长且痛苦的合并过程
2. 因为代码库中的代码十分复杂,以至于任何一个更改可能引起的影响是未知的,为了避免牵一发而动全身的后果,即使是一个微小的更改,也必须执行全部的测试
  • 扩展性受限
1. 如果单体应用中的某一个功能点存在性能问题,那么就需要多部署几个单体应用的实例,再加上负载均衡的设备(比如nginx),才能保证整个应用的性能能够支撑用户的使用
2. 在某些情况下,应用的不同模块对资源的需求是相互冲突的,比如某些模块需要高效的IO,某些模块需要高性能的CPU, 而这些模块都在一个单体应用之内,因此其所部署的服务器必须满足所有的需求   
  • 开发慢,启动慢,严重影响开发效率
  • 交付可靠的单体应用困难
1. 单体应用体积庞大,难以进行全面和彻底的测试,而缺乏可靠的测试意味着代码中的错误会进入生产环境
2. 缺乏故障隔离,因为所有的模块都在同一个进程中运行,每隔一段时间,在一个模块中的代码错误,将会导致整个应用程序的崩溃

微服务架构

我们看到,随着单体应用的发展,最终会变得难以维护,难以实现及时可靠的交付,且开发效率低。那么怎么解决这个问题呢?解决之道就在于微服务架构。

要理解微服务架构,我们得首先理解微服务,那什么是微服务呢?

简单理解一个微服务的本质就是一个麻雀虽小但五脏俱全的应用程序,它按照单一职责原则实现了特定的一组功能。

  • 因为每个微服务的本质都可以是一个应用程序,这就要求,微服务可以独立部署,独立运行,独立对外提供服务(运行在一个独立的进程中)
  • 每个微服务,根据单一职责原则,实现一组相关功能

在此基础上,什么是微服务架构呢?简单理解,就是把应用程序功能写分解为一组服务的架构风格。实际上,微服务架构是模块化开发的一种形式。

模块化是开发大型,复杂应用程序的基础。当一个单体应用程序的规模太大的时候,是很难作为一个整体开发,也很难让一个人完全理解的。为了让不同的人开发和理解(不同的部分),大型应用需要拆分模块。

  • 在单体架构中,模块通常由一组编程语言所提供的的结构(例如Java中的包,或者jar文件)来定义,但是通过这种方式得到的模块,不同模块的代码还是可以相互引用,导致模块中对象依赖关系的混乱
  • 而微服务架构,使用微服务作为模块化的单元,要访问服务,只能通过服务对外提供的API,于是服务的API为它自身构筑了一个不可逾越的边界,你无法越过API去访问服务内部的类。

微服务的优势和弊端

使用微服务架构,可以解决庞大的单体应用的痛点,带来很多好处:

  • 每个服务都相对较小,容易维护
  • 使得大型的应用程序实现快速的持续交付和持续部署
1. 每一个服务相对较小,编写全面的测试代码和执行自动化测试都变得相对容易
2. 每个服务都独立于其他服务部署,如果负责服务的开发人员,需要部署对该服务的更改,不需要与其他开发人员协商,因此将更改频繁部署到生产中要容易的多
  • 应用扩展灵活
1. 应用被拆分为不同的微服务,而微服务可以独立部署,因此,扩容就不在针对整个应用了,哪里出现性能瓶颈,对哪个服务扩容即可
2. 即使不同的的服务需要资源存在冲突,也没有关系,把它们分别部署到具有拥有各自所需要资源的机器上即可
  • 更好的容错
相比于单体架构中,一个故障拖垮整个系统的情况,一个服务的故障,并不会影响想到其他服务的正常运行。

当然,使用微服务也会带来一些弊端

  • 分布式系统可能复杂难以管理
  • 分布式部署追踪问题难
  • 分布式事务比较难处理
  • 服务数量增加,管理复杂性增加

微服务的拆分

服务拆分的注意事项:

  • 每个服务的功能有边界,因此每个服务访问的数据也是有边界的,所以每个服务都有自己的数据库
  • 每个服务的数据库只限于该服务自己直接访问,其他服务不能直接访问
  • 如果一个服务需要其他服务的数据,则可以通过调用其他服务对外暴露的接口,访问到其他服务的数据

微服务的实现

一个微服务架构的项目,大致结构如上图所示,因为每个服务独立运行,独立部署,所以想要将微服务架构在项目中落地,还需要解决一些其他问题,服务之间的调用,服务的治理,比如服务的注册与自动发现,服务调用的负载均衡等等

而这些问题,都由相应的服务框架,已经帮助我们实现了,所以我们在实现微服务架构的项目,都需要基于某个微服务的框架,目前比较流行的有大概有两种SpringCloud和Dubbo,我们的学习主要基于框架SpringCloud。

SpringCloud 基于SpringBoot提供了一套微服务架构实现的解决方案,包括服务的注册与自动发现,面向接口的服务调用,服务调用的负载均衡,服务网关,服务熔断等等组件,它利用SpringBoot开发的便利性,巧妙的简化了分布式系统的基础设施搭建,使开发者可以基于SpringBoot的开发风格做到快速启动和部署。

服务调用的场景

在下单的时候,我们除了需要给用户展示待下单的商品数据,通常我们还需要给用户展示其地址信息,以供其选择。而订单由订单服务管理,用户的地址信息由用户服务管理。

这也就意味着,在下单之前,订单服务不仅需要查询出订单信息,还需要调用用户服务获取用户的地址信息,于是这里就出现了服务调用

服务间的调用

实际上,在基于SpringCloud实现的微服务架构中,一个微服务实例(进程)的本质,就是一个部署在Tomcat中的,满足单一职责原则的应用程序。

服务调用的理论

实际上,服务间的调用,基于我们以前学习过的知识,就可以轻松解决:

  • 基于SpringCloud实现的微服务,支持基于Http协议的通信,因此服务间的调用就变成了一次基于Http的通信
  • 在这次服务调用过程中,调用的目标是谁呢?可以是Controller方法,因为一个Http请求刚好可以被一个Controller方法接收处理,就相当于调用到了这个Controller方法
  • 但是请注意,我们能且只能调用另外一个服务的Controller方法(一个Controller方法就处理一个Http请求,相当于一个服务对外暴露的一个接口),所以说一个服务只能调用另外一个服务对外暴露的接口!

在一次服务调用过程中,我们称调用者为服务消费者(使用或者消费另一个服务的功能),我们称被调用者为服务提供者(提供被消费的功能)

实现服务调用的准备

通过服务调用的理论分析,我们知道服务调用其实就是发送Http请求,接收Http响应的过程,在实现服务调用之前,我们还存在一个问题,就是如何通过代码发送Http请求——使用RestTemplate。

RestTemplate是一个专门用来发送Http请求的工具,通过封装JDK中的HttpURLConnection类库,提供简单易用的模板方法API,它所提供的模板方法几乎覆盖了常用的所有Http请求类型的场景。

我们通常使用其无参构造方法,来获取一个RestTemplate对象:

RestTemplate()
Create a new instance of the RestTemplate using default settings.

同时,我们可以通过一个RestTempate对象,发起POST,GET,DELETE,PUT,PATCH等不同种类的Http请求,我们以最常用的GET和POST两种方式为例来学习RestTemplate的用法。

/*
	当我们只想要获取一个GET请求的结果的时候,可以调用其getForObject方法,其中
	String url: 发起请求的url
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> T getForObject(String url, Class<T> responseType, Map<String,?> uriVariables)
public <T> T getForObject(String url,Class<T> responseType, Object... uriVariables)

/* 
    如果我们不仅想要获取GET请求的响应体数据,还想获取响应头,以及响应码等信息,那么可以使用getForEntity方法,其中
	String url: GET请求的url
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String,?> uriVariables)
    
/*
	当我们只想要获取一个POST请求的结果的时候,可以调用其postForObject方法,其中
	String url: 发起请求的url
	request:表示post请求的请求体数据,也可以不传这个参数
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String,?> uriVariables)
public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
    
/* 
    如果我们不仅想要获取GET请求的响应体数据,还想获取响应头,以及响应码等信息,那么可以使用getForEntity方法,其中
	String url: POST请求的url
	Object request: 表示post请求的请求体数据,也可以不传这个参数
	Class<T> responseType: 响应体返回值类型
	第三个参数: 表示GET请求携带的参数
*/
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String,?> uriVariables)

我们以发送GET请求为例来说明:

       // 准备好请求的url,携带名为name的请求参数,占位符的名称为name 
	   String url = "http://localhost:9001/user/address/{userId}";
        // map中参数值的key一定要和url中参数的占位符同名
        HashMap<String, Object> param = new HashMap<>();
        param.put("userId", userId);
         //map参数方式
        String rest = restTemplate.getForObject(url, String.class, param);
/*
    准备好请求的url,携带名为name的请求参数,占位符的名称就不重要了,
    因为通过可变参数可以通过参数位置确定请求参数对应参数值
*/
String url = "http://localhost:9001/user/address/{userId}";
// 用可变参数的方式来传递请求参数, 这里只有一个参数值name
String rest = restTemplate.getForObject(url,String.class, userId);
        // 获取更完整的响应信息
        ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
        // 获取http响应的响应码
        HttpStatus statusCode = forEntity.getStatusCode();
        System.out.println(statusCode);
        // 返回响应体数据
        return forEntity.getBody();

通常我们还可以用

服务调用的实现

被调用接口

那么接下来,我们来实现一个服务消费者调用服务提供者的例子,假设服务提供者对外暴露的接口为

入参类型
userIdLong

请求路径:/user/address/{userId}

请求类型: GET

请求示例:

http://localhost:9001/user/address/{userId}

出参类型
addressString

父工程依赖

因为代码包含多个maven工程,因此我们使用父子工程来实现,创建父工程,父工程中并不包含代码,主要用来管理子Maven工程

 <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.parameters>true</maven.compiler.parameters>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.2.6</spring-boot.version>
        <spring-cloud.version>2023.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2023.0.1.3</spring-cloud-alibaba.version>
        <mysql.version>8.3.0</mysql.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <lombok.verion>1.18.32</lombok.verion>
    </properties>
    <dependencyManagement>
        <dependencies>
            <!-- spring-boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--spring cloud alibaba-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--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>

            <!--mybatis-plus-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.verion}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

服务提供者实现

依赖如下

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

配置如下

server:
  port: 9001
spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3306/demo_user?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver

代码如下(SpringBoot工程的启动类就不展示了)

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    
    /**
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/address/{id}")
    public String queryById(@PathVariable("id") Long id) {
        // 根据id查询用户的地址信息
        return userService.queryById(id);
    }
}
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public String queryById(Long id) {
        User user = userMapper.findById(id);
        return user.getAddress();
    }
}

服务消费者实现

依赖如下

 	 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

配置如下

server:
  port: 9002
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3306/demo_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver

代码如下

先准备好一个用于发起Http请求的RestTemplate对象

@Configuration
public class ClientConfig {

    @Bean
    public RestTemplate template() {
        return new RestTemplate();
    }
}
@RestController
@RequestMapping("order")
public class OrderController {

   @Autowired
   private OrderService orderService;

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }
}
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;


    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1.url地址
        String url = "http://localhost:9001/user/address/{userId}";
        // 2.2.发送http请求,实现远程调用
        String userAddress = restTemplate.getForObject(url, String.class,order.getUserId());
        // 3.封装user到Order
        order.setUserAdress(userAddress);
        // 4.返回
        return order;
    }
}

服务的注册与发现

服务间调用的问题,但是仅仅实现到这种程度,还远远不够

问题的引出

首先回顾一下,对于服务消费者而言,它如何知道服务提供者的信息,从而调用远程服务的功能呢? 再来看看代码

String url = "http://localhost:9001/user/address/{userId}";
// 实现远程调用
String userAddress = restTemplate.getForObject(url, String.class, order.getUserId());

但是,将服务消费者要调用的服务提供者地址写死,这样好吗?我们来设想如场景

  • 假设对于用户服务的请求量很大,一个用户服务实例(进程)处理不了这么多的请求了,此时我们就可以启动多个用户服务实例(进程),此时,这多个用户服务实例就组成了用户服务的集群
  • 但是我们如果写死了调用的服务提供者的地址,即使有用户服务的集群,有意义吗?没有,因为写死了服务调用的地址,所以我们永远只能调用到集群中的一个服务实例!

此时矛盾就出现了,服务调用时必须知道服务提供者的地址,但是在代码里把改地址写死,就永远只能调用确定的那一个服务实例,但是如果不在代码里写服务调用的地址,我们又从哪里得到服务提供者实例的地址呢?

服务注册中心

如果在定义服务消费者的时候,不指明服务消费者调用的服务提供者地址,那么服务消费者怎么知道去哪里调用服务提供者呢?此时,我们就需要引入一个新的角色——服务注册中心,由服务注册中心来统一管理服务的状态和信息,那么这个问题就可以解决了。

对于每一个服务

  • 在服务启动时,会将自己的信息,注册到服务注册中心,其中就包括,ip地址,端口号等信息。即实现服务的注册
  • 在服务运行过程中,会时时向服务注册中心”报告”自己的状态,因此服务注册中心就可以实时感知到服务的运行状态
  • 同时,在服务启动时,也会去注册中心拉取,其他服务信息,即将服务注册表信息下载到本地,这样一来一个服务就可以知道,其他服务调用地址等信息
  • 在服务运行的过程中,服务会从注册中心时时拉取最新的服务注册表信息,从而实现服务的实时发现

那么服务注册中心需要我们自己去实现吗?不是,已经有很多的注册中心实现供我们使用了,比如SpringCloud Alibaba中的Nacos,以及SpringCloud Netflix中的Eureka等等

Nacos 注册中心

Nacos(Dynamic Naming and Configuration Service)是SpringCloud Alibaba中包含的注册中心组件,实现服务的注册与自动发现功能,Nacos主要采用C-S架构实现

  • NacosServer实现注册中心的功能,可以直接独立运行
  • NacosDiscovertyClient负责帮助服务实例访问NacosServer,实现服务的注册和自动发现

启动Nacos Server

首先,下载好Nacos安装包(.zip压缩包),解压后,如下图

进入bin目录,打开命令行,输入如下命令

windows: startup.cmd -m standalone
linux 或 mac: startup.sh -m standalone

启动之后看到如下界面,这里上面的部分没截全

启动Nacos Server之后,我们就可以通过localhost:8848/nacos 访问Naocs自带的控制台查看注册中心了。 用户名和密码 nacos

下面我们使用Nacos作为服务注册中心,实现服务的注册与自动发现。

服务的注册

首先改造用户服务和订单服务。

在用户服务中,添加如下依赖

<!--Nacos注册中心客户端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

添加如下配置指定Nacos Server地址

spring:
  cloud:
    nacos:
      discovery:
       # nacos server地址
        server-addr: localhost:8848 

在启动类上加注解@EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.cskaoyan.user.mapper")
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

用户服务的其他代码不需要做任何改变

在订单服务中,添加如下依赖

<!--Nacos注册中心客户端-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

在订单服务中,添加如下配置

spring:
  cloud:
    nacos:
      discovery:
       # nacos server地址
        server-addr: localhost:8848
        

在启动类上加注解@EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.cskaoyan.order.mapper")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

做完以上工作之后,我们只需要分别启动用户服务和订单服务,查看Nacos Server

服务的发现

在服务成功注册到服务注册中心之后,我们如何动态的发现服务的信息,获取到服务运行所在的IP地址和端口号,从而根据服务的IP和端口号,利用RestTemplate发起服务调用请求呢,代码如下:

// 注入服务发现的客户端对象,通过该对象访问从注册中心下载的服务信息 
    @Autowired
    DiscoveryClient discoveryClient; 

    public Order queryOrderById(Long orderId) {
         // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        // 2. 调用用户服务
        // 2.1 获取指定服务名称的服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 2.2 选择服务实例,获取uri(格式为http://服务实例ip:服务实例启动端口)
        URI uri = instances.get(0).getUri();
	    // 2.3 向用户服务发起服务调用请求(注意,这里没有写死ip地址和端口号了!!!)
        ResponseEntity<String> response = template.getForEntity(uri.toString() + "/user/address/{userId}", String.class, order.getUserId());
        String result = response.getBody();
        return result;
    }

服务集群

其实,我们上面所说的运行一个服务,严格来说,运行的应该是一个服务实例,对应的是一个独立的JAVA进程。每个服务实例都有自己所对应的服务名称,多个服务实例运行相同的代码,且具有相同的服务名称,我们就称这些服务实例组成了一个服务集群。比如,我们可以把同一个user-service工程启动两次,那么我们就有了两个用户服务实例,它们组成了一个用户服务集群。

本来没什么可说的,但是在上课期间需要在同一台电脑上启动user-service工程启动两次,它们使用相同的代码,相同的配置文件,直接启动会出现端口冲突,所以我们需要学习在IDEA中如何实现该功能。

先修改第一个用户服务实例的启动脚本名称

紧接着,将第一个用户服务实例的启动脚本复制一份,作为第二个用户服务实例的启动脚本并修改启端口为9003

Eureka 注册中心(了解)

Eureka是SpringCloud Netflix中包含的注册中心的组件,Eureka也采用了C-S的架构设计,其中

  • EurekaServer作为服务器端,它具体就实现了服务注册中心的功能
  • EurekaClient作为客户端,帮助服务实例提完成服务的注册与自动发现

接下来,我们就基于Eureka服务注册中心,实现服务的注册与自动发现。

配置并启动EurekaServer

与nacos不同,我们需要新建一个子eureka-server工程,这个工程仅仅只是为了启动一个EurekaServer进程(注册中心)

在该工程中添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

在eureka-server子工程中的application.yml配置文件中添加如下配置

server:
  port: 7001
# Eureka配置
eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: localhost
  client:
    # 表示是否向 Eureka 注册中心注册自己(这个模块本身是服务器,所以不需要)
    register-with-eureka: false
    # fetch-registry如果为false,则表示自己为注册中心,客户端的化为 ture
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:7001/eureka/

启动类上添加注解@EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServer.class, args);
    }
}

一旦运行eureka-server工程,我们就可以通过 localhost:7001(这里的端口号就是我们server.port配置的端口)访问Eureka自带的控制台了

服务注册

在user-service的Maven工程中原有依赖的基础上,注释nacos-discovery依赖,添加如下依赖

        <!--Eureka Client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

添加如下配置

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: user-instance-1 
    hostname: localhost

主启动类添加注解@EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.cskaoyan.user.mapper")
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

在order-service的Maven工程中原有依赖的基础上,注释nacos-discovery依赖,添加如下依赖

      
<!--Eureka Client依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

添加如下配置

# Eureka配置
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: order-instance
    hostname: localhost

主启动类,添加注解

@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.cskaoyan.order.mapper")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

接下来,我们只需要分别启动用户服务实例和订单服务实例,即可观察到服务注册效果

服务发现

在服务消费者的OrderService中添加测试代码

// 注入服务发现的客户端对象,通过该对象访问从注册中心下载的服务信息 
    @Autowired
    DiscoveryClient discoveryClient; 

    public Order queryOrderById(Long orderId) {
         // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        // 2. 调用用户服务
        // 2.1 获取指定服务名称的服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 2.2 选择服务实例,获取uri(格式为http://服务实例ip:服务实例启动端口)
        URI uri = instances.get(0).getUri();
	    // 2.3 向用户服务发起服务调用请求(注意,这里没有写死ip地址和端口号了!!!)
        ResponseEntity<String> response = template.getForEntity(uri.toString() + "/user/address/{userId}", String.class, order.getUserId());
        String result = response.getBody();
        order.setAddress(result);
        return order;
    }

服务集群

在Eureka 中我们同样也可以有多个服务实例组成的服务集群,通过相同的方式,我们同样可以启动两个用户服务实例,并向Eureka注册中心注册,但是对于第二个服务实例,我们需要再启动脚本中,在添加一个instance-id

Eureka的自我保护机制

自我保护机制触发的场景如下:

  • 默认情况下,当eureka server在一定时间内没有收到服务实例的心跳,便会把该实例从注册表中删除(默认是90秒
  • 但是,如果短时间内丢失大量的服务实例心跳数据,这意味着短时间内大量的服务连接丢失了,此时就会触发Eureka的自我保护机制
  • 触发自我保护机制的结果就是,Eureka认为虽然收不到实例的心跳,但它认为实例还是健康的,eureka会保护这些实例,不会把它们从注册表中删掉。

那么Eureka的自我保护机制的意义在哪里呢?

  • 该保护机制的目的是避免网络连接故障,在发生网络故障时,微服务和注册中心之间无法正常通信,但服务本身是健康的
  • 此时,为了避免注册中心同时删除大量本来是正常运行(健康的)的服务实例,于是就会自动触发自我保护机制
  • 这样一来,即使注册中心和某个或者某些服务实例的网络出现问题,其他服务实例还是可以通过注册中心,获取到其地址,正常发起调用请求

但是我们在开发测试阶段,需要频繁地重启发布,如果触发了保护机制,则旧的服务实例没有被删除,这时服务消费者按照注册表中的服务提供者信息,发出服务调用请求,会因为该实例关闭而失败,这就导致请求错误,影响开发测试。

所以,在开发测试阶段,我们可以把自我保护模式关闭,只需在eureka server配置文件中加上如下配置即可

eureka:
  server:
    enable-self-preservation: false

服务调用的负载均衡

学习完了注册中心相关知识,在微服务架构中,我们已经可以实现服务的注册与自动发现了。但是再来看看我们的代码


        // 1 获取指定服务名称的服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 2 选择服务实例,获取uri(格式为http://服务实例ip:服务实例启动端口)
        URI uri = instances.get(0).getUri();
	    // 3 向用户服务发起服务调用请求(注意,这里没有写死ip地址和端口号了!!!)
        String address = template.getForObject(uri.toString() + "/user/address/{userId}", String.class, order.getUserId());

服务是可以有集群的,在发现了一个服务所有的实例之后,在一次服务调用过程中,我们还需要选择其中一个服务实例,发起调用请求,所以发起调用之前还存在着一个选择过程,这就涉及到了选择的策略问题,如何选择出集群中的一个实例呢?在SpringCloud中有一个有LoadBalancer和Ribbon帮我们完成这一选择过程。

LoadBalancer

LoadBalancer是SpringCloud自己实现的客户端负载均衡器。由于从Spring Cloud 2020.0.0 版本开始,Ribbon 正式进入维护模式,所以 Spring Cloud 逐步放弃Ribbon的使用,并且将 LoadBalancer 被推荐为主要的负载均衡解决方案,所以接下来我们在学习一下LoadBalancer。

基本使用

首先我们需要在项目中引入如下依赖:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

接下来,我们就可以开始使用了

@Configuration
public class RestTemplateConfig {

    @Bean
    // 只需要加上该注解,即可完成LoadBalancer的整合
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  // 查询订单数据
        Order order = orderMapper.findById(orderId);
		// 注意url中无需写ip:port,而变成了服务名称
        String url = "http://user-service/user/address/{id}";
        String address = restTemplate.getForObject(url, String.class, order.getUserId());

        order.setAddress(address);

        // 返回服务调用的结果
        return order;

我们发现,项目中整合了LoadBalancer之后,服务调用根据服务名称即可实现。

负载均衡策略

简单使用LoadBalancer之后,选择已经很好的实现了。同时,我们也会发现LoadBalancer默认实现的负载均衡策略是轮训策略(Round-Robin)。那么问题来了,LoadBalancer只能使用轮训策略吗? 当然不是,LoadBalancer本身还支持如下两种种常用的负载均衡策略:

策略实现类描述
轮训策略RoundRobinLoadBalancer轮训选择
随机策略RandomLoadBalancer随机选择

那么如何指定不同的负载均衡策略呢,通过配置类即可,可以分为两步:

第一步:定义所要使用的负载均衡类

public class CustomLoadBalancerConfiguration {

    /*
          environment:包含了一些配置信息
          loadBalancerClientFactory:创建LoadBalancerClient对象的工厂
     */
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {

        // 获取目标服务名称
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        // 创建用于获取目标服务实例列表的工厂对象
        ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSuppliers
                = loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class);
        // 创建随机负载均衡策略
        return new RandomLoadBalancer(serviceInstanceListSuppliers, name);
    }
}

第二步,通过@LoadBalancerClient注解使其生效,完成与RestTemplate的整合

@LoadBalancerClient(value = "user-service", configuration = CustomLoadBalancerConfiguration.class)
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
  • LoadBalancerClient注解中的 value值表示负载均衡策略所针对的服务名称,因为我们可以对不同的服务配置不同的负载均衡策略
  • configuration引入value所表示的服务的,所使用的负载均衡配置类

当然,上述的方式还可以使用另外一个注解完成,统一声明针对所有服务的负载均衡配置:

@LoadBalancerClients(
        @LoadBalancerClient(value = "user-service", configuration = CustomLoadBalancerConfiguration.class)
)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

当然,有的同学可能会问了,如果我想让针对所有服务的负载均衡都是用相同的负载均衡策略可以吗?当然可以

@LoadBalancerClients(
        defaultConfiguration = CustomLoadBalancerConfiguration.class
)
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

LoadBalancer缓存

本来我们每次在调用一个服务的时候,LoadBalancer都应该通过注册中心的客户端,获取当前被调用服务的服务实例列表然后选择。但是,唯一一定程度提高效率,LoadBalancer的默认实现在有一个自己的缓存,用来缓存服务实例列表。关于这个缓存,在实际生产环境开启是可以提升效率的,但是在我们讲课的时候,为了测试方便,我们需要关闭它。

spring:
  cloud:
    loadbalancer:
      cache:
        enabled: false

自定义负载均衡策略

当然,由于LoadBalancer本身提供的负载均衡策略比较少,所以实际开发中,有可能我们会根据需要自己实现自定义的负载均衡策略。

public class MyLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    // 服务名称
    final String serviceId;

    // 获取ServiceInstanceListSupplier对象的工厂
    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public MyLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }
	
    /*
        我们需要实现该方法,实现自己的自定义负载均衡策略
    */
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
       	
    }
}
public class MyLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    // 服务名称
    final String serviceId;

    // 获取ServiceInstanceListSupplier对象的工厂
    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public MyLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 得到获取服务实例列表的supplier对象
        ServiceInstanceListSupplier serviceInstanceListSupplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        //
        return serviceInstanceListSupplier
                // 获取目标服务实例列表
                .get(request)
                // 取出第一个列表(实际上)
                .next()
                // 将服务列表转化为一个服务实例(其实就是选择一个服务实例)
                .map(serviceInstances -> doChooseSingleServer(serviceInstances));
    }

    /*
         通过这个方法时实际完成服务实例的选择
     */
    private Response<ServiceInstance> doChooseSingleServer(List<ServiceInstance> serviceInstances) {
        
        // 选择策略简单粗暴,选择列表中的第一个服务实例
        ServiceInstance serviceInstance = serviceInstances.get(0);

        // 将选择结果封装到DefaultResponse中返回
        return new DefaultResponse(serviceInstance);
    }

}

Ribbon负载均衡(知道)

Ribbon也是是一个客户端负载均衡器,能够给HTTP客户端带来灵活的控制。在SpringCloud2020之前的版本中默认使用的是Ribbon,所以我们也需要知道。它所支持的负载均衡策略如下:

策略实现类描述
随机策略RandomRule随机选择server
轮训策略RoundRobinRule轮询选择
重试策略RetryRule对选定的负载均衡策略(轮训)之上的重试机制,在一个配置时间段内当选择服务不成功,则一直尝试使用该策略选择一个可用的服务;
最低并发策略BestAvailableRule逐个考察服务,如果服务断路器打开,则忽略,再选择其中并发连接最低的服务
可用过滤策略AvailabilityFilteringRule过滤掉因一直失败并被标记为circuit tripped的服务,过滤掉那些高并发链接的服务(active connections超过配置的阈值)
响应时间加权重策略WeightedResponseTimeRule根据server的响应时间分配权重,响应时间越长,权重越低,被选择到的概率也就越低。响应时间越短,权重越高,被选中的概率越高,这个策略很贴切,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间
区域权重策略ZoneAvoidanceRule综合判断服务所在区域的性能,和服务的压力,轮询选择server并且判断一个AWS Zone的运行性能是否可用,剔除不可用的Zone中的所有server

RestTemplate整合Ribbon

因为在SpringCloud2020之前,nacos客户端已经自己整合了ribbon依赖,所以实际上我们并不需要去添加该依赖就可以使用Ribbon了,仍然是使用@LoadBalance注解

@Configuration
public class ClientConfig {

    @Bean
    @LoadBalanced
    public RestTemplate template() {
        return new RestTemplate();
    }
}

然后在使用RestTemplate发起调用的时候,直接使用服务名进行调用即可

        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate发起http请求,查询用户
        // 2.1.url路径
        String url = "http://user-service/user/address/{userId}";
        // 2.2.发送http请求,实现远程调用
        String userAddress = restTemplate.getForObject(url, String.class, order.getUserId());
        // 3.封装user到Order
        order.setAddress(userAddress);
        // 4.返回
        return order;

指定Ribbon负载均衡策略

和Spring Cloud LoadBalancer的使用方式类似

  • Ribbon也可以指定针对某个服务或所有服务使用的负载均衡策略
  • 我们也可以实现Ribbon的自定义负载均衡策略

声明式接口调用

现在我们的服务调用过程,又变得简单了一些,因为Ribbon帮助我们解决了,服务调用过程中的选择问题。再来看一下我们的服务调用代码

        // 2.1.url路径
        String url = "http://user-service/user/address/{userId}";
        // 2.2.发送http请求,实现远程调用
        String userAddress = restTemplate.getForObject(url, String.class, order.getUserId());

我们觉得以上的代码还是不够简洁,如果我们希望对于服务(服务中的Controller方法)调用,就像对普通方法一样的简单?就类似下面的代码一样:

public interface RemoteUserService {

    @GetMapping("/user/address/{id}")
    public String queryById(@PathVariable("id") Long id);
}
// 就像调用普通方法一样,调用到用户服务中的方法
String userAddress = remoteUserService.queryById(1);

OpenFeign就可以帮助我们实现这样的功能,进一步简化服务调用的代码。

OpenFeign 客户端

OpenFeign是一个实现Java代码和Http客户端绑定的绑定器,通俗的来解释,它可以帮助我们以统一的方式,将接口”翻译”成Restful风格的请求。

OpenFeign的使用

因为OpenFeign本身,充当着一个“翻译”的角色,可以将我们的Java接口翻译为对应的Http APIs,所以对于我们来说,OpenFeign也可以理解为一种服务调用的客户端,正因为是服务调用的客户端,所以只在服务消费者一端使用。

简单参数(返回值)

虽然,OpenFeign本身仅仅只是在客户端使用,但是因为使用了OpenFeign意味着服务的调用是面向Java接口的,而非HTTP API的,调用方式发生了改变。所以我们需要给order-service添加OpenFeign依赖

添加如下依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

定义用来调用用户服务的OpenFeign接口

// 接口需要加FeignClient指定该接口中的方法,所调用的目标服务
@FeignClient("user-service")
public interface UserFeignClient {

    /* 接口方法上的注解以及方法参数主机,用来表示调用该方法时
        所发起的http请求的pah路径,以及参数
     */
    @GetMapping("user/address/{id}")
    public String queryById(@PathVariable("id") Long id);
}

在启动类上加注解@EnableFeignClients,才能让我们定义的FeignClient生效

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.cskaoyan.order.mapper")
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

以上的OpenFeign的依赖,接口定义,以及生效注解都准备好了,我们就可以开始使用OpenFeign了,在订单服务中,我们的服务调用代码可以修改如下:

        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.利用OpenFeign接口发起http请求,查询用户
        String userAddress = userFeignClient.queryById(order.getUserId());
        // 3.封装user到Order
        order.setAddress(userAddress);
        // 4.返回
        return order;

注意事项

  • OpenFeign接口中定义的方法名,不需要和被调用的,目标服务的Controller方法的方法名相同,只要请求的path部分以及参数对应即可
  • 如果需要传递请求参数,比如http://ip:port/path?arg1=value1&arg1=value2,此时我们需要给OpenFeign接口的方法参数加上@RequestParam注解
@FeignClient("test-service")
public interface TestArgFeignClient {

    @GetMapping("test/arg")
    public String testOpenFeignArg(@RequestParam("arg1") Long arg1,
                                   @RequestParam("arg2") Long arg2);
}

对象参数(返回值)

刚才我们测试了,简单参数(Jdk中本身就有的数据类型),现在我们来看看另一种情况,当我们被调用的目标服务方法需要返回(或接收)一个自定义对象User的时候,实现如下:

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/address/{id}")
    public User queryObjectById(@PathVariable("id") Long id) {
        // 根据id查询用户的地址信息
        return userService.queryObjectById(id);
    }
}

此时,在订单服务中,我们的OpenFeign接口方法也就要做出相应的改变

@FeignClient("user-service")
public interface UserFeignClient {

    @GetMapping("user/address/{id}")
    public User queryObjectById(@PathVariable("id") Long id);
}

但是,真的这么简单就可以了吗?当然不是

我们很明显的看到报错了,原因是Order-Service和User-Service是两个独立的Maven工程,因此Order-Service并不认识在User-Service中定义的User类,那怎么解决这个问题呢?

  • 让Order-Service也认识User-Service中定义的即可
  • 因此我们在添加一个Maven工程,在该工程中,定义User,在让Order-Service和User-Service都依赖这个Maven工程即可

同时,分别在Order-Service和UserService中添加User-service-api的依赖即可

这里要注意,为什么我们不直接让Order-Service直接依赖UserService,这样不是也可以让Order-Service认识在User-Service中定义的User类吗?

  • 因为一旦Order-Service依赖了User-Service那么OrderService在打包的时候,就会把User-Service的代码直接打包在一起
  • 两个服务打成一个jar包,一旦运行起来,两个服务的代码运行在同一个进程中,这违反了微服务的理念

FeignClient日志输出

当我们调用FeignClient发出请求的时候,如果我们希望能看到其发出的具体Http请求,我们可以通过配置来实现。

# 这里的xxx表示我们自己的定义的FeignClient所在包的包名(比如: com.cskaoyan.feign.consumer.api)
logging:
  level:
    xxx: debug

定义配置类,在配置类中,配置Feign的日志输出级别

@Configuration
public class FeignConfig {

    @Bean
    public Logger.Level logLevel() {
        return Logger.Level.FULL;
    }
}

这样当我们,通过在对应的FeignClient对象上,调动方法,发起http请求的时候,对应的请求就会打印在控制体

服务调用的超时设置

通常,一次远程调用过程中,服务消费者不可能无限制的等待服务提供者返回的结果,正常情况下,服务提供者的一次调用执行过程也不会执行很长时间(除非出现网络故障,或者服务提供者宕机等问题),所以为防止,在非正常情况下服务消费者在调用过程中的长时间阻塞等待,对于一次服务调用过程,我们会设置其超时时间。一次服务调用,超时未返回即认为调用失败。在使用Feign的时候,我们可以配置其超时时间。默认超时时间是60s

spring:
  cloud:
    openfeign:
      client:
        config:
         # default表示设置对所有服务设置调用超时时间的,如果想要设置某个服务的,将default改为对应服务名即可
          default:
            connectTimeout: 5000 # 连接超时时间
            readTimeout: 5000 # 调用超时时间

配置中心

设想一下,如果每个服务都有自己的配置,比如服务访问的数据库地址等,但是某一天,数据库部署的服务器地址变了,此时会发生什么呢?为了让服务能够正确访问到数据库,我们得停止每一个服务,重新修改每一个服务的配置文件,然后在重新启动每个服务,在这个过程中就会出现两个问题:

  • 修改配置文件的工作繁琐,工作量大,尤其当服务数量较多的时候
  • 要让新的配置生效,得重启服务

如果要解决以上问题,那么在我们微服务架构的项目中,我们就得引入一个新的角色——配置中心来解决这个问题了,类似于注册中心,配置中心的实现也有多种,而Nacos同时也实现了配置中心的角色。

  • 使用配置中心可以让您以<span style=’color:red;background:yellow;font-size:文字大小;font-family:字体;’>中心化、外部化和动态化</span>(动态化即可以实时刷新配置)的方式管理所有环境的应用配置和服务配置。
  • 动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

Nacos 配置中心

Nacos除了可以作为服务注册中心之外,还可以实现服务配置中心的功能。

在使用Nacos配置中心之前,我们必须对于注册中心的配置信息有一个清楚的认识:

  • 配置中心中的配置,主要是以键值对的形式存在的,即每条配置都以key-value的形式存储,key是配置的名称,value才是配置的值
  • 所以,很明显,不同配置的key值应该有所区别,或者即使key值相同,我们也应该有办法区分他们,即给key值划分不同的维度。

所以接下来,我们得介绍一下Nacos中定义的基本概念:

  • 配置项: 一个具体的可配置的参数与其值域,通常以 param-key=param-value 的形式存在。例如我们常配置系统的日志输出级别(logLevel=INFO|WARN|ERROR) 就是一个配置项。
  • 配置集:一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置,每一个配置集都对应一个唯一的DataId,DataId必须由我们自己定义。
  • 配置分组: Nacos 中的一组配置集,是组织配置的维度之一,每一个分组都有一个唯一的组名,如果我们未定义,则默认使用DEFAULT-GROUP分组
  • 命名空间: 用于进行用户粒度的配置隔离,每一个命名空间都有一个唯一的Id值,如果我们未定义,则默认使用public命名空间

以上几个概念其实就是在告诉我们区分不同配置项的维度,Nacos提供多个维度帮助我们区分不同的配置,它们的关系如下图所示

有了以上不同的配置项的划分维度,我们就可以灵活定义我们的配置项了。其中

  • 配置项中的key值,以及配置分组的组名都由我们自己根据场景去定义
  • 命名空间的Id值,在我们定义命名空间的时候,由Nacos帮我们生成

Nacos配置中心的使用

服务配置

在用户服务中添加如下,访问nacos配置中心的依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

每个服务都可以在配置中心中管理每个服务自己的配置(Profile配置)。可以指定每个服务所要读取的配置集名称

spring:
  cloud:
    nacos:
      config:
		# 配置中心地址
        server-addr: 192.168.153.130:8848
        # 配置集的文件格式
        file-extension: yml
  config:
    import:
      - nacos:配置集名称
      - nacos:配置集名称?group=MY_GROUP # 覆盖默认上面的group, 读取MY_GROUP的配置集      

虽然这里的配置集id的定义可以是任意字符串,但是推荐大家使用如下公式生成目标配置集id即dataId

${spring.application.name}-${spring.profiles.active}.yml
  • spring.profiles.active 即为当前环境对应的 profile
  • 最后的yml在说明配置文件的格式

这样一来,类比于上一个阶段我们在项目中通过修改spring.profiles.active配置值,我们就可以在配置中心实现多环境配置了。当然,除此之外,我们还可以通过制定namespace和group实现环境配置

spring:
  application:
  name: xxx
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        #namespace: xxx
        #group: xxx
        server-addr: 127.0.0.1:8848
        file-extension: yml
  config:
    import:
      - nacos:配置集名称
      - nacos:配置集名称?group=MY_GROUP # 覆盖默认上面的group, 读取MY_GROUP的配置集

在nacos-config的目录下添加application.yml,在application.yml配置文件中的配置如下

# 项目中的其他配置都包含在application.yml文件中
server:
  port: 3377
spring:
  application:
    name: nacos-config-client
  profiles:
    active: dev # 表示开发环境
  cloud:
    nacos:
      config:
        #Nacos作为配置中心地址
        server-addr: localhost:8848 
        #指定yaml格式的配置,如果是yml文件
        file-extension: yml 
        # 指定配置所属的配置分组
        # group: DEV_GROUP
        # 指定配置所属的命名空间
        # namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4
  config:
    import:
      - nacos:配置集名称
      - nacos:配置集名称?group=MY_GROUP # 覆盖默认上面的group, 读取MY_GROUP的配置集

测试代码如下:


@RestController
public class ConfigController {

    @Value("${nacos.config}")
    String config;


    @GetMapping("/nacos/config")
    public String nacosCofnig() {
        return config;
    }
}

服务配置的动态刷新

配置存储在配置中心之后,如果配置值发生了改变,是否必须重启服务才能让配置生效呢?当然不是,基于Nacos配置中心,我们可以实现配置的动态刷新。只需在需要使用配置值的类上加上@RefreshScope注解即可

@RestController
// 在使用配置的类上加该注解才能实现配置的动态刷新
@RefreshScope
public class UserController {

    @Value("${nacos.config}")
    String config;


    @GetMapping("/nacos/config")
    public String nacosCofnig() {
        return config;
    }
}

注意:

  • 配置中心的配置优先级高于本地配置

Nacos 配置的持久化

我们在Nacos服务器上写入的配置,会被持久化保存到Nacos自带的一个嵌入式数据库derby中,因此当我们重启Nacos之后,仍然可以看到之前的配置信息。但是,使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况,因此,Nacos还支持将配置信息写入Mysql中:

  • 在数据库中,创建名为nacos的数据库
  • 在nacos数据库中,执行数据库初始化文件:nacos-mysql.sql(改文件在conf目录下已经提供)
  • .修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql
db.num=1
# 这里的url要改成你自己的mysql数据库地址,并在你的mysql中创建名为nacos的数据库
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
# 这里要改成你自己登录mysql的用户名和密码
db.user.0=nacos_devtest
db.password.0=youdontknow

在配置了mysql数据库之后,我们会发现,之前配置中心的配置信息全部消失了,那是因为我们之前使用的是nacos的内嵌数据库derby,现在切换到mysql之后数据存储在nacos这个数据库中,而该数据库现在是没有数据的。

但是当我们,在nacos的控制台重新添加配置数据之后,我们就可以在mysql中看到了

网关

如果没有网关,难道不行吗?功能上是可以的,我们直接调用提供的接口就可以了。那为什么还需要网关?

因为网关的作用不仅仅是转发请求而已。我们可以试想一下,如果需要做一个请求认证功能,我们可以接入到 API 服务中。但是倘若后续又有服务需要接入,我们又需要重复接入。这样我们不 仅代码要重复编写,而且后期也不利于维护。

由于接入网关后,网关将转发请求。所以在这一层做请求认证,天然合适。这样这需要编写一次代码,在这一层过滤完毕,再转发给下面的 API。

所以 API 网关的通常作用是完成一些通用的功能,如请求认证,请求记录,请求限流,黑白名单判断等。

API网关是一个服务器,是系统的唯一入口。

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关提供REST/HTTP的访问API。

多入口

网关使用

Spring Cloud Gateway是一个基于Spring生态的API网关,基于WebFlux框架实现。它旨在以简单高效的方式实现,请求路由,以及一些其他的边缘功能,比如,安全,监控等功能。

通用的网关框架除了Gateway之外,还有Zuul,Zuul2等框架。其中,Zuul是由Netflix公司开发出的最早的通用网关框架,功能丰富,但是基于同步阻塞式Servlet API实现,性能不佳。Zuul2可以理解为Zuul的升级版,它基于异步非阻塞模式实现,但是由于zuul2在开发过程中一直延期,所以Spring Coud官方并未采用Zuul2最为自己的通用网关,而是自己推出了自己的基于异步非阻塞实现的第二代服务网关Gateway。

核心概念

  • Predicate:表示路由规则的匹配条件
  • Filter:过滤器,在请求处理之前(Pre)实现队请求的拦截处理,在请求处理之后(Post)实现对响应的拦截处理
  • Route:定义请求和路由目标之间的映射,它由一个唯一ID(自定义),一个目标地址URI,表示路由条件Predicate集合,以及一个Filter集合组成。对于一个请求而言,如果它满足一个路由的全部路由条件(Predicate),那么该请求就会按照该路由(Route)规则,向目标地址URI转发。
spring:
  cloud:
    gateway:
     #定义多个路由
      routes:
      # 一个路由route的id
      - id:  test_route
        # 该路由转发的目标URI
        uri: https://example.org
        # 路由条件集合
        # /red/aaa
        predicates:
        - Path=/red/**
        # 过滤器集合
        filters:
        - AddRequestParameter=color, red

我们再来看一看Gateway是如何工作的

  • 客户端向Gateway发起请求
  • Gateway的Handler Mapping组件,会对请求做路由匹配,如果请求和某个路由规则匹配,则把该请求交给Web Handler处理
  • 在将请求转发给目标之前,Web Handler会将请求,交给满足请求过滤条件的一系列过滤器,即一个过滤器链对该请求进行过滤处理
  • 过滤器链,被虚线分隔,是因为过滤器既可以在转发请求前拦截请求,也可以在请求处理之后对响应进行拦截处理。

网关路由配置

SpringCloud Gateway的网关配置有两种方式,配置文件配置和代码配置两种方式。

在使用Gateway之前,我们得单独创建一个Maven工程,引入Gateway依赖,之后将其独立启动。需要的依赖如下

<dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
          </dependency>
        <!--loadbalancer依赖-->
        </dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <!--单元测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

配置文件配置

假设现在我们在基于Gateway的网关中,添加路由配置,实现如下功能:

  • 将URI匹配/routeconfig/rest/**的请求,转发给某一个服务或应用(将请求路由到服务或应用)
  • 将URI匹配/guonei/**的请求,转发给另一个服务或应用(将请求路由到服务或应用)
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
         #路由的ID,没有固定规则但要求唯一,建议配合服务名
        - id: config_route 
          #匹配后提供服务的路由地址
          uri: http://localhost:8002
           # 断言,路径相匹配的条件
          predicates:
            - Path=/routeconfig/rest/**      
		#路由的ID,没有固定规则但要求唯一,建议配合服务名
        - id: config_news 
          #匹配后提供服务的路由地址
          uri: http://news.baidu.com
          # 断言,路径相匹配的进行路由
          predicates:
            - Path=/guonei/**      

代码配置

@Configuration
public class GateWayConfig
{
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)
    {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
		
        // route方法的第一个参数表示新建的routeID
        // 第二个参数表示Route对象对应的的Builder对象
        routes.route("code_route_config",
                r -> r.path("/guonei")
                        .uri("http://news.baidu.com")).build();

        return routes.build();
    }
}

动态路由

网关接收外部请求,按照一定的规则,将请求转发给其他服务或者应用。如果站在服务调用的角度,网关就扮演着服务消费者的角色,此时,如果我们再来看看服务调用的目标URI配置,就会很自然的发现一个问题,服务提供者调用的地址是写死的,即网关没有动态的发现服务,这就有涉及到了我们之前解决过的服务的自动发现问题,以及发现服务后,所涉及到的服务调用的负载均衡的问题。

回忆一下,我们之前是如何解决这些问题的?通过Nacos或者Eureka注册中心动态发现服务,通过Ribbon进行服务调用的负载均衡。同样,Gateway也可以整合Nacos或者Eureka,Ribbon或LoadBalancer从而实现,动态路由的功能。

想要使用动态路由的功能,首先我们要整合注册中心,这里我们以Nacos为例

        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

然后在配置文件中,添加注册中心的配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

还要修改路由配置,使用动态路由

spring:
  application:
    name: cloud-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:
         #路由的ID,没有固定规则但要求唯一,建议配合服务名
        - id: config_route 
          #匹配后提供服务的路由地址, 这里lb之后,跟的是要调用的服务名称
          uri: lb://nacos-provider-8002
           # 断言,路径相匹配的条件
          predicates:
            - Path=/routeconfig/rest/**      

此时,当id为config_route的路由规则匹配某个请求后,在调用该请求对应的服务时,就会从nacos注册中心自动发现服务,并在服务调用的时候实现负载均衡。

Predicate

在Gateway中,有一些的内置Predicate Factory,有了这些Pridicate Factory,在运行时,Gateway会自动根据需要创建其对应的Predicate对象测试路由条件。具体的有兴趣的话大家可以去查看官网: https://spring.io/projects/spring-cloud-gateway

  • Path 路由断言 Factory: 根据请求路径匹配的路由条件工厂
spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: https://example.org
        predicates:
        # 如果可以匹配的PathPattern有多个,则每个路径模式以,分开
        - Path=/red/{segment},/blue/{segment}
  • After 路由断言 Factory:在指定日期时间之后发生的请求都将被匹配
spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: http://example.org
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  • Cookie 路由断言 Factory

Cookie 路由断言 Factory有两个参数,cookie名称和正则表达式。请求包含次cookie名称且正则表达式为真的将会被匹配。

  spring:
    cloud:
      gateway:
        routes:
        - id: cookie_route
          uri: http://example.org
          predicates:
          - Cookie=chocolate, ch.p
  • Header 路由断言 Factory

Header 路由断言 Factory有两个参数,header名称和正则表达式。请求包含次header名称且正则表达式为真的将会被匹配。

  spring:
   cloud:
     gateway:
       routes:
       - id: header_route
         uri: http://example.org
         predicates:
         - Header=X-Request-Id, \d+
  • Host 路由断言 Factory

Host 路由断言 Factory包括一个参数:host name列表。使用Ant路径匹配规则,.作为分隔符。

  spring:
    cloud:
      gateway:
        routes:
        - id: host_route
          uri: http://example.org
          predicates:
          - Host=**.somehost.org,**.anotherhost.org
  • Method 路由断言 Factory

Method 路由断言 Factory只包含一个参数: 需要匹配的HTTP请求方式

  spring:
    cloud:
      gateway:
        routes:
        - id: method_route
          uri: http://example.org
          predicates:
          - Method=GET

所有GET请求都将被路由

Filter

Gateway内置了很多的Filter这里就不再一一列举了。我们重点来学习下自定义Filter。

@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, HttpCookie> cookies = request.getCookies();
        List<HttpCookie> tokens = cookies.get("access_token");
        if (tokens == null || tokens.size() == 0) {
            throw new RuntimeException("少了cookie!");
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
       return 0;
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇