这一节系统地学习一下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
注解
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/8originalUri.getHost()
:获取uri路径的主机名,其实就是服务id,user-service
this.loadBalancer.execute()
:处理服务id,和用户请求。 这里的this.loadBalancer
是LoadBalancerClient
类型,我们继续跟入。
(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的依赖:
<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)。第二部分是我们的运行环境,我们之前不是学过环境隔离,我们的环境有开发环境和测试环境等,所以这里可以命名成dev
、test
或者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快速入门
下面,我们就演示下网关的基本路由功能。基本步骤如下:
- 创建SpringBoot工程gateway,引入网关依赖
- 编写启动类
- 编写基础配置和路由规则
- 启动网关服务进行测试
(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)网关路由的流程图
整个访问的流程如下:
总结:
网关搭建步骤:
- 创建项目,引入nacos服务发现和gateway依赖
- 配置application.yml,包括服务基本信息、nacos地址、路由
接下来,就重点来学习路由断言和路由过滤器的详细知识
3.路由断言工厂
路由断言工厂 Route Predicate Factory
网关路由可以配置的内容包括:
- 路由id:路由的唯一标示
- 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
- 路由断言(predicates):判断路由的规则,
- 路由过滤器(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 # 这次跨域检测的有效期