视频学习:黑马程序员Java微服务

网盘资源:https://pan.baidu.com/s/1LxIxcHDO7SYB96SE-GZfuQ 提取码:dor4

学习路线及部分内容参考:Kyle’s Blog

微服务技术栈

什么是微服务,和SpringCloud的关系?微服务是分布式架构的一种,所谓的分布式架构就是把服务做拆分,在差分过程中就会出现各种各样的问题,SpringCloud只是解决了服务差分中的服务治理问题。至于其他的差分问题并没有包含。

微服务到底包含哪些知识?

  • 差分前:(传统的单体架构,代码间耦合程度高,后续升级和维护比较困难)

    • image-20230729134444662
  • 差分后:

    • image-20230729134201810

    • 在上述架构基础上进行自动化部署:

      image-20230729134732549


认识微服务

服务架构演变

单体架构

将业务的所有功能集中在一个项目中开发,打成一个包部署

优点:

  • 架构简单
  • 部署成本低

缺点

  • 耦合度高(维护困难、升级困难)

image-20230729135537705

分布式架构

根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务

优点

  • 降低服务耦合
  • 有利于服务升级和拓展

缺点

  • 服务调用关系错综复杂

image-20230729135714451

分布式架构需要考虑的问题:

  • 服务拆分粒度如何?
  • 服务集群地址如何维护?
  • 服务之间如何实现远程调用?
  • 服务健康状态如何感知?

微服务

微服务是一种经过良好架构设计的分布式架构方案,微服务特征如下:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题(例如积分服务挂了,不能影响到用户服务等其他服务)

image-20230729140319128

优点:

  • 拆分粒度更小
  • 服务更独立
  • 耦合度更低

缺点:

  • 架构非常复杂,运维、监控、部署难度提高

微服务技术对比:

Bubbo SpringCloud SpringCloudAlibaba
注册中心 zookeeper、redis Eureka、Consul Nacos、Eureka
服务远程调用 Dubbo协议 Feign Dubbo、Feign
配置中心 SpringCloudConfig SpringCloudConfig、Nacos
服务网关 SpringCloudGateway、Zuul SpringCloudGateway、Zuul
服务监控和保护 dubbo-admin、功能弱 Hystrix Sentinel

SpringCloud

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

常见组件:

image-20230729145922782

SpringCloud 底层是依赖于SpringBoot的,SpringCloud与SpringBoot的版本兼容如下:

Release Train Boot Version
2020.0.x aka llford 2.4.x
Hoxton 2.2.x,2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.X

本课程的学习版本是Hoxton.SR10,因此对应的是SpringBoot版本是2.3.x


服务的拆分和远程调用

服务拆分

注意事项

  1. 不同微服务,不要重复开发相同业务
  2. 微服务数据独立,不要访问其他微服务的数据库
  3. 微服务可以将自己的业务暴露为接口,供其他微服务调用

image-20230729150336023

示例

首先导入课程提供的工程 cloud-demo,项目的结构如下:

1
2
3
4
5
cloud-demo
|
|___order-service(根据id查询订单)
|
|___user-service(根据id查询用户)
  1. 首先在MySQL中创建两个对应的数据库,导入课程提供的sql文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    CREATE DATABASE cloud_order;
    USE cloud_order;
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for tb_order
    -- ----------------------------
    DROP TABLE IF EXISTS `tb_order`;
    CREATE TABLE `tb_order` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
    `user_id` bigint(20) NOT NULL COMMENT '用户id',
    `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
    `price` bigint(20) NOT NULL COMMENT '商品价格',
    `num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `username`(`name`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

    -- ----------------------------
    -- Records of tb_order
    -- ----------------------------
    INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
    INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
    INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
    INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
    INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
    INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
    INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
    INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);

    SET FOREIGN_KEY_CHECKS = 1;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    CREATE DATABASE cloud_user;
    USE cloud_user;
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for tb_user
    -- ----------------------------
    DROP TABLE IF EXISTS `tb_user`;
    CREATE TABLE `tb_user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
    `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `username`(`username`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

    -- ----------------------------
    -- Records of tb_user
    -- ----------------------------
    INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
    INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
    INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
    INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
    INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
    INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');

    SET FOREIGN_KEY_CHECKS = 1;
  2. 导入cloud-demo到IDEA,修改相应的配置

    配置文件:修改为自己的数据库和密码。此外,项目中使用的是mysql5版本,我这里用的8版本,所以需要加上cj

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: wzy123
    driver-class-name: com.mysql.cj.jdbc.Driver

    pom文件:首先是springboot版本修改,原始的2.3.9我这里启动不起来,这里改用为2.3.12

    1
    2
    3
    4
    5
    6
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/>
    </parent>

    并且修改对应的数据库连接驱动

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>
    </dependency>
  3. 启动这两个项目

    浏览器访问localhost:8081/user/1得到对应的用户数据

    1
    2
    3
    4
    5
    {
    "id": 1,
    "username": "柳岩",
    "address": "湖南省衡阳市"
    }

    浏览器访问localhost:8080/order/101得到对应的订单数据

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "id": 101,
    "price": 699900,
    "name": "Apple 苹果 iPhone 12 ",
    "num": 1,
    "userId": 1,
    "user": null
    }

此时,两个服务的数据库是分离的、对应的业务逻辑代码及配置也是分离的,因此实现了服务的差分

服务远程调用

需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回

image-20230730162507137

远程调用方式分析:

以往获取用户信息都是通过网页发送ajax请求到用户模块的controller,对应方法获取到进行数据库读取操作。那这里如果能让订单模块也发送同样的ajax请求,不就能获取到用户信息么。这里Java提供了RestTemplate来通过java代码发送ajax请求。

image-20230730162645905

RestTemplate

通过java代码发送ajax请求

在order-service服务中的OrderApplication启动类中,注册RestTemplate实例

1
2
3
4
5
6
7
8
9
10
11
12
13
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

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

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

示例

修改order-service服务中的queryById方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private RestTemplate restTemplate;

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

此时访问localhost:8080/order/101,查询的订单就包含了用户信息

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 101,
"price": 699900,
"name": "Apple 苹果 iPhone 12 ",
"num": 1,
"userId": 1,
"user": {
"id": 1,
"username": "柳岩",
"address": "湖南省衡阳市"
}
}

注意:上述查询不仅需要启动Order服务,也要启动User服务。不然就会报错,这也说明了其中包含了远程调用

消费者与提供者

在服务调用关系中有两个不同的角色

  • 服务提供者:一次业务中,被其他微服务调用的服务(提供接口给其他微服务)
  • 服务消费者:一次业务中,调用其他微服务的服务(调用其他微服务提供的接口)

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。比如,如果服务A调用了服务B,而服务B又调用的服务C,那么服务B的角色是什么?

  • 对于A调用B的业务而言:A是服务消费者,B是服务提供者
  • 对于B调用C的业务而言:B是服务消费者,C是服务提供者

因此服务B既可以是服务提供者,也可以是服务消费者


Eureka注册中心

远程调用问题

在上面的远程调用中,http请求的地址是硬编码在代码中,并且是固定写死了,如果有多个服务提供者,那么此时硬编码就不太合适了

服务调用出现的问题:

  • 服务消费者该如何获取服务提供者的地址信息
  • 如果有多个服务提供者,消费者应如何选取?
  • 消费者如何获取提供者的健康状态?

eureka原理

上述问题的解决:

  • 服务消费者该如何获取服务提供者的地址信息
    • 这里eureka-service注册中心将提供者的信息进行注册,消费者从注册中心拉取user-service的信息
  • 如果有多个服务提供者,消费者应如何选取?
    • 从实例列表中利用负载均衡算法选中一个实例地址,对其进行远程调用
  • 消费者如何获取提供者的健康状态?
    • user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自状态,这称为心跳续约
    • 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除

image-20230730205701816

总结:

在Eureka架构中,微服务角色有两类:

  • EurekaServer:服务端,注册中心
    • 记录服务信息
    • 心跳监测
  • EurekaClient:客户端
    • Provider:服务提供者,例如案例中的user-service
      • 注册自己的信息到EurekaServer
      • 每隔30s向EurekaServer发送心跳
    • Consumer:服务消费者,例如案例中的order-service
      • 根据服务名称从EurekaServer中拉取服务列表
      • 基于服务列表做负载均衡,选择其中一个微服务发起远程调用

搭建EurekaServer

  1. 首先在cloud-demo父工程下创建一个子模块eureka-server

  2. 在pom依赖中添加eureka服务端依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  3. 编写启动类

    1
    2
    3
    4
    5
    6
    7
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
    }
    }
  4. 编写配置文件application.yml

    为什么也需要eureka的服务名称呢?因为eureka自己也是一个微服务,所以eureka也会将自己注册为一个服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 10086 # 服务端口
    spring:
    application:
    name: eureka-server # eureka的服务名称
    eureka:
    client:
    service-url: # eureka的地址信息
    defaultZone: http://127.0.0.1:10086/eureka
  5. 启动服务

    启动main函数,然后在浏览器访问 http://localhost:10086/, 看到如下结果:

    image-20230730213229792

服务注册

  1. 在user-service的pom.xml文件中,引入eureka-client依赖

    1
    2
    3
    4
    5
    <!--引入eureka-client依赖-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 修改配置文件,添加服务名称、eureka地址

    1
    2
    3
    4
    5
    6
    7
    spring:
    application:
    name: user-service
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10086/eureka

同理,order-service也是同样的步骤,只是服务的名称进行对应修改即可

此时将三个服务都跑起来,然后访问10086端口查看,可以看到这三个服务都已经注册进来了

image-20230730214206703

同一服务启动多个实例

这里将user-service多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口设置。

  1. 在下方Services标签中右键,对指定配置进行copy

    image-20230730214424541

  2. 然后会弹出以下窗口,首先修改名称,然后需要修改对应端口防止冲突

    image-20230730214705273

    这里默认不显示VM option,需要点击Modify options,然后勾选VM option,然后就会出现对应位置,填写对应端口-Dserver.port=8082

    image-20230730214841783

此时到浏览器中查看,可以看到出现两个实例

image-20230730215003848

服务发现

服务发现也就是前面说的服务拉取,服务消费者从注册中心获取服务提供者列表,然后再对服务列表做负载均衡

实现:

  1. 修改OrderService的代码,修改访问路径,用服务名代替ip、端口

    这里的服务名就是前面配置文件中的application.name

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2. 利用RestTemplate发起http请求,查询User
    // 2.1 url地址
    - String url = "http://localhost:8081/user/" + order.getUserId();
    + String url = "http://user-service/user/" + order.getUserId();
    // 2.2 发起http请求,实现远程调用
    User user = restTemplate.getForObject(url, User.class);
    // 3. 将user封装到order
    order.setUser(user);
    // 4.返回
    return order;
    }
  2. 在order-service项目的启动类OrderApplication中,给RestTemplate这个Bean添加负载均衡注解@LoadBalanced

    1
    2
    3
    4
    5
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }

验证:

因为我们前面开起两个user-service的实例,这里我们访问两次localhost:8080/order/+id,查看两个实例的输出日志

对应两个服务都输出了一次查询日志,说明实现了负载均衡,这里只是简单的轮询

1
2
3
07-30 22:00:31:641 DEBUG 18352 --- [nio-8082-exec-1] c.i.user.mapper.UserMapper.findById      : ==>  Preparing: select * from tb_user where id = ? 
07-30 22:00:31:661 DEBUG 18352 --- [nio-8082-exec-1] c.i.user.mapper.UserMapper.findById : ==> Parameters: 1(Long)
07-30 22:00:31:681 DEBUG 18352 --- [nio-8082-exec-1] c.i.user.mapper.UserMapper.findById : <== Total: 1
1
2
3
4
07-30 22:00:36:907 DEBUG 1648 --- [nio-8081-exec-1] c.i.user.mapper.UserMapper.findById      : ==>  Preparing: select * from tb_user where id = ? 
07-30 22:00:36:929 DEBUG 1648 --- [nio-8081-exec-1] c.i.user.mapper.UserMapper.findById : ==> Parameters: 3(Long)
07-30 22:00:36:950 DEBUG 1648 --- [nio-8081-exec-1] c.i.user.mapper.UserMapper.findById : <== Total: 1
07-30 22:01:11:377 INFO 1648 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration

Ribbon负载均衡

负载均衡原理

SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能

image-20230730220509275

之前在进行服务远程调用时还需要获取ip和端口,但现在输入了service名称就可以访问了。显然是Ribbon帮我们根据service名称,获取到了服务实例的ip和端口。这里用到的是LoadBalancerInterceptor,这个类会对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法,得到真实的服务地址信息,替换服务名称

这里从源码角度查看Ribbon负载均衡实现的原理:

这里连按两次shft即可调出类搜索;这里debug order-service项目,访问http://localhost:8080/order/101

  1. 首先进入LoadBalancerInterceptor类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
    this.loadBalancer = loadBalancer;
    this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
    this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
    }

    可以看到这里的intercept方法,拦截了用户的HTTPRequest请求

    1. request.getURI():获取请求uri,这里是http://user-service/user/1
    2. originalUri.getHost():获取uri路径的主机名,其实就是服务id,user-service
    3. this.loadBalancer.execute:处理服务id和用户请求

    image-20230731000209821

  2. 然后上面调用了loadBalancer.execute方法,这里继续跟入

    1
    2
    3
    4
    5
    6
    7
    public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;

    URI reconstructURI(ServiceInstance instance, URI original);
    }

    快捷键ctrl+alt+B进入其实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
    ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
    Server server = this.getServer(loadBalancer, hint);
    if (server == null) {
    throw new IllegalStateException("No instances available for " + serviceId);
    } else {
    RibbonServer ribbonServer = new RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
    }
    }
    1. getLoadBalancer(serviceId):根据服务id获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来
    2. getServer(loadBalancer, hint):利用内置的负载均衡算法,从服务列表中选择一个,本例中,可以看到获取到的是8081端口
  3. 负载均衡策略IRule。

    获取服务是通过一个getServer的方法来做负载均衡,继续跟入,会发现这样一段代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public Server chooseServer(Object key) {
    if (this.counter == null) {
    this.counter = this.createCounter();
    }

    this.counter.increment();
    if (this.rule == null) {
    return null;
    } else {
    try {
    return this.rule.choose(key);
    } catch (Exception var3) {
    logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
    return null;
    }
    }
    }

    这里this.rule.choose(key)进行服务选择,rule默认值是一个RoundRobinRule,也就是轮询。

整体的原理如下:

image-20230730234211963

负载均衡策略

Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:

image-20230731112511231

这里罗列了一些常见的规则

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略:
(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。
(2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

默认的实现就是ZoneAvoidanceRule,是一种轮询方案

自定义负载均衡策略

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

  1. 代码方式:在order-service中的OrderApplication启动类中,定义一个IRule,此种方式定义的负载均衡规则,对所有微服务均有效

    1
    2
    3
    4
    @Bean
    public IRule randomRule(){
    return new RandomRule();
    }
  2. 配置文件方式:在order-service中的application.yml文件中,添加新的配置也可以修改规则

    1
    2
    3
    user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

注意:一般使用默认的负载均衡规则,不做修改

饥饿加载

Ribbon默认是采用懒加载,即第一次访问时,才回去创建LoadBalanceClient,请求时间会很长。而饥饿加载在则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载

1
2
3
4
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: user-service # 指定对user-service这个服务进行饥饿加载,可以指定多个服务

如果加载多个服务,换行加-即可,示例:

1
2
3
clients: 
- user-service
- xxx-service

Nacos注册中心

认识和安装Nacos

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

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

Nacos的默认端口是8848,如果其它进程占用了8848端口,请先尝试关闭该进程。如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件application.properties中的server.port

Nacos的启动

  • 进入bin目录,打开cmd窗口执行startup.cmd -m standalone命令即可
  • 然后在浏览器访问http://localhost:8848/nacos ,默认的登录账号和密码都是nacos

image-20230731131028441

服务注册到Nacos

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos与使用Eureka对于微服务来说,并没有太大区别。服务消费者和提供者的代码都不需要做改变,只需要改变依赖和服务地址即可

引入依赖:

在cloud-demo父工程的pom.xml文件中引入SpringCloudAlibaba的依赖

1
2
3
4
5
6
7
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

在user-service和order-service中的pom文件注释掉eureka依赖,引入nacos-discovery依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置Nacos地址

在user-service和order-service的application.yml中添加Nacos地址,注释掉原来的eureka

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:8848

重启服务

启动之后,查看浏览器nacos控制台,可以在服务列表中看到以下内容

image-20230731132215305

Nacos服务分级存储模型

一个服务可以有多个实例,如果把多个实例都放在一个机房,如果出现意外,就会一网打尽,所以更多是把实例分散到各地,从而实现容灾。这里以机房划分集群。

image-20230731132458485

微服务相互访问时,应该尽可能访问同集群实例,因为本地访问速度更快,房本集群内不可用时,才去访问其他集群

服务集群属性

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

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称,杭州

首先启动一个user-service实例,然后修改集群名称为SH,然后再启动第二个实例,此时查看Nacos控制台,就能看到这两个集群

image-20230731133157245

Nacos服务分级存储模型

  1. 一级是服务,例如user-service
  2. 二级是集群,例如杭州或上海
  3. 三级是实例,例如杭州机房的某台部署了user-service的服务器

同集群优先的负载均衡

默认的ZoneAvoidanceRule并不能根据同集群优先来实现负载均衡。

  • 给order-service配置集群信息,修改其application.yml文件,将集群名称配置为HZ

    1
    2
    3
    4
    5
    6
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848
    discovery:
    cluster-name: HZ # 集群名称,杭州
  • 然后浏览器访问localhost:8080/order/101多次,查看三个服务(order-HZ、user-HZ、user-SH)的日志,可以看到,我们order发送的请求并不是只在HZ的集群中,也来到了SH,因此没有根据同集群优先来实现负载均衡

因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例

  • 在order-service的配置文件中修改负载均衡规则,设置IRule为NacosRule

    1
    2
    3
    user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
    ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

此时再像之前测试,此时只在本地集群中进行请求传递,并且本地集群中同一实例的轮询

此时如果将集群名称为HZ的user-service服务停掉,那么现在访问http://localhost:8080/order/101, 则集群名称为SH的user-service会输出日志。这说明Nacos优先访问本地,本地没有就会访问其他集群的服务,并发起警告。

根据权重负载均衡

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

  • 服务器设备性能由差异溢,部分实例所在的机器性能较好,而另一些较差,我们希望性能好的机器承担更多的用户请求
  • 但默认情况下NacosRule是统计群内随机挑选,不会考虑机器性能的问题

Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高

  1. 在Nacos控制台可以设置实例的权重值,首先选中实例后面的编辑按钮

    image-20230731134912372

  2. 然后在弹出的窗口中对权重继续修改即可(一般是在0~1之间)

    image-20230731135004943

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

  • 实际场景:我们想要对服务进行升级,所以需要重启服务器,但是这就需要半夜没用户使用时进行,极为不便。但是现在用Nacos的话,可以将某一台服务器的权重设置为0,逐渐就没有用户访问,然后再重启进行升级,再调回权重,以此类推实现服务升级。

环境隔离

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

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

image-20230731152224511

创建命名空间:

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

  • 点击命名空间 -> 新建命名空间 -> 填写表单,可以创建一个新的namespace

    id不填默认使用UUID生成

    image-20230731152419657

  • 此时再查看服务列表,就能看到创建的dev命名空间

    image-20230731152631530

给微服务配置namespace:

  • 修改order-service的application.yml文件,添加namespace内容,注意填写的是命名空间的id

    1
    2
    3
    4
    5
    6
    7
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848
    discovery:
    cluster-name: HZ
    + namespace: 3d542282-a73b-4af9-81fe-6baf05ccf3e5 # 命名空间,填上图中的命名空间ID
  • 此时可以从服务列表看到,order已经到了dev命名空间下:

    image-20230731152940782

    并且此时如果在进行访问localhost:8080/order/101,就会报错,因为此时user-service和order-service从属于不同的命名空间,中间存在隔离,因而无法访问

Nacos和Eureka的区别

Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异

  • Nacos中的实例分为临时实例和非临时实例。

    • 临时实例采用心跳检测,定时向注册中心报告自己的健康状态;并且如果实例宕机超过一定时间,会从服务列表剔除
    • 非临时实例则是nacos主动向实例询问,查看其状态;如果实例宕机,不会从服务列表剔除,只是标记为不健康,等待服务恢复

    默认的实例都是临时实例。因为非临时实例给服务器造成的压力较大,一般不使用

  • nacos注册中心支持服务列表变更的消息推送模式,服务列表更新会主动推送给消费者变更消息,更新更及时

  • Nacos集群默认采用AP方式,当急群众存在非临时实例时,采用CP模式;Eureka采用AP方式

image-20230731160052825

配置一个服务实例为永久实例

1
2
3
4
5
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例

Nacos配置管理

统一配置管理

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

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

在Nacos中添加配置文件

在浏览器的Nacos页面,找到配置管理,点击配置列表,然后点击右侧加号新增配置

image-20230731161836354

在新建配置表单中填写对应内容

img

注意:只有需要热更新的配置才有放到Nacos管理的必要,基本不会变更的一些配置,还是保存到微服务本地比较好(例如数据库连接配置等)

从微服务拉取配置

现在相较于之前,改动只是在读取本地配置文件前先读取nacos中的配置文件,但是读取nacos配置文件首先要获取nacos地址,但是这个地址在本地配置文件中,那如何获取nacos地址呢?

Spring引入了一种新的配置文件:bootstrap.yml文件,会在application.yml之前被读取

image-20230731164208874

步骤:

  1. 引入nacos-config依赖

    在user-service服务中,引入nacos-config的客户端依赖

    1
    2
    3
    4
    5
    <!--nacos配置管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 在user-service的resources文件夹下新建一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    application:
    name: user-service # 服务名称
    profiles:
    active: dev #开发环境,这里是dev
    cloud:
    nacos:
    server-addr: localhost:8848 # Nacos地址
    config:
    file-extension: yaml # 文件后缀名
    namespace: 3d542282-a73b-4af9-81fe-6baf05ccf3e5 # 因为配置文件实在dev命名空间新增的,所以添加命名空间

    这里name+active+file-extention结合就是之前在Nacos中配置的名称,即user-service-dev.yaml

    最后一行是我自己加的,因为老师是在public命名空间下做的,我这里是在dev

  3. 此时注释掉原本application.yml中重复的文件,比如服务名、nacos地址

测试:

测试是否真的读取到了,在user-service的UserController中添加业务逻辑,读取nacos中的配置信息pattern.dateformat配置

1
2
3
4
5
6
7
@Value("${pattern.dateformat}")
private String dateformat;

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

此时打开浏览器访问localhost:8081/user/now,可以看到如下结果,说明配置成功

image-20230731171422154

配置热更新

前面的配置还存在问题,现在修改一下配置内容

1
2
3
pattern:
# dateformat: yyyy-MM-dd HH:mm:ss
dateformat: yyyy年MM月dd日 HH:mm:ss

然后刷新页面,发现得到的时间还是用-分隔,没有做到热更新。这还需要下面两种配置实现:

  • 方式一:在@Value注入的变量类上添加注解@RefreshScope(刷新作用域)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
       @Slf4j
    @RestController
    @RequestMapping("/user")
    + @RefreshScope
    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));
    }

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

    此时再访问,得到的时间就是年月日

    image-20230731172724764

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

    在user-service服务中,添加一个类config.PatternProperties,读取pattern.dateformat属性

    1
    2
    3
    4
    5
    6
    @Component
    @Data
    @ConfigurationProperties(prefix = "pattern")
    public class PatternProperties {
    private String dateformat;
    }

    在UserController中进行修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Slf4j
    @RestController
    @RequestMapping("/user")
    //@RefreshScope
    public class UserController {
    @Autowired
    private PatternProperties patternProperties;

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

    这种方法可以跟上面达到相同效果

配置共享

微服务启动时会去Nacos读取多个配置文件

  • [spring.application.name]-[spring.profiles.active].yaml,例如:user-service-dev.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

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

示例:

  1. 在Nacos中创建一个新的配置,user-service.yaml,添加如下内容

    1
    2
    pattern:
    envSharedValue: 多环境共享属性值

    image-20230803111948077

  2. 修改user-service中的PatternProperties类,添加envSharedValue

    1
    2
    3
    4
    5
    6
    7
    @Component
    @Data
    @ConfigurationProperties(prefix = "pattern")
    public class PatternProperties {
    private String dateformat;
    + private String envSharedValue;
    }
  3. 修改UserController,添加一个方法,用于展示其中的属性赋值情况

    1
    2
    3
    4
    @GetMapping("/prop")
    public PatternProperties prop(){
    return patternProperties;
    }

测试:

首先开启user-service服务(使用dev环境),然后另开启一个服务但是修改环境为test,右键服务,然后选择Edit Configuration,在Active profiles中填写环境即可。

image-20230803113159773

然后访问得到以下结果:

dev

1
2
3
4
{
"dateformat": "yyyy年MM月dd日 HH时mm分ss秒",
"envSharedValue": "多环境共享属性值"
}

test

1
2
3
4
{
"dateformat": null,
"envSharedValue": "多环境共享属性值"
}

可以看出,这两个环境都读取到了user-service.yaml文件中的内容,并且有各自独特的内容xx-profile.yaml

配置共享的优先级

当Nacos、服务本地同时出现相同属性时,优先级也有高低之分:服务名-profile.yaml > 服务名.yaml > 本地配置

  • user-service-dev.yaml > user-service.yaml > application.yaml

搭建Nacos集群

Nacos生产环境下一定要部署为集群状态

集群结构图

image-20230803113904248

搭建集群

总体步骤:

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

具体步骤:

  1. 搭建数据库,初始化数据库表结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    CREATE DATABASE IF NOT EXISTS nacos;
    USE nacos;
    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

    进入Nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf`,然后替换原本内容为以下内容

    如果后面启动报错了,就把这里的127.0.0.1换成本机真实IP(我这里就报错了,所以换了本机IP,使用ipconfig命令查看)

    1
    2
    3
    127.0.0.1:8845
    127.0.0.1:8846
    127.0.0.1:8847

    还是在这个文件夹,修改application.properties文件,添加数据库配置,这里主要是以下内容去掉注释,并修改为自己的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ### If use MySQL as datasource:
    spring.datasource.platform=mysql

    ### Count of DB:
    db.num=1

    ### Connect URL of DB:
    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=wzy123
  3. 启动Nacos集群

    将nacos文件夹复制3份,分别命名为:nacos1、nacos2、nacos3。然后分别修改这三个文件夹中的application.properties

    • nacos1:server.port=8845
    • nacos2:server.port=8846
    • nacos3:server.port=8847

    然后将三个都启动,此时还是在conf文件夹下打开cmd命令,但是此时启动命令有变化:startup.cmd即可

  4. Nginx反向代理

    修改conf/nginx.conf文件,将下面的配置粘贴到http块中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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:在nignx的根目录下执行start nginx.exe即可

测试:

此时在浏览器访问http://localhost/nacos 即可,此时nginx就对这三个集群做了负载均衡

image-20230803121159628

然后新增一个配置,可以看到数据库中也进行了保存

image-20230803123459275

Java代码中需要将将bootstrap.yml中的Nacos地址修改为localhost:80

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址

启动服务,可以看到:

image-20230803124703670


Feign远程调用

Feign的本意是假装、伪装的意思,它可以把Rest的请求进行隐藏,伪装成类似SpringMvc的Controller一样,不用再自己拼接url,拼接参数等,一切都交给Feign做。

Feign是Netflix开发的声明式、模板化的HTTP客户端,可以帮助我们更加便捷、优雅地调用HTTP Api。

Feign替代RestTemplate

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

1
2
String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);

存在以下问题:

  1. 代码可读性差,编程体验不统一
  2. 参数复杂的URL难以维护(百度随便搜一个中文名词,然后看一下url有多长,有多少参数)

我们可以利用Feign来解决上面提到的问题。Feign是一个声明式的http客户端,官网地址https://github.com/OpenFeign/feign, 其作用就是帮助我们优雅的实现http请求的发送

使用Feign:

  1. 引入依赖

    在order-service服务的pom文件中引入Feign的依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 添加注解

    在order-service的启动类上添加@EnableFeignClients注解,开启Feign的功能

  3. 编写Feign客户端

    在order-service中新建com.itcast.order.client包,然后新建一个接口,内容如下

    1
    2
    3
    4
    5
    @FeignClient("user-service")
    public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }

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

    1. 服务名称:user-service
    2. 请求方式:GET
    3. 请求路径:/user/{id}
    4. 请求参数:Long id
    5. 返回值类型:User

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

  4. 修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2. 利用Feign发起http请求,查询User
    User user = userClient.findById(order.getUserId());
    // 3. 将user封装到order
    order.setUser(user);
    // 4.返回
    return order;
    }

此时再次访问浏览器localhost:8080/order/101就能查询到订单信息以及用户信息

自定义配置

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即可,下面以日志为例来演示如何自定义配置

配置文件方式

基于配置文件修改Feign的日志级别可以针对单个服务

1
2
3
4
5
feign:  
client:
config:
user-service: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别

也可以针对所有服务

1
2
3
4
5
feign:  
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别

而日志的级别分为四种

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

此时就可以看到日志:

image-20230803202857376

Java代码方式

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

config.DefaultFeignConfiguration

1
2
3
4
5
6
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; //日志级别设置为 BASIC
}
}

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

1
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)

如果是局部生效,则把它放到对应的@FeignClient注解中

1
@FeignClient(value = "user-service", configuration = DefaultFeignConfiguration.class)

Feign使用优化

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

  1. URLConnection:默认实现,不支持连接池
  2. Apache HttpClient:支持连接池
  3. OKHttp:支持连接池

因此优化Feign的性能主要包括:

  • 使用连接池代替默认的URLConnection
  • 日志级别,最好使用basic或者none

这里以HttpClinet为例

  1. 引入依赖

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

    1
    2
    3
    4
    5
    <!--httpClient的依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 配置连接池

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    feign:
    client:
    config:
    default: # default全局的配置
    logger-level: BASIC # 日志级别,BASIC就是基本的请求和响应信息
    httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

最佳实践

这里最佳实践指的是在使用过程中总结的经验,实际使用中最好的一种使用方式。

在之前,Feign的客户端与服务提供者的controller代码十分相似

FeignClient

1
2
3
4
5
@FeignClient(value = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}

其中大部分代码都一样,那么是否能够简化这种重复代码的编写呢?

继承方式

给消费者的FeignClient和提供者Controller定义统一的父接口最为标准

image-20230803205157727

这里UseAPI是抽取出的一个父接口,让FeignClient和Controller方法继承这个父接口

总结:

  • 优点

    1. 简单
    2. 实现了代码共享
  • 缺点

    1. 服务紧耦合(服务提供方、服务消费方紧耦合)
    2. 父接口参数列表中的映射不会被继承(参数列表中的注解映射并不会继承,所以Controller中必须再次声明方法、参数列表、注解)

抽取方式

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

image-20230803205811561

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

实现基于抽取的实践

  • 首先创建一个新的module,命名为feign-api,然后在pom文件中引入feign的starter依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  • 然后将order-service中编写的UserClient、User、DefaultFeignConfiguration(我这里是基于配置文件实现,所以没有这个配置类)都复制到feign-api项目中

    image-20230803224531858

  • 然后删除order-service中的以上内容,在order-service的pom文件中引入 feign-api的依赖

    1
    2
    3
    4
    5
    6
    <!--引入feign-api-->
    <dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
    </dependency>
  • 修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包

注意:这里会出现UserClinet Could not autowire. No beans of 'UserClient' type found. 的问题,这是因为包扫描的问题。因为现在UserClient在cn.itcast.feign.clients包下,而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。解决方法如下:

  • 方式一:指定FeignClient所在的包

    1
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
  • 方式二:指定FeignClient字节码(更推荐)

    1
    @EnableFeignClients(clients = {UserClient.class})

GateWay服务网关

为什么需要网关

网关功能:

  • 身份验证和权限校验
    • 网关作为微服务的入口,需要校验用户是否有请求资格,如果没有则拦截
  • 服务路由、负载均衡
    • 一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫路由。当然路由的目标服务有多个时,还需要做负载均衡
  • 请求限流
    • 当请求量过高时,在网关中按照微服务能够接受的速度来放行请求,避免服务压力过大

image-20230803230111096

gateway快速入门

搭建网关服务的步骤:

  1. 创建新的module,引入SpringCloudGateway的依赖和Nacos的服务发现依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--网关-->
    <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. 编写启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class GatewayApplication {
    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
    }
    }
  3. 编写路由配置及nacos地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    server:
    port: 10010
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: localhost:8848
    gateway:
    routes:
    - id: user-service # 路由id,自定义,只需要唯一即可
    uri: lb://user-service # 路由的目标地址,lb表示负载均衡,后面跟服务名称
    # uri: http://localhost:8081 # 路由的目标地址,http就是固定地址
    predicates: # 路由断言,判断请求是否符合路由规则的条件
    - Path=/user/** # 路径断言,判断路径是否以/user开头的,如果是则符合规则

    - id: order-service # 按照上面的写法,再配置一下order-service
    uri: lb://order-service
    predicates:
    - Path=/order/**

    这里url支持两种格式:lb、http

测试:

网关路由流程:

image-20230804100704210

总结

  • 网关搭建的步骤
    1. 创建项目,引入nacos和gateway依赖
    2. 配置application.yml,包括服务基本信息,nacos地址、路由
  • 路由配置包括
    1. 路由id:路由的唯一表示
    2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名称负载均衡
    3. 路由断言(predicates):判断路由的规则
    4. 路由过滤器(filters):对请求或相应做处理

断言工厂

断言工厂其实就是读取用户定义的断言规则,将其解析,并对请求做出判断。

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

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

像这样的断言工厂,在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=.somehost.org,.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 权重处理

关于更详细的使用方法,可以参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

过滤器工厂

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

image-20230804101532273

Spring提供了31中不同的路由过滤器工厂,例如

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

具体用法参考官方文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

使用示例

需求:给所有进入user-service的请求都添加一个请求头:Truth=YSKM

实现:

  1. 修改gateway服务的application.yml文件,添加路由过滤即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
        gateway:
    routes:
    - id: user-service # 路由id,自定义,只需要唯一即可
    uri: lb://user-service # 路由的目标地址,lb表示负载均衡,后面跟服务名称
    # uri: http://localhost:8081 # 路由的目标地址,http就是固定地址
    predicates: # 路由断言,判断请求是否符合路由规则的条件
    - Path=/user/** # 路径断言,判断路径是否以/user开头的,如果是则符合规则
    + filters:
    + - AddRequestHeader=Truth, YSKM
  2. 在UserController中编写对应方法测试

    因为上述配置的过滤器是针对user-service服务的,所以只在这里生效

    1
    2
    3
    4
    @GetMapping("/truth")
    public void getTruth(@RequestHeader("Truth") String truth){
    System.out.println(truth);
    }

测试:重启网关和user-service,然后访问localhost:10010/user/truth,可以看到控制台打印:YSKM


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- id: order-service # 按照上面的写法,再配置一下order-service
uri: lb://order-service
predicates:
- Path=/order/**
+ default-filters:
+ - AddRequestHeader=Truth, YSKM

总结:

  • 过滤器的作用是什么?
    • 对路由的请求或响应做加工处理,比如添加请求头
    • 配置在路由下的过滤器只对当前路由请求生效
  • default-filters的作用是什么?
    • 对所有路由都生效的过滤器

全局过滤器

简介

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

定义的方式就是实现GlobalFilter接口

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

使用示例

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

  1. 参数中是否有authorization
  2. authorization参数值是否为admin

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

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Order(-1) //设置执行顺序,可能存在多个过滤器
@Component
public class AuthorizeFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
//2. 获取参数中的authorization参数
String auth = params.getFirst("authorization");
//3. 判断是否等于admin
if ("admin".equals(auth)){
//4. 是,放行
return chain.filter(exchange);
}
//5. 否,拦截
//5.1 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//这里也就是401,表示未登录
//5.2 拦截请求
return exchange.getResponse().setComplete();
}
}

测试:

重启网关测试拦截器是否生效,打开浏览器访问http://localhost:10010/user/1,无法正常访问:

image-20230804112430936

加上authorization请求参数访问http://localhost:10010/user/1?authorization=admin, 才能得到数据:

image-20230804112534002

过滤器的执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter。请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

image-20230804114454952

排序的规则:

  • 上一部分内容中说到,每个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前(默认值为2147483647,即int最大值)

  • order的指定:

    • GlobalFilter的order由我们自己决定,可以使用@Order注解(这里前面演示了),或者实现Ordered接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class AuthorizationFilter implements GlobalFilter, Ordered {
      @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.UNAUTHORIZED);
      return exchange.getResponse().setComplete();
      }

      @Override
      public int getOrder() {
      return -1;
      }
      }
    • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增

  • 当过滤器的order值一样时,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

跨域问题

什么是跨域问题

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

  1. 域名不同:www.baidu.comwww.baidu.orgwww.js.commiaosha.js.com
  2. 域名相同,端口不同:localhost:8080和localhost:8081

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。这是由于浏览器的同源策略所致,所谓的同源策略,其实就是协议、主机、端口都相同的,才可以互相访问,否则只要有一个不同,是不能访问的。

解决方案:CORS

  • CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。
  • 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

解决跨域问题

网关处理跨域同样是CORS方案,只需要在gateway服务的application.yml文件中,添加下面的配置即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

注意:这里的http://localhost:8090就是前端访问的端口,但是此时后端使用gateway的端口是10010,所以就会产生跨域问题