【微服务】第15节:动态路由&配置热部署

目录

1.配置管理

1.1.配置共享

1.1.1.添加共享配置

1.1.2.拉取共享配置

1.2.配置热更新

1.2.1.添加配置到Nacos

1.2.2.配置热更新

1.3.动态路由

1.3.1.监听Nacos配置变更

1.3.2.更新路由

1.3.3.实现动态路由


1.配置管理

到目前为止我们已经解决了微服务相关的几个问题:

🔹 微服务远程调用

🔹 微服务注册、发现

🔹 微服务请求路由、负载均衡

🔹 微服务登录用户信息传递

不过,现在依然还有几个问题需要解决:

🔹 网关路由在配置文件中写死了,如果变更必须重启微服务

🔹 某些业务配置在配置文件中写死了,每次修改都要重启服务

🔹 每个微服务都有很多重复的配置,维护成本高

这些问题都可以通过统一的配置管理器服务解决。而Nacos不仅仅具备注册中心功能,也具备配置管理的功能:

微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。

网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。

1.1.配置共享

我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:

  • 在Nacos中添加共享配置

  • 微服务拉取配置

1.1.1.添加共享配置

以cart-service为例,我们看看有哪些配置是重复的,可以抽取的:

首先是jdbc相关配置:

然后是日志配置:

然后是swagger以及OpenFeign的配置:

我们在nacos控制台分别添加这些配置。

首先是jdbc相关配置,在配置管理->配置列表中点击+新建一个配置:

在弹出的表单中填写信息:

其中详细的配置如下:

spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: ${hm.db.un:root}
    password: ${hm.db.pw:123}
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  global-config:
    db-config:
      update-strategy: not_null
      id-type: auto

注意这里的jdbc的相关参数并没有写死,例如:

  • 🔹 数据库ip:通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值

  • 🔹 数据库端口:通过${hm.db.port:3306}配置了默认值为3306,同时允许通过${hm.db.port}来覆盖默认值

  • 🔹 数据库database:可以通过${hm.db.database}来设定,无默认值

然后是统一的日志配置,命名为shared-log.yaml,配置内容如下:

logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
  file:
    path: "logs/${spring.application.name}"

然后是统一的swagger配置,命名为shared-swagger.yaml,配置内容如下:

knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:商城接口文档}
    description: ${hm.swagger.description:商城接口文档}
    email: ${hm.swagger.email:zhanghuyi@itcast.cn}
    concat: ${hm.swagger.concat:idler}
    url: https://www.itcast.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${hm.swagger.package}

注意,这里的swagger相关配置我们没有写死,例如:

  • 📖title:接口文档标题,我们用了${hm.swagger.title}来代替,将来可以有用户手动指定

  • 📪email:联系人邮箱,我们用了${hm.swagger.email:zhanghuyi@itcast.cn},默认值是zhanghuyi@itcast.cn,同时允许用户利用${hm.swagger.email}来覆盖。

1.1.2.拉取共享配置

接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。

不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml

也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?

SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

因此,微服务整合Nacos配置管理的步骤如下:

1)引入依赖:

在cart-service模块引入依赖:

  <!--nacos配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>

2)新建bootstrap.yaml

在cart-service中的resources目录新建一个bootstrap.yaml文件:

内容如下:

spring:
  application:
    name: cart-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.150.101 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置

3)修改application.yaml

由于一些配置挪到了bootstrap.yaml,因此application.yaml需要修改为:

server:
  port: 8082
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
hm:
  swagger:
    title: 购物车服务接口文档
    package: com.hmall.cart.controller
  db:
    database: hm-cart

重启服务,发现所有配置都生效了。

1.2.配置热更新

有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10,对应代码如下:

现在这里购物车是写死的固定值,我们应该将其配置在配置文件中,方便后期修改。

但现在的问题是,即便写在配置文件中,修改了配置还是需要重新打包、重启服务才能生效。能不能不用重启,直接生效呢?

这就要用到Nacos的配置热更新能力了,分为两步:

  • 在Nacos中添加配置

  • 在微服务读取配置

1.2.1.添加配置到Nacos

首先,我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中:

注意文件的dataId格式:

[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 🌏 服务名:我们是购物车服务,所以是cart-service

  • 📂 spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置

  • 🔍后缀名:例如yaml

这里我们直接使用cart-service.yaml这个名称,则不管是dev还是local环境都可以共享该配置。

配置内容如下:

hm:
  cart:
    maxAmount: 1 # 购物车商品数量上限

提交配置,在控制台能看到新添加的配置:

1.2.2.配置热更新

接着,我们在微服务中读取配置,实现配置热更新。

cart-service中新建一个属性读取类:

代码如下:

package com.hmall.cart.config;

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

@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
    private Integer maxAmount;
}

接着,在业务中使用该属性加载类:

测试,向购物车中添加多个商品:

我们在nacos控制台,将购物车上限配置为5:

无需重启,再次测试购物车功能:

加入成功!

无需重启服务,配置热更新就生效了!

1.3.动态路由

网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上节课学习的配置热更新来实现路由更新。

因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:

  • 如何监听Nacos配置变更?

  • 如何把路由信息更新到路由表?

1.3.1.监听Nacos配置变更

在Nacos官网中给出了手动监听Nacos配置变更的SDK:

https://nacos.io/zh-cn/docs/sdk.html

如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。

public void addListener(String dataId, String group, Listener listener)

请求参数说明:

参数名

参数类型

描述

dataId

string

配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符("."、":"、"-"、"_")。不超过 256 字节。

group

string

配置分组,一般是默认的DEFAULT_GROUP。

listener

Listener

监听器,配置变更进入监听器的回调函数。

示例代码:

String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {
        @Override
        public void receiveConfigInfo(String configInfo) {
        // 配置变更的通知处理
                System.out.println("recieve1:" + configInfo);
        }
        @Override
        public Executor getExecutor() {
                return null;
        }
});

这里核心的步骤有2步:

  • 创建ConfigService,目的是连接到Nacos

  • 添加配置监听器,编写配置变更的通知处理逻辑

由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配,因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了:

NacosConfigManager中是负责管理Nacos的ConfigService的,具体代码如下:

因此,只要我们拿到NacosConfigManager就等于拿到了ConfigService,第一步就实现了。

第二步,编写监听器。虽然官方提供的SDK是ConfigService中的addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的API是这个:

String getConfigAndSignListener(
    String dataId, // 配置文件id
    String group, // 配置组,走默认
    long timeoutMs, // 读取配置的超时时间
    Listener listener // 监听器
) throws NacosException;

既可以配置监听器,并且会根据dataId和group读取配置并返回。我们就可以在项目启动时先更新一次路由,后续随着配置变更通知到监听器,完成路由更新。

1.3.2.更新路由

更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口:

package org.springframework.cloud.gateway.route;

import reactor.core.publisher.Mono;

/**
 * @author Spencer Gibb
 */
public interface RouteDefinitionWriter {
        /**
     * 更新路由到路由表,如果路由id重复,则会覆盖旧的路由
     */
        Mono<Void> save(Mono<RouteDefinition> route);
        /**
     * 根据路由id删除某个路由
     */
        Mono<Void> delete(Mono<String> routeId);

}

这里更新的路由,也就是RouteDefinition,之前我们见过,包含下列常见字段:

  • id:路由id

  • predicates:路由匹配规则

  • filters:路由过滤器

  • uri:路由目的地

将来我们保存到Nacos的配置也要符合这个对象结构,将来我们以JSON来保存,格式如下:

{
  "id": "item",
  "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
  }],
  "filters": [],
  "uri": "lb://item-service"
}

以上JSON配置就等同于:

spring:
  cloud:
    gateway:
      routes:
        - id: item
          uri: lb://item-service
          predicates:
            - Path=/items/**,/search/**

OK,我们所需要用到的SDK已经齐全了。

1.3.3.实现动态路由

首先, 我们在网关gateway引入依赖:

<!--统一配置管理-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

然后在网关gatewayresources目录创建bootstrap.yaml文件,内容如下:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.150.101
      config:
        file-extension: yaml
        shared-configs:
          - dataId: shared-log.yaml # 共享日志配置

接着,修改gatewayresources目录下的application.yml,把之前的路由移除,最终内容如下:

server:
  port: 8080 # 端口
hm:
  jwt:
    location: classpath:hmall.jks # 秘钥地址
    alias: hmall # 秘钥别名
    password: hmall123 # 秘钥文件密码
    tokenTTL: 30m # 登录有效期
  auth:
    excludePaths: # 无需登录校验的路径
      - /search/**
      - /users/login
      - /items/**

然后,在gateway中定义配置监听器:

其代码如下:

package com.hmall.gateway.route;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

    private final RouteDefinitionWriter writer;
    private final NacosConfigManager nacosConfigManager;

    // 路由配置文件的id和分组
    private final String dataId = "gateway-routes.json";
    private final String group = "DEFAULT_GROUP";
    // 保存更新过的路由id
    private final Set<String> routeIds = new HashSet<>();

    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
        // 1.注册监听器并首次拉取配置
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 5000, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        updateConfigInfo(configInfo);
                    }
                });
        // 2.首次启动时,更新一次配置
        updateConfigInfo(configInfo);
    }

    private void updateConfigInfo(String configInfo) {
        log.debug("监听到路由配置变更,{}", configInfo);
        // 1.反序列化
        List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
        // 2.更新前先清空旧路由
        // 2.1.清除旧路由
        for (String routeId : routeIds) {
            writer.delete(Mono.just(routeId)).subscribe();
        }
        routeIds.clear();
        // 2.2.判断是否有新的路由要更新
        if (CollUtils.isEmpty(routeDefinitions)) {
            // 无新路由配置,直接结束
            return;
        }
        // 3.更新路由
        routeDefinitions.forEach(routeDefinition -> {
            // 3.1.更新路由
            writer.save(Mono.just(routeDefinition)).subscribe();
            // 3.2.记录路由id,方便将来删除
            routeIds.add(routeDefinition.getId());
        });
    }
}

重启网关,任意访问一个接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1

发现是404,无法访问。

接下来,我们直接在Nacos控制台添加路由,路由文件名为gateway-routes.json,类型为json

配置内容如下:

[
    {
        "id": "item",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
        }],
        "filters": [],
        "uri": "lb://item-service"
    },
    {
        "id": "cart",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/carts/**"}
        }],
        "filters": [],
        "uri": "lb://cart-service"
    },
    {
        "id": "user",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
        }],
        "filters": [],
        "uri": "lb://user-service"
    },
    {
        "id": "trade",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/orders/**"}
        }],
        "filters": [],
        "uri": "lb://trade-service"
    },
    {
        "id": "pay",
        "predicates": [{
            "name": "Path",
            "args": {"_genkey_0":"/pay-orders/**"}
        }],
        "filters": [],
        "uri": "lb://pay-service"
    }
]

无需重启网关,稍等几秒钟后,再次访问刚才的地址:

网关路由成功了!

  • 11
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

枫蜜柚子茶

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值