Spring Cloud知识点全总结

2022-11-22 08:48:53 浏览数 (1)

这一节系统地学习一下Spring Cloud这个微服务框架。本文篇幅很长,并且知识点讲解全面详细,我截稿时已经有6万字了,建议大家收藏后慢慢学习。看在我这么辛苦整理知识点的份上,大家可以点个关注和点赞吗?谢谢大家,这对我真的很重要!

  • 一.Spring Cloud 框架概述
    • 1.基本介绍
    • 2.Spring Cloud与Spring Boot的版本兼容
  • 二.Spring Cloud 框架入门使用
    • 1.服务拆分
    • 2.案例准备
    • 3.微服务远程调用
    • 4.服务的提供者与消费者
  • 三. Eureka注册中心
  • 1.Eureka原理分析
    • 2.搭建eureka服务
    • 3.服务注册
    • 4.服务发现
  • 四.Ribbon负载均衡
    • 1.负载均衡的原理
    • 2.负载均衡策略
    • 3.自定义负载均衡策略
    • 4.饥饿加载
  • 五. Nacos注册中心
    • 1.认识Nacos
    • 2.Windows下安装Nacos
    • 3.Linux下安装Nacos
    • 4. 服务注册到nacos
    • 5. 服务分级存储模型
    • 6.配置服务集群属性
    • 7.NacosRule负载均衡
    • 8.服务实例的权重设置
    • 9.环境隔离
    • 10.创建namespace环境隔离
    • 11.给微服务配置namespace
    • 12.Nacos和Eureka的区别
  • 六. Nacos配置管理
  • 1.统一配置管理
    • 2.微服务拉取配置
    • 3.配置热更新
    • 4.多环境配置共享
    • 5.搭建Nacos集群
  • 七. Feign远程调用
  • 1. RestTemplate存在的问题
    • 2. Feign替代RestTemplate
    • 2. 自定义配置Feign
    • 3. Feign使用优化
    • 4. Feign最佳实践的分析
    • 5. Feign 最佳实践的代码实现
  • 八.Gateway服务网关
  • 1. 为什么需要网关
    • 2. Gateway快速入门
    • 3.路由断言工厂
    • 4.路由过滤器的配置
    • 5. 全局过滤器
    • 6. 过滤器的执行顺序
    • 7. 网关的跨域配置

一.Spring Cloud 框架概述

1.基本介绍

SpringCloud是目前国内使用最广泛的微服务框架之一。官网地址:https://spring.io/projects/spring-cloud。 SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。 如下图:

Spring Boot最擅长的事情就是自动装配,而Spring Cloud就是把那些官方原生开源的一些组件给整合进来了,并且基于Spring Boot做了自动装配。那你只要拿过来就能用,而无需复杂的配置。之后我会逐一地去学习这些组件。

2.Spring Cloud与Spring Boot的版本兼容

下图就是我刚刚在官网截的一张版本兼容的示意图,也可以到官网自己去查看:

左边的每一个Spring Cloud版本,右边都有一个对应的Spring Boot版本。比如说,你们公司用了Spring Cloud的Greenwich版本,那你的Spring Boot就必须是2.1.x的版本。如果你用了其他的版本,你也要选择右边对应的Spring Boot版本号。如果使用的版本不一致,将来运行时可能会报错。

我接下来学习的使用的版本是Hoxton的版本,所以使用的Spring Boot版本就要用2.2.x或者时2.3.x的版本。

二.Spring Cloud 框架入门使用

前言:

从这里开始,会理论与操作相结合来学习Spring Cloud框架。我本篇用到的案例代码我会上传到CSDN的资源中去,我已经开启了免费下载,欢迎大家下载学习。

1.服务拆分

先来了解一下服务拆分的细节和注意事项,服务拆分说起来很简单。一个单体架构,我们按照功能模块进行拆分,变成多个服务就行了。比如下图的4个模块,我们就拆分为4个服务。当然。我们在实际生产中,单个模块的功能可能会越来越多,我们还会继续拆分。

但是单体应用开发的多了,很多人可能会产生一种思维定势,容易犯一些错。这里总结一下:比如说,我现在有一个需求,是查询订单,同时把订单里面关联的用户信息,商品信息都给它查出来。如果是以前的开发模式,我们肯定是写一个方法去查询订单,在订单的查询过程中得到了用户Id,然后去数据库里面把用户查出来,得到了商品Id我再去把商品查出来。那么这个功能全部写到了订单的模块里面。这种写法是完全违背了我们的微服务的原则的。微服务拆分的目的就是单一职责,只做与自己相关的事情。订单模块就做订单业务,就不要去做用户查询等非订单模块的操作。我们的每一个微服务都不能去开发重复的业务,如果在你的微服务中出现了重复的业务,这就证明你的某些地方可能做得有问题。

为了做好这些,我们还会有一些要求。比如说我们微服务的数据要独立,一个微服务不要访问其他的微服务的数据库 。每个微服务都会有自己的数据库,用户功能的数据库里就存放的是用户相关的信息,别的都不存。订单模块的数据库里面存放的自然就是订单信息。这个时候你在做订单相关的业务时,如果要查用户信息,它自己的数据库里面没有。这就降低了业务的耦合。

微服务在拆分的时候还要注意一些事情,就是微服务可以将自己的业务暴露为接口,供其他的微服务使用。比方说我的用户有用户查询功能,如果订单需要,那我就暴露成一个接口,需要的时候发请求就可以访问其他微服务了。

2.案例准备

下面我们就通过一个案例来具体学习:

大家提前在下载的资料里找到cloud-demo(资料文件夹里面14k大小的那个压缩包,不是代码文件夹的那个完整版哦!),解压后会得到一个cloud-demo的项目。我们把它用idea打开就行了。

下面我先来介绍一下这个项目的结构:

我们这个项目会有上图的结构,父工程叫做cloud-demo,负责管理整个项目的依赖,下面有两个模块,分别是order-service和user-service。order-service里面做订单相关的内容(比如根据id查询订单),user-service里做用户相关的功能(比如根据id查询用户),这两个模块就是将来我们的两个微服务。并且我还为这两个服务准备了各自的表。(将来我们生产环境时,一定会把他们部署到不同的数据库服务器里面,只是这里我们学习,就在同一台数据库里了。)

大家的资源里面会有如下的俩个sql文件:

下面我们先来做一些项目开始的准备工作:

(1)随便打开一个你安装的数据库可视化工具,然后分别创建两个数据库cloud_order和cloud_user。 (2)两个数据库分别运行导入上面的两个sql,把表导入进去。

我们来看一下这个cloud_demo父工程的pom.xml文件。

代码语言:javascript复制
<?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>com.haiexijun.demo</groupId>
    <artifactId>cloud-demo</artifactId>
    <version>1.0</version>
    <modules>
        <module>user-service</module>
        <module>order-service</module>
    </modules>

    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <mysql.version>8.0.25</mysql.version>
        <mybatis.version>2.1.1</mybatis.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--mybatis-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

这里有两点要注意的就是你拿到项目之后要根据自己电脑里面装的java版本和mysql的版本在pom.xml文件里面的properties里更改对应的版本号。我的电脑的java版本时jdk11,mysql的版本是8.0.25。

下面是子工程里的pom.xml ,我这里也列在下面给大家看看:

user-service子项目:

代码语言:javascript复制
<?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>cloud-demo</artifactId>
        <groupId>com.haiexijun.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

order-service子项目:

代码语言:javascript复制
<?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>cloud-demo</artifactId>
        <groupId>com.haiexijun.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>order-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

然后我们来看一下这俩个子项目的具体内容:

先从来看user-service的yml配置文件:

代码语言:javascript复制
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: zc20020106
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package: com.haiexijun.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.haiexijun: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

启动类上要加上@MapperScan注解(日常mybatis研发,需要在每个interface配置@Mapper,为了开发简便使用@MapperScan可以指定要扫描的Mapper类的包的路径)

代码语言:javascript复制
package com.haiexijun.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.mybatis.spring.annotation.MapperScan;

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

然后下面是web包下的controller类的相关代码:

代码语言:javascript复制
package com.haiexijun.user.web;

import com.haiexijun.user.pojo.User;
import com.haiexijun.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private UserService userService;

    /**
     * 路径: /user/110
     *
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

service层的代码:

代码语言:javascript复制
package com.haiexijun.user.service;

import com.haiexijun.user.mapper.UserMapper;
import com.haiexijun.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) {
        return userMapper.findById(id);
    }
}

pojo和mapper:

代码语言:javascript复制
package com.haiexijun.user.pojo;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String address;
}
代码语言:javascript复制
package com.haiexijun.user.mapper;

import com.haiexijun.user.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper {
    
    @Select("select * from tb_user where id = #{id}")
    User findById(@Param("id") Long id);
}

下面就简单列一下order-service子项目的代码:

代码语言:javascript复制
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: zc20020106
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package: com.haiexijun.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.haiexijun: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

controller层:

代码语言:javascript复制
package com.haiexijun.order.web;

import com.haiexijun.order.pojo.Order;
import com.haiexijun.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@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层:

代码语言:javascript复制
package com.haiexijun.order.service;

import com.haiexijun.order.mapper.OrderMapper;
import com.haiexijun.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 4.返回
        return order;
    }
}

pojo和mapper:

代码语言:javascript复制
package com.haiexijun.order.pojo;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String address;
}
代码语言:javascript复制
package com.haiexijun.order.pojo;

import lombok.Data;

@Data
public class Order {
    private Long id;
    private Long price;
    private String name;
    private Integer num;
    private Long userId;
    private User user;
}
代码语言:javascript复制
package com.haiexijun.order.mapper;

import com.haiexijun.order.pojo.Order;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {

    @Select("select * from tb_order where id = #{id}")
    Order findById(Long id);
}

我们分别启动这两个项目(一定要都运行起来),分别访问不同的端口,进行数据访问:

到这来,我们就完成了项目的准备工作。同时实现了服务拆分。下一节就来学习如何做远程调用。

3.微服务远程调用

在学习之前先来引出案例需求: 我们要实现根据订单Id查询订单的同时,把订单所属的用户信息一起返回。

现在我们的订单服务还不能做到这一点,而且我们不能直接通过访问用户服务的数据库获得信息,要去访问用户服务获取才对。

但是问题来了,我们以前没有学过从一个服务到另外一个服务的远程调用。下面就来分析一下,如何进行远程调用。

远程调用方式分析: 我们的user服务通过@GetMapping(“/user/{id}”)对外暴露了一个restful的接口,只要我们在浏览器里面输入对应的地址,就可以拿到用户信息。我们的order订单服务如果也能像浏览器一样发起一个http的请求,用户服务也应该返回一个对应的信息给我们。这时候,订单模块再结合本地数据库查询出来的订单信息,就组合出了最终的目标了。

所以我们的问题就变成了如何在Java代码当中发起HTTP请求,如果能发起HTTP请求,就可以调用其他服务的restful接口了。

Spring它提供了一个工具叫做RestTemplate ,这个工具就是Spring提供给我们来发HTTP请求的。我们要使用这个工具,就要先在配置类里面注册,而启动类就是一个配置类。所以我们可以在order-service的OrderApplication中通过@Bean注解注册RestTemplate。

代码语言:javascript复制
package com.haiexijun.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.haiexijun.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

    /**
     * 创建RestTemplate,并注入Spring容器
     * @return
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

完成了这一步以后,我们接下来就可以利用它来发HTTP请求了。

所以我们下面要对订单的查询业务进行修改:

代码语言:javascript复制
package com.haiexijun.order.service;

import com.haiexijun.order.mapper.OrderMapper;
import com.haiexijun.order.pojo.Order;
import com.haiexijun.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@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请求,查询用户
        String url="http://localhost:8081/user/" order.getUserId();
        User user= restTemplate.getForObject(url, User.class);
        // 3.封装user 到 order里面去
        order.setUser(user);
        // 4.返回
        return order;
    }
}

我们重启项目后,下面来运行两条查询订单试试:

到这里,我们就实现了,跨服务的远程调用。

4.服务的提供者与消费者

这一节又是讲解概念的一节,会了解什么是服务的提供者与消费者。

服务提供者:一次业务中,被其他微服务调用的服务就称之为服务的提供者。(提供接口给其他微服务)

服务消费者:一次业务中,调用其他微服务的服务就称之为服务的消费者。(调用其他微服务提供的接口)

我们上一节的案例中,user-service是服务提供者提供者,而order-service是服务消费者。

现在就会引发出一个问题了,现在服务A调用了服务B,而服务B调用了服务C,那服务B是什么角色呢?这时,比相对与A而言是提供者,而相对于C而言是消费者。所以一个服务既可以是提供者,又可以是消费者。

三. Eureka注册中心

在这一节里,我们会聊一聊之前案例里面存在的一些问题,以及Eureka如何解决这些问题,然后会介绍Eureka具体如何去使用。

1.Eureka原理分析

先来回顾一下之前的案例,在之前的案例当中,我们有一个订单服务和用户服务。订单服务需要远程调用我们的用户服务,它采用的方式是发起一次HTTP请求。不过在我们的代码当中,我们是将user-service的IP和端口硬编码在代码当中的。

但这样的写法是存在一定的问题的。比如说我们开发的时候,我们会分开发环境、测试环境和生产环境等等等。每一次环境的变更,可能服务的地址也会发生改变。如果你采用硬编码的方式写死了,难道每一次都要重新修改代码然后编译打包吗?而且,为了应对更高的并发,我们的user服务可能会部署成多实例,形成一个集群,端口和地址可能就不一样了。这时候,我们到底改写谁的地址呢?如果选择其中一个实例,那其他几个的意义岂不是就没有了吗?

所以这里一定不能采用硬编码的方式。那问题也来了,如果我不做硬编码,那这三个服务的地址我该如何去获取呢?而且万一以后又有第四台和第五台呢?如果拿到了他们的地址,我该如何挑选其中一台去使用呢?如果你挑中了一台,你怎么知道这台现在依然是健康的呢?万一它挂了呢?是不是一堆的问题啊?

但是啊,我们的 Eureka是可以解决这些问题的!

Eureka的作用

在Eureka的结构当中,它分成了2个概念(角色),第一个角色是eureka-server 注册中心 ,它的作用是记录和管理这些微服务。而第二个角色是我们的服务提供者和服务消费者,但不管是提供者还是消费者,都是微服务,我们把它叫做eureka-client 客户端

我们的user-service和order-service在启动的时候会做一件事情,它会把自己的信息注册给eureka。注意:是每开一个服务启动时都会注册。eureka会把你的服务信息给记录下来。

那么全都记下来了,所有的服务信息都在注册中心里面了。如果这个时候我们有一个服务想要消费,它不需要自己去记录信息,直接找eureka就好了。如果eureka找到发现有,而且还有3个呢,就会把信息给服务消费者。

服务消费者拿到了服务提供者的信息,发现有三个,这个时候就会用到我们以前学过的负载均衡的知识点,从这三个里面挑一个出来。

那可能会想了,那你调用的这一个会不会是挂了的啊?这肯定不会,因为我们的服务每隔30秒都会向eureka发一次心跳续约 ,来确认自己的状态。如果哪一天,它不跳了。eureka就会把那个服务移除,服务消费者自然也不会得到挂掉的那个服务。

2.搭建eureka服务

我们来做三件事情,第一就是搭建eureka注册中心 ,第二步我们会完成服务的注册,把所有的服务都注册到eureka,第三步我们会去做服务发现,会让order-service取拉去服务列表,得到user-service的所有实例,利用负载均衡去挑一个。下面我们就来一步一步完成:

搭建EurekaServer 注册中心

(1)eureka的搭建需要创建一个独立的微服务,所以等一会儿我们会在Spring Cloud父项目下创建一个新的项目(new一个maven的Module),这个子项目名叫eureka-server,然后子项目会引入下面的依赖:

代码语言:javascript复制
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

(2)创建启动类,添加@EnableEurekaServer注解

代码语言:javascript复制
package com.haiexijun.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class,args);
    }
}

(3)在application.yml配置文件中添加如下的配置(这里最好复制,不然容易出错):

代码语言:javascript复制
server:
  port: 10086  # 服务端口
spring:
  application:
    name: eureka-server # 服务的名称
eureka:
  client:
    service-url:  # eureka的地址信息,如果是eureka集群的话,用逗号隔开
      defaultZone: http://127.0.0.1:10086/eureka

下面就可以运行起来了。

idea的功能很强大,我们点击之后会跳转到Eureka的界面,如下图所示:

这个界面的Instances currently registered with Eureka就是注册到Eureka中的实例的列表。上面我们看见了,eureka也注册了自己的实例。

3.服务注册

服务注册只要经过以下两个步骤就够了: (1)在user-service的pom文件中,引入下面的eureka-client依赖:

代码语言:javascript复制
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

(2)编写user-service的application.yml文件,内容如下:

代码语言:javascript复制
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: zc20020106
    driver-class-name: com.mysql.cj.jdbc.Driver
  application: #服务的名称
    name: userservice
mybatis:
  type-aliases-package: com.haiexijun.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.haiexijun: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
eureka:
  client:
    service-url:  # eureka的地址
      defaultZone: http://127.0.0.1:10086/eureka

下面我们重新启动以下这个项目,发现会多一个USERSERVICE的实例:

如果我想把order-service也注册到Eureka,也是一样的按上面的步骤就行。

我们如果要想体验之前所说的服务列表,一个服务多个实例的话,按理来说我们是无法在同一台电脑上多次运行来实现的,我们只是在自己的电脑上,不是真正的部署。但是idea可以帮助我们实现一个服务启动多个实例,我们的idea可以把服务拷贝一份,然后去启动运行。

下面来演示一下如何启动多个user-service实例:

(1) 首先,复制原来的user-service启动配置: 选择我们的服务,然后右键一下,点击Copy Configuration。

然后,在弹出的窗口中,填写信息:

然后就会发现idea帮我们复制了一个实例,我们把那个实例运行起来,然后打开Eureka的页面,会发现user-server注册了2个实例,如下图所示:

4.服务发现

我们希望order-service可以基于服务名称拉取到服务列表,然后再对服务列表做负载均衡。我们可以这样做:

(1)修改OrderService的代码,修改访问的url路径,用服务名代替IP和端口:

(2)服务拉取和负载均衡 , 这些动作不用我们去做,只需要添加一些注解即可。 在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解

代码语言:javascript复制
package com.haiexijun.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.haiexijun.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

    /**
     * 创建RestTemplate,并注入Spring容器
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

我们重新运行项目,访问order的接口可以查询到订单的相关信息了:

然后我们查看user-service的日志,会发现有调用。

四.Ribbon负载均衡

1.负载均衡的原理

我们先来回顾一下之前的流程,我们有一个order-service和两个user-service。order-service在发起请求时,是通过http://userservice/user/1来调用的。但是这个地址却不是一个真实可用的地址,浏览器里面根本无法访问到这个地址。所以这中间一定会有东西把请求拦截下来,然后再处理找到真实的IP和端口才行,这中间就是我们的Ribbon在做的这件事情。Ribbon拦截下你的请求以后,它会想办法找到你的真实地址,而Ribbon会去EurekaServer拉去服务信息。至于里面具体如何操作,我们得看源码。

源码跟踪: 为什么我们只输入了service名称就可以访问了呢?显然有人帮我们根据service名称,获取到了服务实例的IP和端口。它就是LoadBalancerInterceptor,这个类会在对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。

我们进行源码跟踪:

(1) LoadBalancerIntercepor

可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:

  • request.getURI():获取请求uri,本例中就是 http://user-service/user/8
  • originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service
  • this.loadBalancer.execute():处理服务id,和用户请求。 这里的this.loadBalancerLoadBalancerClient类型,我们继续跟入。

(2) LoadBalancerClient 继续跟入execute方法:

代码是这样的:

  • getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。
  • getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8082端口的服务

放行后,再次访问并跟踪,发现获取的是8081:

果然实现了负载均衡。

(3) 负载均衡策略IRule 在刚才的代码中,可以看到获取服务使通过一个getServer方法来做负载均衡:

我们继续跟入:

继续跟踪源码chooseServer方法,发现这么一段代码:

我们看看这个rule是谁:

这里的rule默认值是一个RoundRobinRule,看类的介绍:

这不就是轮询的意思嘛。

到这里,整个负载均衡的流程我们就清楚了。

总结: SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:

基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1
  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

2.负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类 (默认的实现就是ZoneAvoidanceRule,是一种轮询方案) :

不同规则的含义如下:

内置负载均衡规则类

规则描述

RoundRobinRule

简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。

AvailabilityFilteringRule

对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。

WeightedResponseTimeRule

为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。

ZoneAvoidanceRule

以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。

BestAvailableRule

忽略那些短路的服务器,并选择并发数较低的服务器。

RandomRule

随机选择一个可用的服务器。

RetryRule

重试机制的选择逻辑

3.自定义负载均衡策略

通过定义IRule实现可以修改负载均衡规则,有两种方式:

(1)代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:

代码语言:javascript复制
@Bean
public IRule randomRule(){
    return new RandomRule();
}

(2)配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:

代码语言:javascript复制
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 

注意,一般用默认的负载均衡规则就好,不做修改。

4.饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。饥饿加载就好比是饿了,看到什么都往上吭,可以选择那些服务,一上来就想去把这些服务加载进来,不管三七二十一。

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

代码语言:javascript复制
ribbon:
  eager-load:
    enabled: true  # 开启饥饿加载
    clients: userservice  # 指定饥饿加载的服务名称

如果是多个服务要换个行:

代码语言:javascript复制
ribbon:
  eager-load:
    enabled: true  # 开启饥饿加载
    clients:
    	- userservice
    	- xxxxservice

五. Nacos注册中心

大家可能会有疑问,我们已经学习过Eureka的注册中心,现在为什么又要学习Nacos的注册中心呢? 国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。

1.认识Nacos

Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。

2.Windows下安装Nacos

下面就来介绍如何在window下面安装Nacos。开发阶段采用单机安装即可。

(1)下载安装包

在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:

GitHub主页:https://github.com/alibaba/nacos

GitHub的Release下载页:https://github.com/alibaba/nacos/releases

如图:

但是本篇教程用最新版的2.1.0的版本来学习。

这里要注意zip的是windows的版本,而.tar.gz的版本是Linux的版本。不要用错了。

我们解压到一个非中文的目录下就行了,如图:

目录说明:

  • bin:启动脚本
  • conf:配置文件

(2)端口配置

Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。

如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口:

(3) 启动

启动非常简单,进入bin目录,结构如下:

然后执行命令即可:

windows命令:

代码语言:javascript复制
startup.cmd -m standalone

执行后的效果如图,并且不会报错:

(4)访问

在浏览器输入地址:http://127.0.0.1:8848/nacos即可访问:

默认的账号和密码都是nacos,进入后:

这里就成功进入到我们nacos的控制台了!

3.Linux下安装Nacos

Linux或者Mac安装方式与Windows类似。

(1)上传安装包,随便用什么方式上传上去, 但是要记住上传.tar.gz的那个包。

(2)移动到Linux服务器的某个目录,例如/usr/local/src目录下

(3)解压

命令解压缩安装包:

代码语言:javascript复制
tar -xvf nacos-server-2.1.0.tar.gz

然后删除安装包:

代码语言:javascript复制
rm -rf nacos-server-2.1.0.tar.gz

(4)端口配置与windows中类似

(5)启动 在nacos/bin目录中,输入命令启动Nacos:

代码语言:javascript复制
sh startup.sh -m standalone

4. 服务注册到nacos

下面我们就可以用Nacos来完成服务注册和服务发现了。

不管是Eureka也好,还是Nacos也好,只要是做服务注册和服务发现,都会遵循Spring Cloud Commons的一些通用接口。那么,我们在使用Eureka或者Nacos时,我们的服务提供者和服务消费者的代码是不用做任何变化的。要改变的东西是我们要引用的依赖,以前是Eureka的依赖,现在该引用Nacos的依赖了。第二呢就是服务地址,以前我们的服务地址配的是Eureka的地址,现在改成配Nacos的地址就可以了。

总结一下就是: Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。

主要差异在于:

  • 依赖不同
  • 服务地址不同

(1)引入依赖

在cloud-demo父工程的pom文件中的<dependencyManagement>中添加SpringCloudAlibaba的依赖:

代码语言:javascript复制
<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>

然后在user-service和order-service中的pom文件中分别引入nacos-discovery依赖:

代码语言:javascript复制
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

注意:不要忘了注释掉eureka的依赖。

(2)配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:8848

这里也不要忘记把之前Eureka的配置删除掉

更改后的yml分别如下:

代码语言:javascript复制
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: zc20020106
    driver-class-name: com.mysql.cj.jdbc.Driver
  application: #服务的名称
    name: userservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
mybatis:
  type-aliases-package: com.haiexijun.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.haiexijun: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS
代码语言:javascript复制
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: zc20020106
    driver-class-name: com.mysql.cj.jdbc.Driver
  application: #服务的名称
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
mybatis:
  type-aliases-package: com.haiexijun.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.haiexijun: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

(3)重启所有服务

打开nacos的控制台,可以查看到服务列表了。

我们点开详情,会有更详细的服务信息:

5. 服务分级存储模型

你光听名字是不是觉得服务分级存储模型很高级啊,但其实我们已经接触过这样的分级概念了。

之前,我们有服务的概念,我们提供用户查询的user-service,还有提供订单查询的order-service,他们都叫服务。然后我们user-service还部署了多个实例。所以,我们之前是分有两层概念的,第一层是服务,而第二层是实例,一个服务可以包含多个实例。

不过啊,随着我们的业务规模越来越大,那么我们就会考虑更多的问题了。比如说,我们现在我们把所以的实例都部署在一个机房,就像你把鸡蛋放在一个篮子里,要是哪一天,不小心篮子翻了,蛋不就全打了吗?那你的机房要是天灾人祸出了问题,那整个服务不久完了吗?

所以,为了解决这个问题,我们会将一个服务的多个实例部署到多个机房。特别像阿里和京东这种财大气粗的,我全国各地都整些机房。一个倒了,还有其他的在正常运行。这就叫容灾。

而我们的Nacos服务分级存储模型,就是引入了类似机房的概念或者地域的概念。他把同在一个机房的多个实例称为一个集群。比方说杭州的某一个服务的机房的所有实例就称杭州的集群,北京机房的服务实例就称为北京的集群。

所以在nacos的服务分级存储模型中,一级是服务,往下是集群,再往下是实例。

那为什么Nacos要引入这样的服务分级的模型呢?我原来直接用服务找实例不好吗?

我们设想有这样一种情况,比方说我有一个杭州的机房,里面有order-service的集群和user-service的集群,然后我还有一个上海的机房,也是一样的配置,将来还可能会有北京机房等等。现在我们的order-service想要访问user-service,他有两种选择,一种是在自己本地访问,一种是去局域网外访问,你觉得它该选哪一个啊?肯定选本地啊!我们局域网内的访问距离比较短,速度就比较快,延迟也就比较低。而你跨越了集群的访问,比如说你从杭州去请求广州,达到了数百公里,这个时候延迟是非常高的。所以,在服务调用时应该尽可能地去访问本地集群,只有在本地集群不可用的情况下,才去访问其他的集群。

我们现在还没有配置集群,我们进入nacos的控制台,点开一个服务的详情:

会发现它的集群为DEFAULT ,也就是说没有配置集群。

下面就来学习一下如何配置一个服务的集群。

6.配置服务集群属性

我们有3个userservice服务,假如我们想要前两个user服务的集群放到上海集群,最后一个user服务放到杭州集群。来模拟一下这种跨集群部署的方式。

修改user-service的application.yml文件,添加集群配置:

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

编写好配置文件后,我们先启动前面的两个服务实例:

然后我们要配置第三个实例的集群,我们再修改yml配置文件:

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: SH # 集群名称

然后再启动最后一个服务(前两个服务不要关掉):

或者在服务配置里面配置集群:

我们再次打开Nacos的控制台,然后点击用户服务的详情:

还没有完,我们最终想要实现的是order-service远程调用user-service时,优先选择本地集群。因此,我们还需要给order-service也配置一个集群属性。

下面回到idea,为order-service也设置一下集群属性。我们配置为杭州HZ,然后重启order-service。

7.NacosRule负载均衡

我们清空日志后,在浏览器访问order服务获取三个订单数据,然后回到idea的控制台看日志。会发现8081,8082,8083端口的三个服务都被访问了。

我们发现,order-service发起远程调用的时候居然没有选择同集群的8081和8082。它仍然采用的是轮询方案,这又是什么原因啊?

我们知道,服务在选择一个实例时,全都是由负载均衡的规则来决定的。我们现在没有配置,默认的就是轮询的规则。所以要想实现优先同集群访问的负载均衡规则,我们必须去修改负载均衡。

默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。

(1) 给order-service配置集群信息(上一节配好了,就可以不用配了)

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称

(2) 修改负载均衡规则 修改order-service的application.yml文件,修改负载均衡规则:

代码语言:javascript复制
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 

然后,我们重新启动一下order-service服务,然后在浏览器里多访问几次订单查询。

我们发现只有8081和8082的控制台才有日志,而8083的控制台没有日志。这说明我们已经实现了Nacos同集群优先的负载均衡。

我们还会发现Nacos的一个特点就是它在8081和8082并不是轮询,是随机的。

也就是说,Nacos规则优先选择本地集群,在集群内,随机选择不同的服务实例进行负载均衡。

我们在来演示一下跨集群的访问吧,在idea里面把8081和8082的两个HZ集群的服务全部关闭,然后Nacos控制台会显示健康的实例数只有一个了。然后我们浏览器访问订单服务。

会发现8083的日志出现了,并且order-service服务也有调用远程集群的提示日志,这证明order-service进行了跨集群的访问。

8.服务实例的权重设置

实际部署中会出现这样的场景:

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。

在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:

在弹出的编辑窗口,修改权重:

我们把8081实例的权重值设置为 0.1 ,理论上讲8081被访问到的概率就会是8082的十分之一了。

注意:如果权重修改为0,则该实例永远不会被访问

把实例的权重调成0时,这个实例就压根不会被访问,那这有啥作用呢?

在我们以前,一个服务我们想要对他做一个版本的升级,我们是不是要把它重启啊,但是你光天化日之下你去重启一个服务,你好意思吗?用户都还在访问呢,你一重启客户都访问不上了,这样做就有问题啊。所以说我们不能随便去重启的。

我们有了权重之后,我们是不是可以这么做呢?假设我们有多个服务器8081、8082、8083(这里假设一下),我先把8081的服务的权重调成0,这个时候8081就不接收用户请求了。此时我对这台服务器做停机,用户就不会有感知了。我们就可以对8081进行一些版本的升级,升级完成之后,我再重启,并且权重我也先不调太大,先放少数用户进来测试看看行不行,如果没有问题,我们就可以依次扩大比例,依次都升级。用户是无感知的,这可以做到平滑升级,非常优雅。

9.环境隔离

Nacos提供了namespace来实现环境隔离功能。

我们知道Nacos是一个注册中心,它还是一个数据中心。所以在Nacos里面,它为了去做数据和服务的管理,会有一个环境隔离的概念。

Nacos中服务存储和数据存储的最外层都是一个namespace的东西,用来做最外层隔离。

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

有人可能会问,我们既然把服务实例划分成了集群,怎么要再整一个隔离呢?服务划分和实例划分是基于业务去做的划分,但事实上我们会有开发环境、测试环境、生产环境的变化吧。所以我们会基于这种环境变化去做隔离,namespace就是来做这样一件事情的。

而至于Group是分组的意思,把一些业务相关度比较高的服务放到一个组。假设你的订单服务和支付服务业务相关度比较高,那你就可以把它们放到一个Group里面去。

所以这是概念上的一个划分,他不是要求你必须得用这个来划分,你可以选择来用namespace或group,这不是强制的。

下面就来演示一下Nacos的使用。

10.创建namespace环境隔离

默认情况下,所有service、data、group都在同一个namespace,名为public:

我们点击命名空间栏后,右上角可以点击新建命名空间:

我们可以点击页面新增按钮,添加一个namespace:

然后,填写表单:

然后点击确定。然后切换到服务列表:

11.给微服务配置namespace

给微服务配置namespace只能通过修改配置来实现。

例如,修改order-service的application.yml文件:

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: b4401444-96e0-4b9a-a58b-9b24b74bfdfd # 命名空间,填ID

重启order-service后,访问控制台,可以看到下面的结果:

此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:

这里环境隔离我们就演示完了。

12.Nacos和Eureka的区别

我们先来看下图,这个图大家应该比较熟悉了吧,我们之前在学习Eureka的时候也见到过。不管是什么样的注册中心啊,我们服务提供者在启动时都会把自己的信息提交给注册中心,而注册中心就会把这些信息保留下来。那么当我们的服务消费者需要去消费时,就会去找注册中心,去拉去服务的信息。不过啊,当时我没讲的一点是,这个拉去的动作,并不是每一次都要做的。如果每一次发请求都要去做一次拉取,那么这样对Eureka和Nacos来讲压力是不是太大了。所以作为消费者在做服务拉取时,它会将获取到的服务信息缓存到一个列表当中。我拉去了一次,那么我接下来一段时间我就可以不用去拉去了,而是直接缓存中的服务信息了。当然了,我这个缓存一直不更新也不行,万一服务提供者变化了怎么办呢,所以会每隔30秒去拉去一次信息进行更新。

消费者拿到服务信息以后,再去进行负载均衡,挑选一个发起远程调用。当时在Nacos里面,它于Eureka会有一些差别,差别在于服务提供者的健康检测。我们的Nacos会把服务提供者划分成临时实例非临时实例

我们打开Nacos的控制台,随便打开一个服务的详情:

服务默认都是临时实例,因为我们没有配置。也就是说默认情况下,所有的实例都是临时实例。

临时实例和非临时实例在Nacos里面在做健康检测时是不一样的。

临时实例在Nacos做检测时采用的是心跳检测,这一点和Eureka的心跳检测完全一致。服务提供者每隔一段时间发一个请求到Nacos。

但是呢,我们的非临时实例就不一样了。非临时实例我们的Nacos不会去做心跳,这个时候健康检测这么检测的呢?是由Nacos主动发请求给服务提供者询问。并且如果非临时实例如果挂掉了,Nacos不会把它从列表中剔除,只会标注这个服务实例不健康,等这个服务恢复健康。

还有一个差别在于消费者,我们的Eureka采用的是定时拉去,每隔一段时间拉去一次。那你思考一下,如果在30秒内有服务提供者挂了,那服务消费者知道吗?如果它不知道,去调用挂掉了的服务提供者实例,不久出问题了吗?我们的Eureka做服务拉取的效率比较差,更新的不够及时,而我们的Nacos做了一件事,叫消息推送 。也就是说我们的Eureka采用的是pull,而我们的Nacos采用的是pull加push两者结合,我们的Nacos如果发现有服务挂了,它会发一条消息推送给我们的消费者,然后告诉你服务变更了,更加具有时效性。

这就是Eureka和Nacos之间的差别了。

下面小小总结一下:

Nacos的服务实例分为两种l类型:

  • 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例(非临时实例):

代码语言:javascript复制
spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置为非临时实例
  • Nacos与eureka的共同点
    • 都支持服务注册和服务拉取
    • 都支持服务提供者心跳方式做健康检测
  • Nacos与Eureka的区别
    • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

六. Nacos配置管理

Nacos除了可以做注册中心,同样可以做配置管理来使用。在这一章中,我们会学习统一配置管理的相关知识,然后会学习怎么去实现配置的热更新,还有不同微服务之间,微服务不同环境之间的配置共享,最后会学习搭建一个生产环境可用的Nacos集群。

1.统一配置管理

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。

Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。

我们可以点开Nacos的控制台的配置管理的配置列表,发现配置列表是空的:

我们点击右上角的那个加号:

他会弹出一个新建配置的表单要我们填写:

到这里,第一个要填写的东西叫Data ID,其实就是配置文件的名称。但是你注意了,这个名字你不能像我们Idea里面那样都起application.yml ,因为你叫这个名字的话就有一个问题了,将来我们所有的微服务都来找Nacos管理,大家叫这个名字,那不就冲突了吗?所以我们的Data ID必须唯一,不能冲突。那怎么办呢?

微服务的名称是不冲突的吧,所以Data ID的命名方式一般是这样子的,第一部分是服务名称 (如userservice)。第二部分是我们的运行环境,我们之前不是学过环境隔离,我们的环境有开发环境和测试环境等,所以这里可以命名成devtest或者prod。最后一部分我们写后缀名,我们写.yaml

然后就是Group分组名称,我们不要去更改,它默认就好了。

描述就是介绍你的配置文件是干什么的。

配置格式 我们选择yaml就好(目前只支持yaml和properties这两种格式)。

最后一个是配置内容,但是并不是把项目里面application.yml的所有配置内容都粘过来,这里填的配置都是用来做热更新的配置。

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

发布之后,配置列表里面就有配置了:

关于怎么读取这些配置,我们在下一小节来学习。

2.微服务拉取配置

我们在Nacos里面编写了配置列表,我们的微服务就要想办法得到这些配置了吧。那么怎样去得到呢?

我们下面先来看一下在没有Nacos服务时如何获取配置的。它的流程大概是这样子的:

首先项目启动,启动完了以后,会读取本地application.yml配置文件,读取完了以后会去创建Spring容器,而后会加载各种bean,之后就不说了,我们只关心配置这一部分。

在这里读取的是本地的yml配置文件,但现在我们多了Nacos里面的配置文件,将来我们的项目会把Nacos里面的配置与本地的配置文件做一个合并,然后再去完成容器创建和加载bean的操作。我们的项目启动的过程就会变成如下的过程:

这个时候,流程听起来很简单对不对?但是你们要注意一件事儿啊,项目在读取Nacos配置文件的时候,它需要去知道一些信息。第一,去哪读取?第二,读取谁?所以在读取Nacos配置文件的时候,得先知道它得地址吧?这个地址就在application.yml当中。但这里既然要先读取Nacos的配置,再去读取本地的yml配置,那这个Nacos地址还得用其他的办法提前知道才行啊。那有什么是比本地application.yml还要提前的?其实有,Spring里提供了一个bootstrap.yml的文件,这个文件的优先级会比application.yml的优先级要高很多。所以项目启动以后,他会先读取bootstrap.yml文件,我们只要把Nacos地址啊,文件的相关信息啊,都配置进来,那是不是就可以先完成Nacos配置的读取了,然后再和本地的application.yml结合,接着完成后续的操作。

下面演示一下具体的操作:

我们在操作做之前,先把dev那个namespace命名空间给删掉,然后配置文件配置的namespace也删掉先,,然后在public里面重新创建配置,为了方便后面的一些操作。

并且这里还有一个注意点,就是我们之前用的是最新版(2.1.0版本)的Nacos,但是最新版的Nacos与Spring Cloud兼容性并不是非常好,往往会出很多奇奇怪怪的问题。所以我们尽量使用1.4.1版本的Nacos。

下面也列出了一张Spring Cloud与其各组件的版本兼容表(最新版本用*标记)::

Spring Cloud Alibaba Version

Sentinel Version

Nacos Version

RocketMQ Version

Dubbo Version

Seata Version

2021.0.1.0*

1.8.3

1.4.2

4.9.2

2.7.15

1.4.2

2.2.7.RELEASE

1.8.1

2.0.3

4.6.1

2.7.13

1.3.0

2.2.6.RELEASE

1.8.1

1.4.2

4.4.0

2.7.8

1.3.0

2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE

1.8.0

1.4.1

4.4.0

2.7.8

1.3.0

2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE

1.8.0

1.3.3

4.4.0

2.7.8

1.3.0

2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE

1.7.1

1.2.1

4.4.0

2.7.6

1.2.0

2.2.0.RELEASE

1.7.1

1.1.4

4.4.0

2.7.4.1

1.0.0

2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE

1.7.0

1.1.4

4.4.0

2.7.3

0.9.0

2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE

1.6.3

1.1.1

4.4.0

2.7.3

0.7.1

下表为按时间顺序发布的 Spring Cloud Alibaba 以及对应的适配 Spring Cloud 和 Spring Boot 版本关系(由于 Spring Cloud 版本命名有调整,所以对应的 Spring Cloud Alibaba 版本号也做了对应变化)

Spring Cloud Alibaba Version

Spring Cloud Version

Spring Boot Version

2021.0.1.0

Spring Cloud 2021.0.1

2.6.3

2.2.7.RELEASE

Spring Cloud Hoxton.SR12

2.3.12.RELEASE

2021.1

Spring Cloud 2020.0.1

2.4.2

2.2.6.RELEASE

Spring Cloud Hoxton.SR9

2.3.2.RELEASE

2.1.4.RELEASE

Spring Cloud Greenwich.SR6

2.1.13.RELEASE

2.2.1.RELEASE

Spring Cloud Hoxton.SR3

2.2.5.RELEASE

2.2.0.RELEASE

Spring Cloud Hoxton.RELEASE

2.2.X.RELEASE

2.1.2.RELEASE

Spring Cloud Greenwich

2.1.X.RELEASE

2.0.4.RELEASE(停止维护,建议升级)

Spring Cloud Finchley

2.0.X.RELEASE

1.5.1.RELEASE(停止维护,建议升级)

Spring Cloud Edgware

1.5.X.RELEASE

(1)引入nacos-config依赖 首先,在user-service服务中,引入nacos-config的客户端依赖:

代码语言:javascript复制
<!--nacos配置管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

(2)添加bootstrap.yaml 然后,在user-service的resources目录中添加一个bootstrap.yaml文件,内容如下:

代码语言:javascript复制
spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev #开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名

这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据

{spring.application.name}-{spring.profiles.active}.

本例中,就是去读取userservice-dev.yaml

(3)读取nacos配置 在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:

全部代码如下:

代码语言:javascript复制
package com.haiexijun.user.web;

import com.haiexijun.user.pojo.User;
import com.haiexijun.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

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

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;
	
	// 获得当前时间,通过nacos配置格式化。
    @GetMapping("/now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
    
    /**
     * 路径: /user/110
     *
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

在页面访问,可以看到效果:

到这里就实现了配置管理的第二步,微服务获取Nacos中的配置了。

3.配置热更新

我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新

要实现配置热更新,可以使用两种方式:

方式一

方式二: 使用@ConfigurationProperties注解代替@Value注解。

在user-service服务中,添加一个类,读取patterrn.dateformat属性:

代码语言:javascript复制
package com.haiexijun.user.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

在UserController中使用这个类来代替@Value:

完整代码:

代码语言:javascript复制
package com.haiexijun.user.web;

import com.haiexijun.user.config.PatternProperties;
import com.haiexijun.user.pojo.User;
import com.haiexijun.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

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

    @Autowired
    private UserService userService;

    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }

    // 略
}

更改后重启项目,我们更改Nacos的配置来测试一下热更新:

4.多环境配置共享

这一节我们会来了解一下微服务之间的配置共享问题,有人可能会有疑问了,什么情况下我们会碰到微服务的配置共享呢?

比方说这样一个场景,有一个配置属性,他在开发、生产和测试三个环境下的值是一样的。这样的配置,在每一个配置文件中都去写一份是不是有点浪费啊?而且将来如果要改动,我还得在每一个配置文件里都去改。这样显然是不合适的吧?我想要每个上配一次以后,不管环境怎么变,这个配置都能够被加载。这就是多环境共享的一个需求了。

微服务在启动时会从Nacos读取多个配置文件: 第一个是我们非常熟悉的[spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml。 第二个文件是[spring.application.name].yaml,例如:userservice.yaml。

无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此可以被多个环境共享。

比如下图:

我们定义一个dev的yml,一个userservice的yml。我们不管环境怎么变化,这个userservice.yml一定会被共享。

下面我们通过案例来测试配置共享:

(1)添加一个环境共享配置

(2)在user-service中读取共享配置

(3)运行两个UserApplication,使用不同的profile 修改UserApplication2这个启动项,改变其profile值:

这样,UserApplication(8081)使用的profile是dev,UserApplication2(8082)使用的profile是test。

启动UserApplication和UserApplication2

访问http://localhost:8081/user/prop,结果:

访问http://localhost:8082/user/prop,结果:

(4)配置共享的优先级

当nacos、服务本地同时出现相同属性时,优先级有高低之分:

5.搭建Nacos集群

下面是官方给出的Nacos集群图:

SLB就是负载均衡器,它将我们的请求分发到不同的Nacos节点,就形成一个集群结构了。

其中包含3个nacos节点(这里假设有3台,以后你要有钱多少台都行),然后一个负载均衡器代理3个Nacos。

这里负载均衡器可以使用nginx。

我们计划的集群结构:

我们会这一个MySQL的集群,让多个Nacos都去访问这个集群,在里面完成数据读写,这样数据不久共享了吗。

而后用户请求进入以后,我们还要把请求分发到不同的Nacos节点,我们通过Nginx来实现负载均衡。这样整个集群结构就有了。

虽然我们要按照这个来做,但是条件有限,我们就一台电脑,不会真的去做上台机器去演示,我们在当前的一台电脑上去部署三个Nacos节点。MySQL理论上讲也是集群,但我们也弄个单点。

三个nacos节点的地址:

节点

ip

port

nacos1

10.129.186.226

8845

nacos2

10.129.186.226

8846

nacos3

10.129.186.226

8847

因为是同一台机器,所以ip地址是一样的,不同的是端口号。

这里的ip学习时以自己的电脑的ip为准。

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载nacos安装包
  • 配置nacos
  • 启动nacos集群
  • nginx反向代理

下面就一步一步来操作:

(1)初始化数据库 Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。

官方推荐的最佳实践是使用带有主从的高可用数据库集群。

这里环境条件有限,而且配置繁琐,写下来估计没个几万字讲不好,我们以单点的数据库为例来讲解。

首先新建一个数据库,命名为nacos,而后导入下面的SQL:

代码语言:javascript复制
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) DEFAULT NULL,
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_aggr   */
/******************************************/
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_beta   */
/******************************************/
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_tag   */
/******************************************/
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_tags_relation   */
/******************************************/
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = group_capacity   */
/******************************************/
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = his_config_info   */
/******************************************/
CREATE TABLE `his_config_info` (
  `id` bigint(64) unsigned NOT NULL,
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `src_user` text,
  `src_ip` varchar(50) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = tenant_capacity   */
/******************************************/
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
	`username` varchar(50) NOT NULL PRIMARY KEY,
	`password` varchar(500) NOT NULL,
	`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
	`username` varchar(50) NOT NULL,
	`role` varchar(50) NOT NULL,
	UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
    `role` varchar(50) NOT NULL,
    `resource` varchar(255) NOT NULL,
    `action` varchar(8) NOT NULL,
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

(2)下载nacos

(3)配置Nacos 将这个包解压到任意非中文目录下,如图:

目录说明:

  • bin:启动脚本
  • conf:配置文件

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

然后添加内容:

代码语言:javascript复制
10.129.186.226:8845
10.129.186.226:8846
10.129.186.226:8847

然后修改application.properties文件,添加数据库配置:

代码语言:javascript复制
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=zc20020106

(4)启动 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3:

然后分别修改三个文件夹中的application.properties nacos1:

代码语言:javascript复制
server.port=8845

nacos2:

代码语言:javascript复制
server.port=8846

nacos3:

代码语言:javascript复制
server.port=8847

然后分别启动三个nacos节点(现在是集群启动,不用加-m什么的了,默认就是集群启动):

每个nacos都要运行一遍哦!

代码语言:javascript复制
startup.cmd

(5)nginx反向代理 下载Nginx,然后解压到任意非中文目录下:

修改conf/nginx.conf文件,配置如下(把它粘贴到http{ }的内部就好了):

代码语言:javascript复制
upstream nacos-cluster {
    server 127.0.0.1:8845;
	server 127.0.0.1:8846;
	server 127.0.0.1:8847;
}

server {
    listen       80;
    server_name  localhost;

    location /nacos {
        proxy_pass http://nacos-cluster;
    }
}

然后在上一级目录运行命令启动Nginx (或者双击运行nginx):

代码语言:javascript复制
start nginx.exe

而后在浏览器访问:http://localhost/nacos即可。

项目代码中application.yml文件配置如下:

代码语言:javascript复制
spring:
  cloud:
    nacos:
      server-addr: localhost:80 # Nacos地址

(6)优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.
  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

这一小节的一些坑:

  • cluster.conf里面要填写本机的真实IP,不能写127.0.0.1 。
  • 单机启动nacos服务后,服务注册出现以下异常:

解决办法: 删除data目录下的protocol文件夹,重启服务即可。 异常原因: 1.4.0使用了jraft, jraft会记录前一次启动的集群地址,如果重启机器ip变了的话,会导致jraft记录的地址失效,从而导致选主出问题。 1.4.0之后,单机情况下也是存在节点了。流程和集群一样,需要先选出leader,再提供服务。

  • 配置好Nacos集群,我们重启Nacos后,我们之前的Nacos配置文件会被清掉一次,所以最好把之前项目里面读取Nacos配置文件的代码都删除掉。

七. Feign远程调用

这一章会先分析一下RestTemplate存在的问题,然后学习用Feign去替代RestTemplate。当然,我们还会学习一下Feign的自定义的一些配置,以及使用时的一些性能优化。最后会学习Feign在企业当中的最佳实践方案。

1. RestTemplate存在的问题

先来看我们以前利用RestTemplate发起远程调用的代码:

这个请求是通过URL地址,指明要访问的服务名称,还有请求路径,以及请求的参数信息。而后由RestTemplate帮我们向指定地址发起请求,再把结果转换成对应类型。

这段代码存在以下的问题:

•代码可读性差,编程体验不统一

•参数复杂URL难以维护

Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign

其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。

2. Feign替代RestTemplate

Feign的使用步骤如下:

(1) 引入依赖 我们在order-service服务的pom文件中引入feign的依赖:

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

(2) 添加注解 在order-service的启动类添加注解开启Feign的功能:

代码语言:javascript复制
@EnableFeignClients
@MapperScan("com.haiexijun.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

(3) 编写Feign的客户端 在order-service中新建一个接口,内容如下:

代码语言:javascript复制
package com.haiexijun.order.client;

import com.haiexijun.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:

  • 服务名称:userservice
  • 请求方式:GET
  • 请求路径:/user/{id}
  • 请求参数:Long id
  • 返回值类型:User

这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。

(4)测试 修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

是不是看起来优雅多了。而且Feign非常强大,不仅实现了远程调用,还实现了负载均衡。

(5) 总结 使用Feign的步骤:

① 引入依赖

② 添加@EnableFeignClients注解

③ 编写FeignClient接口

④ 使用FeignClient中定义的方法代替RestTemplate

2. 自定义配置Feign

SpringBoot虽然帮我们实现了自动装配,但它是允许我们覆盖默认配置的。

Feign可以支持很多的自定义配置,如下表所示(只是部分):

类型

作用

说明

feign.Logger.Level

修改日志级别

包含四种不同的级别:NONE(默认)、BASIC、HEADERS、FULL

feign.codec.Decoder

响应结果的解析器

http远程调用的结果做解析,例如解析json字符串为java对象

feign.codec.Encoder

请求参数编码

将请求参数编码,便于通过http请求发送

feign. Contract

支持的注解格式

默认是SpringMVC的注解

feign. Retryer

失败重试机制

请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

有配置文件和java代码两种方式。

方式一配置文件方式 基于配置文件修改feign的日志级别可以针对单个服务:

代码语言:javascript复制
feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

也可以针对所有服务:

代码语言:javascript复制
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

方式二Java代码方式 也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

代码语言:javascript复制
public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

代码语言:javascript复制
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

如果是局部生效,则把它放到对应某一个服务的@FeignClient这个注解中:

代码语言:javascript复制
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 

3. Feign使用优化

Feign的性能已经很好了,但是还是有优化的余地。我们先来了解一下Feign底层的实现:

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

•URLConnection:默认实现(JDK自带的),不支持连接池

•Apache HttpClient :支持连接池

•OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

这里我们用Apache的HttpClient来演示。

(1) 引入依赖

在order-service的pom文件中引入Apache的HttpClient依赖:

代码语言:javascript复制
<!--httpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

(2) 配置连接池

在order-service的application.yml中添加配置:

代码语言:javascript复制
feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

接下来,在FeignClientFactoryBean中的loadBalance方法中打断点:

Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:

总结,Feign的优化:

1.日志级别尽量用basic

2.使用HttpClient或OKHttp代替URLConnection

① 引入feign-httpClient依赖

② 配置文件开启httpClient功能,设置连接池参数

4. Feign最佳实践的分析

什么是最佳实践呢?就是企业在使用一个东西的过程中,各种踩坑,最后总结出来的一个比较好的使用方式。

观察可以发现,Feign的客户端与服务提供者的controller代码非常相似:

feign客户端:

UserController:

有没有一种办法简化这种重复的代码编写呢?

这一节会介绍两种比较好的Feign的实践方案。

方式一:继承方式

一样的代码可以通过继承来共享:

1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

2)Feign客户端和Controller都集成改接口

优点:

  • 简单
  • 实现了代码共享

缺点:

  • 服务提供方、服务消费方紧耦合
  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

方式二:抽取方式 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

5. Feign 最佳实践的代码实现

下面以抽取的方式来实现Feign的最佳实践。

(1) 抽取

首先创建一个module,命名为feign-api:

项目结构:

在feign-api中然后引入feign的starter依赖:

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中。

(2)在order-service中使用feign-api

首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。

在order-service的pom文件中中引入feign-api的依赖:

代码语言:javascript复制
<dependency>
    <groupId>com.haiexijun.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包

(3) 重启测试

(4) 解决扫描包问题

方式一:

指定Feign应该扫描的包:

代码语言:javascript复制
@EnableFeignClients(basePackages = "com.haiexijun.feign.clients")

方式二:

指定需要加载的Client接口:

代码语言:javascript复制
@EnableFeignClients(clients = {UserClient.class})

八.Gateway服务网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

1. 为什么需要网关

Gateway网关是我们服务的守门神,所有微服务的统一入口。

我们的微服务如果直接任人都能发请求来访问是不是不太安全啊。你要知道,不是所有的业务都是对外公开的,有好多业务属于公司内部的或者管理人员才可以去访问的,所以得对用户的身份做一个认证,如果说是我们的内部人员,才允许访问一些服务。网关就是来做这样一件事情的。

网关的核心功能特性

  • 请求路由
  • 权限控制
  • 限流

架构图:

权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。

路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。

限流:当请求流量过高时,在网关中按照微服务能够接受的速度来放行请求,避免服务压力过大。

在SpringCloud中网关的实现包括两种:

  • gateway
  • zuul

Zuul是基于Servlet的实现,属于阻塞式编程。而Spring Cloud Gateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。所以我们选择Spring Cloud Gateway。

2. Gateway快速入门

下面,我们就演示下网关的基本路由功能。基本步骤如下:

  1. 创建SpringBoot工程gateway,引入网关依赖
  2. 编写启动类
  3. 编写基础配置和路由规则
  4. 启动网关服务进行测试

(1)创建gateway服务,引入依赖

创建项目:

引入依赖:

代码语言:javascript复制
<!--网关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(2)编写启动类

代码语言:javascript复制
package com.haiexijun.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

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

(3)编写基础配置和路由规则

创建application.yml文件,内容如下:

代码语言:javascript复制
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
         - id: order-service
          uri: lb://orderservice
          predicate:
            - Path=/order/**

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。

本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。

(4)重启测试 重启网关,访问http://localhost:10010/order/101时,符合/user/**规则,请求转发到uri:http://userservice/order/101,得到了结果:

(5)网关路由的流程图

整个访问的流程如下:

总结:

网关搭建步骤:

  1. 创建项目,引入nacos服务发现和gateway依赖
  2. 配置application.yml,包括服务基本信息、nacos地址、路由

接下来,就重点来学习路由断言和路由过滤器的详细知识

3.路由断言工厂

路由断言工厂 Route Predicate Factory

网关路由可以配置的内容包括:

  1. 路由id:路由的唯一标示
  2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则,
  4. 路由过滤器(filters):对请求或响应做处理

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。

例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的。

像这样的断言工厂在SpringCloudGateway还有十几个:

名称

说明

示例

After

是某个时间点后的请求

- After=2037-01-20T17:42:47.789-07:00[America/Denver]

Before

是某个时间点之前的请求

- Before=2031-04-13T15:14:47.433 08:00[Asia/Shanghai]

Between

是某两个时间点之前的请求

- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]

Cookie

请求必须包含某些cookie

- Cookie=chocolate, ch.p

Header

请求必须包含某些header

- Header=X-Request-Id, d

Host

请求必须是访问某个host(域名)

- Host=**.somersetting, **.anotherhost.org

Method

请求方式必须是指定方式

- Method=GET,POST

Path

请求路径必须符合指定规则

- Path=/red/{segment},/blue/**

Query

请求参数必须包含指定参数

- Query=name, Jack或者- Query=name

RemoteAddr

请求者的ip必须是指定范围

- RemoteAddr=192.168.1.1/24

Weight

权重处理

我们只需要掌握Path这种路由工程就可以了。如果需要用其他的断言规则,可以点击这里进行查看。

4.路由过滤器的配置

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:

路由过滤器的种类 Spring提供了31种不同的路由过滤器工厂。例如:

名称

说明

AddRequestHeader

给当前请求添加一个请求头

RemoveRequestHeader

移除请求中的一个请求头

AddResponseHeader

给响应结果中添加一个响应头

RemoveResponseHeader

从响应结果中移除有一个响应头

RequestRateLimiter

限制请求的流量

有其他更多的需求,同样可以点击官方文档。

下面我们以AddRequestHeader 为例来讲解。

需求:给所有进入userservice的请求添加一个请求头:Truth=haiexijun is freaking awesome!

实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Truth, haiexijun is freaking awesome! # 添加请求头

当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。

然后,我们更改一下我们的UserController的代码,来测试一下。

代码语言:javascript复制
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth",required = false) String truth) {
        System.out.println(truth);
        return userService.queryById(id);
    }

默认过滤器

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      default-filters: # 默认过滤项
      - AddRequestHeader=Truth, haiexijun is freaking awesome! 

5. 全局过滤器

上一节学习的过滤器,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。

全局过滤器作用:

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

定义方式是实现GlobalFilter接口。

代码语言:javascript复制
public interface GlobalFilter {
    /**
     *  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
     *
     * @param exchange 请求上下文,里面可以获取Request、Response等信息
     * @param chain 用来把请求委托给下一个过滤器 
     * @return {@code Mono<Void>} 返回标示当前过滤器业务结束
     */
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

在filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

下面来实践一下自定义全局过滤器

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有authorization,
  • authorization参数值是否为admin

如果同时满足则放行,否则拦截

实现

在gateway中定义一个过滤器:

代码语言:javascript复制
package com.haiexijun.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

// 过滤器的优先级,越小越先执行
@Order(-1)
// 注册为组件
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
        // 2.获取authorization参数
        String auth = params.getFirst("authorization");
        // 3.校验
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.拦截
        // 4.1.禁止访问,设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.结束处理
        return exchange.getResponse().setComplete();
    }
}

6. 过滤器的执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:

排序的规则是什么呢?

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。

详细内容,可以查看源码:

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。

org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链

7. 网关的跨域配置

我们以前在JavaWeb阶段已经学习过跨域问题的解决方案了,那微服务里面为什么要学这个东西呢?

这是因为在微服务当中,所有的请求都要先经过网关,再到微服务。也就是说,跨域请求你不需要在每一个微服务里都去处理,仅仅在网关处理就可以了。但是网关又和我们之前的实现不一样,网关是基于webflux实现的,没有servlet相关的API ,因此我们以前所学的那些解决方案不一定能够适用。

下面先来回顾一下跨域:

跨域:域名不一致就是跨域,主要包括:

  • 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
  • 域名相同,端口不同:localhost:8080和localhost8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html

解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置:

代码语言:javascript复制
spring:
  cloud:
    gateway:
      # 。。。
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
              - "http://www.leyou.com"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

0 人点赞