微服务系列四:热更新措施与配置共享

目录

前言

一、基于Nacos的管理中心整体方案

二、配置共享动态维护

[2.1 分析哪些配置可拆,需要动态提供哪些参数](#2.1 分析哪些配置可拆,需要动态提供哪些参数)

[2.2 在nacos 分别创建共享配置](#2.2 在nacos 分别创建共享配置)

创建jdbc相关配置文件

创建日志相关配置文件

创建接口文档配置文件

[2.3 拉取本地合并配置文件](#2.3 拉取本地合并配置文件)

[2.3.1 拉取出现的先后顺序问题------SpringCloud 和 SpringBoot 读取顺序问题](#2.3.1 拉取出现的先后顺序问题——SpringCloud 和 SpringBoot 读取顺序问题)

[2.3.2 解决措施------添加引导配置文件,放在Springcloud初始化时读取](#2.3.2 解决措施——添加引导配置文件,放在Springcloud初始化时读取)

[.2.3.3 引入依赖](#.2.3.3 引入依赖)

[2.3.4 新增bootstrap.yaml文件](#2.3.4 新增bootstrap.yaml文件)

[2.3.5 重启测试购物车功能](#2.3.5 重启测试购物车功能)

[2.3.6 同理修改其他微服务的配置文件](#2.3.6 同理修改其他微服务的配置文件)

三、配置热更新(不停机更新)

[3.1 前提条件](#3.1 前提条件)

[3.2 业务实践------热更新购物车容量](#3.2 业务实践——热更新购物车容量)

[3.3 测试热更新效果](#3.3 测试热更新效果)

四、网关动态路由监听

监听Nacos配置变更

第一步:注入Nacos依赖

第二步:配置bootstrap.yaml文件

第三步:修改application.yaml文件,删除路由

第四步:编写动态路由加载器框架

第五步:更新路由方法拆解

【问题】如何获取RouteDefinition对象?如何更好的转换RouteDefinition对象?

第六步:动态路由加载器完整实现

第七步:在Nacos配置初始化的JSON格式路由

第八步:无需重启,测试网关生效

五、Nacos配置相关知识追问巩固


前言

这是微服务相关技术学习记录的第四篇文章啦!到此为止,微服务开发相关的技术其实已经分享了七七八八了。本篇则是着重介绍微服务配置文件管理相关、配置更新相关的内容。

回顾先前我们学习的微服务开发技术,我们已经解决了以下几个问题:

  • 实现微服务远程调用 (OpenFeign)

  • 实现微服务注册、发现 (nacos)

  • 微服务请求路由、负载均衡 (nacos)

  • 微服务登录用户信息传递 (网关 + 拦截器)

还有哪些问题需要解决呢?

试想一下,假如网关路由发生了更新,你是打算重新启动网关模块更新配置么?那么停机更新会导致服务不可用怎么办?

再来,目前每个微服务都需要编写配置文件,但是实际上里面很多内容是一样的。如果对这一部分公共内容进行修改,是不是又得大规模停机更新微服务呢?

这就概括出了目前微服务配置方面存在的弊端和我们本篇改进的方向:

  • 如何简化重复配置,降低维护成本?
  • 如何实现项目配置的不停机维护?
  • 如何实现网关路由的动态维护?

一、基于Nacos的管理中心整体方案

上述问题我们不难想到,那就将所有配置文件统一管理起来,成立一个管理中心即可。确实,这个管理中心还可以由我们的Nacos一并担任!
整体方案图示

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

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

二、配置共享动态维护

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

主要步骤如下:

  • 在Nacos中添加共享配置

  • 微服务拉取配置

配置共享最大的好处就是将重复配置封装成一份交Nacos统一管理。其他微服务只需要提供一些配置参数就行了,极大的简化了开发。接下来我们以cart-service为例,实现配置文件拆分。

2.1 分析哪些配置可拆,需要动态提供哪些参数

2.2 在nacos 分别创建共享配置

配置管理->配置列表 中点击 + 新建一个配置:


创建jdbc相关配置文件

bash 复制代码
spring:
  datasource:
    url: jdbc:mysql://${hm.db.host:192.168.186.140}:${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
  • 数据库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}来设定,无默认值


创建日志相关配置文件

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

创建接口文档配置文件

bash 复制代码
knife4j:
  enable: true
  openapi:
    title: ${hm.swagger.title:黑马商城接口文档}
    description: ${hm.swagger.description:黑马商城接口文档}
    email: ${hm.swagger.email:weizhicong@stu.gpnu.cn}
    concat: ${hm.swagger.concat:weizhicong}
    url: https://www.weizhicong.cn
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - ${hm.swagger.package}
  • title:接口文档标题,我们用了${hm.swagger.title}来代替,将来可以有用户手动指定

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

2.3 拉取本地合并配置文件

现在线上的配置文件创建好了,我们需要想办法让微服务项目接下来拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。但是这其中存在一些拉取的问题,让我们逐步分析。

2.3.1 拉取出现的先后顺序问题------SpringCloud 和 SpringBoot 读取顺序问题

读取Nacos配置是发生在SpringCloud上下文初始对象(ApplicationContext)时发生的,而Nacos地址配置在**application.yaml** 却是SpringBoot启动时才发生的。也就是说在读取Nacos配置时,根本不知道去哪里读取Nacos地址信息。从而造成无法加载Nacos配置文件的问题。

2.3.2 解决措施------添加引导配置文件,放在Springcloud初始化时读取

.2.3.3 引入依赖

在cart-service模块引入依赖:

XML 复制代码
  <!--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.3.4 新增bootstrap.yaml文件

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

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

2.3.5 重启测试购物车功能

2.3.6 同理修改其他微服务的配置文件

user-service成功!

trade-service成功!

item-service成功!

pay-service成功!

联调测试成功!


三、配置热更新(不停机更新)

配置热更新 :当我们修改配置文件中的参数属性时,无需重启服务即可生效。

3.1 前提条件

3.2 业务实践------热更新购物车容量

购物车业务,购物车数量有一个上限,默认是10,对应代码如下:

第一步:创建容器配置类,并添加@ConfigurationProperties注解指定服务配置

第二步:注入使用修改业务代码

第三步:增加Nacos配置

文件名称由三部分组成:[服务名]-[spring.active.profile].[后缀名]

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

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

  • 后缀名:例如yaml

3.3 测试热更新效果

四、网关动态路由监听

目前,我们的网关路由是写死在配置文件的。一旦路由有所变动,必须重启网关才能生效。对此我们希望网关路由也能和上一小节的配置热更新一样,动态维护,无需停机即可更新。

但是,网关的路由配置全部是在项目启动时由++org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator++ 在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,

所以,我们无法利用上节课学习的配置热更新来实现路由更新。
因此,我们必须监听Nacos的配置变更,然后手动把最新的路由更新到路由表中。这里有两个难点:

  • 如何监听Nacos配置变更?

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

监听Nacos配置变更

官方文档:Nacos 监听配置 Java SDKhttps://nacos.io/zh-cn/docs/sdk.html

第一步:注入Nacos依赖

第二步:配置bootstrap.yaml文件

bash 复制代码
spring:
  application:
    name: hm-gateway # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.186.140:8848 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-log.yaml # 共享日志配置

第三步:修改application.yaml文件,删除路由

bash 复制代码
server:
  port: 8080 # 网关端口 前端请求统一处理

hm:
  jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
  auth:
    excludePaths:
      - /search/**
      - /users/login
      - /items/**
      - /hi

第四步:编写动态路由加载器框架

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

    // 用来获取ConfigServer中的配置信息,与nacos建立连接
    private final NacosConfigManager nacosConfigManager;

    private final String dataId = "gateway-routers.json";

    private final String group = "DEFAULT_GROUP";

    @PostConstruct // 初始化方法,这个Bean一初始化就会执行
    public void initRouteConfigListener() throws NacosException {

        //1. 项目启动,先拉取一次配置,并且添加监听器
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 3000, new Listener() {
                    @Override
                    // 线程池
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override

                    public void receiveConfigInfo(String s) {
                        //TODO 2. 监听器,当配置发生变化时,需要更新路由表

                    }
                });

        // 3.第一次读取到配置,也需要更新到路由表
        updateConfigInfo(configInfo);
    }

    public void updateConfigInfo(String configInfo) {
        // TODO 更新路由表
    }
}

第五步:更新路由方法拆解

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

【问题】如何获取RouteDefinition对象?如何更好的转换RouteDefinition对象?

我们知道通过前面编写的这个方法可以获取到字符串格式的配置文件,保存在configInfo。它就和我们的配置文件一模一样,只不过是字符串表示。

所以第一个问题:如何获取RouteDefinition对象?我们需要解析字符串形式的yaml文件,将它转成java对象。

但是啊,将yaml文件转成Java的对象比较困难,那么什么格式转java对象比较方便呢?------JSON格式。

所以第二个问题:如何更好的转换RouteDefinition对象?我们将路由等信息存储成JSON格式而不是yaml格式,这样可以更好地转换成Java对象

第六步:动态路由加载器完整实现

java 复制代码
package com.hmall.gateway.routers;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosServiceManager;
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;
    // 用来获取ConfigServer中的配置信息,与nacos建立连接
    private final NacosConfigManager nacosConfigManager;

    // 配置文件id 和 分组
    private final String dataId = "gateway-routers.json";
    private final String group = "DEFAULT_GROUP";

    // 保存更新过的路由id
    private final Set<String> routeIds = new HashSet<>();

    
    @PostConstruct // 初始化方法,这个Bean一初始化就会执行
    public void initRouteConfigListener() throws NacosException {

        //1. 项目启动,先拉取一次配置,并且添加监听器
        String configInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, 3000, new Listener() {
                    @Override
                    // 线程池
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override

                    public void receiveConfigInfo(String configInfo) {
                        //2. 监听器,当配置发生变化时,需要更新路由表
                        updateConfigInfo(configInfo);
                    }
                });

        // 3.第一次读取到配置,也需要更新到路由表
        updateConfigInfo(configInfo);
    }

    /**
     * configInfo 是nacos中配置的json字符串
     * @param configInfo
     */
    public 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());
        });
    }
}

第七步:在Nacos配置初始化的JSON格式路由

当前还没配置Nacos时,此时的网关没有任何路由,可以测试一下:

第八步:无需重启,测试网关生效

五、Nacos配置相关知识追问巩固

1.如何简化重复配置,降低维护成本?谈谈具体实施步骤

  1. 如何解决读取远程Nacos配置时无法从Springboot中获取到Nacos地址的配置信息?

  2. 如何实现配置热更新?谈谈具体的实施步骤

  3. 如何实现动态路由热更新?谈谈具体的实施步骤

  4. 请你谈谈 @PostConstruct 注解的作用?

  5. 请你谈谈监听Nacos配置中如何获取Nacos的ConfigServer中的配置信息?

  6. 谈谈更新路由监听器方法是怎么实现的?你是否赞同采用先删除后更新的策略,为什么?

  7. Nacos提供了RouteDefinitionWriter接口用于实现添加路由,路由对象是RouteDefinition类型的。请问你在路由监听中如何获取该对象?如何更好、更快的获取该对象?

相关推荐
sevevty-seven几秒前
java重要知识点 JVM基本结构
java·开发语言·jvm
张飞的猪1 分钟前
什么是AOP面向切面编程?怎么简单理解?
java·spring·aop·面向切面编程
TANGLONG2222 分钟前
【初阶数据结构与算法】复杂度分析练习之轮转数组(多种方法)
java·c语言·数据结构·c++·python·算法·面试
墨如初见3 分钟前
vue前端进行AES加密,JAVA对其进行AES解密
java·前端·vue.js
jonyleek4 分钟前
JVS开源框架:工作流引擎代理中心的设计挑战与实现方案
java·gitee·开源·github·软件需求
hi_zf4 分钟前
面试知识目录
java
码蜂窝编程官方7 分钟前
【含开题报告+文档+源码】基于Web的房地产销售网站的设计与实现
java·前端·vue.js·spring boot·spring
提笔惊蚂蚁8 分钟前
java-web-day7-会话跟踪技术
java·开发语言·前端·程序人生
Alina_shu14 分钟前
springboot使用kafka推送数据到服务端,带认证
java·spring boot·kafka
Kalika0-017 分钟前
Linux 系统启动
linux·运维·服务器·学习