这一节系统地学习一下Spring Cloud这个微服务框架。本文篇幅很长,并且知识点讲解全面详细,我截稿时已经有6万字了,建议大家收藏后慢慢学习。看在我这么辛苦整理知识点的份上,大家可以点个关注和点赞吗?谢谢大家,这对我真的很重要!
一.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文件。
<?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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<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子项目:
<?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>
<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子项目:
<?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>
<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配置文件:
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类的包的路径)
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类的相关代码:
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;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
service层的代码:
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:
package com.haiexijun.user.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
}
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子项目的代码:
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层:
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) {
return orderService.queryOrderById(orderId);
}
}
service层:
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) {
Order order = orderMapper.findById(orderId);
return order;
}
}
pojo和mapper:
package com.haiexijun.order.pojo;
import lombok.Data;
@Data
public class User {
private Long id;
private String username;
private String address;
}
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;
}
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。
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);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
完成了这一步以后,我们接下来就可以利用它来发HTTP请求了。
所以我们下面要对订单的查询业务进行修改:
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) {
Order order = orderMapper.findById(orderId);
String url="http://localhost:8081/user/"+order.getUserId();
User user= restTemplate.getForObject(url, User.class);
order.setUser(user);
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,然后子项目会引入下面的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
(2)创建启动类,添加@EnableEurekaServer注解
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配置文件中添加如下的配置(这里最好复制,不然容易出错):
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
下面就可以运行起来了。 idea的功能很强大,我们点击之后会跳转到Eureka的界面,如下图所示: 这个界面的Instances currently registered with Eureka就是注册到Eureka中的实例的列表。上面我们看见了,eureka也注册了自己的实例。
3.服务注册
服务注册只要经过以下两个步骤就够了: (1)在user-service的pom文件中,引入下面的eureka-client依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)编写user-service的application.yml文件,内容如下:
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:
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);
}
@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:
@Bean
public IRule randomRule(){
return new RandomRule();
}
(2)配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意,一般用默认的负载均衡规则就好,不做修改。
4.饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。饥饿加载就好比是饿了,看到什么都往上吭,可以选择那些服务,一上来就想去把这些服务加载进来,不管三七二十一。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true
clients: userservice
如果是多个服务要换个行:
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的版本。不要用错了。
我们解压到一个非中文的目录下就行了,如图: 目录说明:
(2)端口配置
Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。
如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口: (3) 启动
启动非常简单,进入bin目录,结构如下: 然后执行命令即可:
执行后的效果如图,并且不会报错: (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)解压
命令解压缩安装包:
tar -xvf nacos-server-2.1.0.tar.gz
然后删除安装包:
rm -rf nacos-server-2.1.0.tar.gz
(4)端口配置与windows中类似
(5)启动 在nacos/bin目录中,输入命令启动Nacos:
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依赖:
<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地址:
spring:
cloud:
nacos:
server-addr: localhost:8848
这里也不要忘记把之前Eureka的配置删除掉
更改后的yml分别如下:
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
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
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
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文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
编写好配置文件后,我们先启动前面的两个服务实例: 然后我们要配置第三个实例的集群,我们再修改yml配置文件:
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配置集群信息(上一节配好了,就可以不用配了)
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
(2) 修改负载均衡规则 修改order-service的application.yml文件,修改负载均衡规则:
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文件:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: b4401444-96e0-4b9a-a58b-9b24b74bfdfd
重启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类型:
配置一个服务实例为永久实例(非临时实例):
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的客户端依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
(2)添加bootstrap.yaml 然后,在user-service的resources目录中添加一个bootstrap.yaml文件,内容如下:
spring:
application:
name: userservice
profiles:
active: dev
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} 作为文件id,来读取配置。
本例中,就是去读取userservice-dev.yaml : (3)读取nacos配置 在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置: 全部代码如下:
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;
@GetMapping("/now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
在页面访问,可以看到效果: 到这里就实现了配置管理的第二步,微服务获取Nacos中的配置了。
3.配置热更新
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
要实现配置热更新,可以使用两种方式:
方式一:
方式二: 使用@ConfigurationProperties 注解代替@Value注解。
在user-service服务中,添加一个类,读取patterrn.dateformat属性:
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: 完整代码:
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:
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';
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='增加租户字段';
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';
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';
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';
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容量信息表';
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='多租户改造';
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 将这个包解压到任意非中文目录下,如图: 目录说明:
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf: 然后添加内容:
10.129.186.226:8845
10.129.186.226:8846
10.129.186.226:8847
然后修改application.properties文件,添加数据库配置:
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:
server.port=8845
nacos2:
server.port=8846
nacos3:
server.port=8847
然后分别启动三个nacos节点(现在是集群启动,不用加-m什么的了,默认就是集群启动):
每个nacos都要运行一遍哦!
startup.cmd
(5)nginx反向代理 下载Nginx,然后解压到任意非中文目录下: 修改conf/nginx.conf文件,配置如下(把它粘贴到http{ }的内部就好了):
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):
start nginx.exe
而后在浏览器访问:http://localhost/nacos即可。
项目代码中application.yml文件配置如下:
spring:
cloud:
nacos:
server-addr: localhost:80
(6)优化
这一小节的一些坑:
-
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的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2) 添加注解 在order-service的启动类添加注解开启Feign的功能:
@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中新建一个接口,内容如下:
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的日志级别可以针对单个服务:
feign:
client:
config:
userservice:
loggerLevel: FULL
也可以针对所有服务:
feign:
client:
config:
default:
loggerLevel: FULL
而日志的级别分为四种:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
方式二Java代码方式 也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}
如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是局部生效,则把它放到对应某一个服务的@FeignClient这个注解中:
@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依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
(2) 配置连接池
在order-service的application.yml中添加配置:
feign:
client:
config:
default:
loggerLevel: BASIC
httpclient:
enabled: true
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都集成改接口 优点:
缺点:
方式二:抽取方式 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
5. Feign 最佳实践的代码实现
下面以抽取的方式来实现Feign的最佳实践。
(1) 抽取
首先创建一个module,命名为feign-api: 项目结构: 在feign-api中然后引入feign的starter依赖:
<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的依赖:
<dependency>
<groupId>com.haiexijun.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包
(3) 重启测试
(4) 解决扫描包问题
方式一:
指定Feign应该扫描的包:
@EnableFeignClients(basePackages = "com.haiexijun.feign.clients")
方式二:
指定需要加载的Client接口:
@EnableFeignClients(clients = {UserClient.class})
八.Gateway服务网关
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
1. 为什么需要网关
Gateway网关是我们服务的守门神,所有微服务的统一入口。
我们的微服务如果直接任人都能发请求来访问是不是不太安全啊。你要知道,不是所有的业务都是对外公开的,有好多业务属于公司内部的或者管理人员才可以去访问的,所以得对用户的身份做一个认证,如果说是我们的内部人员,才允许访问一些服务。网关就是来做这样一件事情的。
网关的核心功能特性:
架构图:
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照微服务能够接受的速度来放行请求,避免服务压力过大。
在SpringCloud中网关的实现包括两种:
Zuul是基于Servlet的实现,属于阻塞式编程。而Spring Cloud Gateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。所以我们选择Spring Cloud Gateway。
2. Gateway快速入门
下面,我们就演示下网关的基本路由功能。基本步骤如下:
- 创建SpringBoot工程gateway,引入网关依赖
- 编写启动类
- 编写基础配置和路由规则
- 启动网关服务进行测试
(1)创建gateway服务,引入依赖
创建项目: 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
(2)编写启动类
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文件,内容如下:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/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的路由添加过滤器:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters:
- AddRequestHeader=Truth, haiexijun is freaking awesome!
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。
然后,我们更改一下我们的UserController的代码,来测试一下。
@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下。格式如下:
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接口。
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
在filter中编写自定义逻辑,可以实现下列功能:
下面来实践一下自定义全局过滤器
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
-
参数中是否有authorization, -
authorization参数值是否为admin
如果同时满足则放行,否则拦截
实现:
在gateway中定义一个过滤器:
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) {
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
String auth = params.getFirst("authorization");
if ("admin".equals(auth)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
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 ,因此我们以前所学的那些解决方案不一定能够适用。
下面先来回顾一下跨域:
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedOrigins:
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods:
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*"
allowCredentials: true
maxAge: 360000
|