Spring Cloud Nacos源码讲解(九)- Nacos客户端本地缓存及故障转移

news2024/12/19 12:17:22

Nacos客户端本地缓存及故障转移

​ 在Nacos本地缓存的时候有的时候必然会出现一些故障,这些故障就需要进行处理,涉及到的核心类为ServiceInfoHolder和FailoverReactor。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tebMgFe-1677029918442)(image-20211027191504884.png)]

​ 本地缓存有两方面,第一方面是从注册中心获得实例信息会缓存在内存当中,也就是通过Map的形式承载,这样查询操作都方便。第二方面便是通过磁盘文件的形式定时缓存起来,以备不时之需。

​ 故障转移也分两方面,第一方面是故障转移的开关是通过文件来标记的;第二方面是当开启故障转移之后,当发生故障时,可以从故障转移备份的文件中来获得服务实例信息。

ServiceInfoHolder功能概述

​ ServiceInfoHolder类,顾名思义,服务信息的持有者。每次客户端从注册中心获取新的服务信息时都会调用该类,其中processServiceInfo方法来进行本地化处理,包括更新缓存服务、发布事件、更新本地文件等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cuBsv384-1677029918446)(image-20211027152442627.png)]

​ 除了这些核心功能以外,该类在实例化的时候,还做了本地缓存目录初始化、故障转移初始化等操作,下面我们来分析。

ServiceInfo的本地内存缓存

​ ServiceInfo,注册服务的信息,其中包含了服务名称、分组名称、集群信息、实例列表信息,上次更新时间等,所以我们由此得出客户端从服务端注册中心获得到的信息在本地都以ServiceInfo作为承载者。

​ 而ServiceInfoHolder类又持有了ServiceInfo,通过一个ConcurrentMap来储存

// ServiceInfoHolder
private final ConcurrentMap<String, ServiceInfo> serviceInfoMap;

​ 这就是Nacos客户端对服务端获取到的注册信息的第一层缓存,并且之前的课程中我们分析processServiceInfo方法时,我们已经看到,当服务信息变更时会第一时间更新ServiceInfoMap中的信息

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
 	....
    //缓存服务信息
    serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
    // 判断注册的实例信息是否更改
    boolean changed = isChangedServiceInfo(oldService, serviceInfo);
    if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {
        serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));
    }
    ....
    return serviceInfo;
}

​ serviceInfoMap的使用就是这样,当变动实例向其中put最新数据即可。当使用实例时,根据key进行get操作即可。

​ serviceInfoMap在ServiceInfoHolder的构造方法中进行初始化,默认创建一个空的ConcurrentMap。但当配置了启动时从缓存文件读取信息时,则会从本地缓存进行加载。

public ServiceInfoHolder(String namespace, Properties properties) {
    initCacheDir(namespace, properties);
    // 启动时是否从缓存目录读取信息,默认false。
    if (isLoadCacheAtStart(properties)) {
        this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(DiskCache.read(this.cacheDir));
    } else {
        this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
    }
    this.failoverReactor = new FailoverReactor(this, cacheDir);
    this.pushEmptyProtection = isPushEmptyProtect(properties);
}

​ 这里我们要注意一下,涉及到了本地缓存目录,在我们上节课的学习中我们知道,processServiceInfo方法中,当服务实例变更时,会看到通过DiskCache#write方法向该目录写入ServiceInfo信息。

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
   	.....
    // 服务实例已变更
    if (changed) {
        NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
                           JacksonUtils.toJson(serviceInfo.getHosts()));
        // 添加实例变更事件InstancesChangeEvent,订阅者
        NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                                                           serviceInfo.getClusters(), serviceInfo.getHosts()));
        // 记录Service本地文件
        DiskCache.write(serviceInfo, cacheDir);
    }
    return serviceInfo;
}

本地缓存目录

​ 本地缓存目录cacheDir是ServiceInfoHolder的一个属性,用于指定本地缓存的根目录和故障转移的根目录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iNCn2sip-1677029918448)(image-20211027163722859.png)]

​ 在ServiceInfoHolder的构造方法中,初始化并且生成缓存目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RJQq3COb-1677029918449)(image-20211027163940247.png)]

​ 这个initCacheDir就不用了细看了,就是生成缓存目录的操作,默认路径:${user.home}/nacos/naming/public,也可以自定义,通过System.setProperty(“JM.SNAPSHOT.PATH”)自定义

​ 这里初始化完目录之后,故障转移信息也存储在该目录下。

private void initCacheDir(String namespace, Properties properties) {
    String jmSnapshotPath = System.getProperty(JM_SNAPSHOT_PATH_PROPERTY);

    String namingCacheRegistryDir = "";
    if (properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR) != null) {
        namingCacheRegistryDir = File.separator + properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR);
    }

    if (!StringUtils.isBlank(jmSnapshotPath)) {
        cacheDir = jmSnapshotPath + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
            + File.separator + FILE_PATH_NAMING + File.separator + namespace;
    } else {
        cacheDir = System.getProperty(USER_HOME_PROPERTY) + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
            + File.separator + FILE_PATH_NAMING + File.separator + namespace;
    }
}

故障转移

​ 在ServiceInfoHolder的构造方法中,还会初始化一个FailoverReactor类,同样是ServiceInfoHolder的成员变量。FailoverReactor的作用便是用来处理故障转移的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmQqhDeE-1677029918451)(image-20211027170511242.png)]

public ServiceInfoHolder(String namespace, Properties properties) {
    ....
    // this为ServiceHolder当前对象,这里可以立即为两者相互持有对方的引用
    this.failoverReactor = new FailoverReactor(this, cacheDir);
    .....
}

​ 我们来看一下FailoverReactor的构造方法,FailoverReactor的构造方法基本上把它的功能都展示出来了:

1. 持有ServiceInfoHolder的引用
2. 拼接故障目录:${user.home}/nacos/naming/public/failover,其中public也有可能是其他的自定义命名空间
3. 初始化executorService(执行者服务)
4. init方法:通过executorService开启多个定时任务执行
public FailoverReactor(ServiceInfoHolder serviceInfoHolder, String cacheDir) {
    // 持有ServiceInfoHolder的引用
    this.serviceInfoHolder = serviceInfoHolder;
    // 拼接故障目录:${user.home}/nacos/naming/public/failover
    this.failoverDir = cacheDir + FAILOVER_DIR;
    // 初始化executorService
    this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            // 守护线程模式运行
            thread.setDaemon(true);
            thread.setName("com.alibaba.nacos.naming.failover");
            return thread;
        }
    });
    // 其他初始化操作,通过executorService开启多个定时任务执行
    this.init();
}

init方法执行

​ 在这个方法中开启了三个定时任务,这三个任务其实都是FailoverReactor的内部类:

1. 初始化立即执行,执行间隔5秒,执行任务SwitchRefresher
2. 初始化延迟30分钟执行,执行间隔24小时,执行任务DiskFileWriter
3. 初始化立即执行,执行间隔10秒,执行核心操作为DiskFileWriter
public void init() {
	// 初始化立即执行,执行间隔5秒,执行任务SwitchRefresher
    executorService.scheduleWithFixedDelay(new SwitchRefresher(), 0L, 5000L, TimeUnit.MILLISECONDS);
	// 初始化延迟30分钟执行,执行间隔24小时,执行任务DiskFileWriter
    executorService.scheduleWithFixedDelay(new DiskFileWriter(), 30, DAY_PERIOD_MINUTES, TimeUnit.MINUTES);

    // backup file on startup if failover directory is empty.
    // 如果故障目录为空,启动时立即执行,立即备份文件
    // 初始化立即执行,执行间隔10秒,执行核心操作为DiskFileWriter
    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            try {
                File cacheDir = new File(failoverDir);

                if (!cacheDir.exists() && !cacheDir.mkdirs()) {
                    throw new IllegalStateException("failed to create cache dir: " + failoverDir);
                }

                File[] files = cacheDir.listFiles();
                if (files == null || files.length <= 0) {
                    new DiskFileWriter().run();
                }
            } catch (Throwable e) {
                NAMING_LOGGER.error("[NA] failed to backup file on startup.", e);
            }

        }
    }, 10000L, TimeUnit.MILLISECONDS);
}

​ 这里我们先看DiskFileWriter,这里的逻辑不难,就是获取ServiceInfo中缓存的ServiceInfo,判断是否满足写入磁盘,如果条件满足,就将其写入拼接的故障目录,因为后两个定时任务执行的都是DiskFileWriter,但是第三个定时任务是有前置判断的,只要文件不存在就会立即执行把文件写入到本地磁盘中。

class DiskFileWriter extends TimerTask {

    @Override
    public void run() {
        Map<String, ServiceInfo> map = serviceInfoHolder.getServiceInfoMap();
        for (Map.Entry<String, ServiceInfo> entry : map.entrySet()) {
            ServiceInfo serviceInfo = entry.getValue();
            if (StringUtils.equals(serviceInfo.getKey(), UtilAndComs.ALL_IPS) || StringUtils
                .equals(serviceInfo.getName(), UtilAndComs.ENV_LIST_KEY) || StringUtils
                .equals(serviceInfo.getName(), UtilAndComs.ENV_CONFIGS) || StringUtils
                .equals(serviceInfo.getName(), UtilAndComs.VIP_CLIENT_FILE) || StringUtils
                .equals(serviceInfo.getName(), UtilAndComs.ALL_HOSTS)) {
                continue;
            }
			// 将缓存写入磁盘
            DiskCache.write(serviceInfo, failoverDir);
        }
    }
}

​ 接下来,我们再来看第一个定时任务SwitchRefresher的核心实现,具体逻辑如下:

1. 如果故障转移文件不存在,则直接返回(文件开关)
2. 比较文件修改时间,如果已经修改,则获取故障转移文件中的内容。
3. 故障转移文件中存储了0和1标识。0表示关闭,1表示开启。
4. 当为开启状态时,执行线程FailoverFileReader。
class SwitchRefresher implements Runnable {

    long lastModifiedMillis = 0L;

    @Override
    public void run() {
        try {
            File switchFile = new File(failoverDir + UtilAndComs.FAILOVER_SWITCH);
            // 文件不存在则退出
            if (!switchFile.exists()) {
                switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());
                NAMING_LOGGER.debug("failover switch is not found, {}", switchFile.getName());
                return;
            }

            long modified = switchFile.lastModified();
			
            if (lastModifiedMillis < modified) {
                lastModifiedMillis = modified;
                // 获取故障转移文件内容
                String failover = ConcurrentDiskUtil.getFileContent(failoverDir + UtilAndComs.FAILOVER_SWITCH,
                                                                    Charset.defaultCharset().toString());
                if (!StringUtils.isEmpty(failover)) {
                    String[] lines = failover.split(DiskCache.getLineSeparator());

                    for (String line : lines) {
                        String line1 = line.trim();
                        // 1 表示开启故障转移模式
                        if (IS_FAILOVER_MODE.equals(line1)) {
                            switchParams.put(FAILOVER_MODE_PARAM, Boolean.TRUE.toString());
                            NAMING_LOGGER.info("failover-mode is on");
                            new FailoverFileReader().run();
                        // 0 表示关闭故障转移模式
                        } else if (NO_FAILOVER_MODE.equals(line1)) {
                            switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());
                            NAMING_LOGGER.info("failover-mode is off");
                        }
                    }
                } else {
                    switchParams.put(FAILOVER_MODE_PARAM, Boolean.FALSE.toString());
                }
            }

        } catch (Throwable e) {
            NAMING_LOGGER.error("[NA] failed to read failover switch.", e);
        }
    }
}

FailoverFileReader

​ 顾名思义,故障转移文件读取,基本操作就是读取failover目录存储的备份服务信息文件内容,然后转换成ServiceInfo,并且将所有的ServiceInfo储存在FailoverReactor的ServiceMap属性中。

​ 流程如下:

1. 读取failover目录下的所有文件,进行遍历处理
2. 如果文件不存在跳过
3. 如果文件是故障转移开关标志文件跳过
4. 读取文件中的备份内容,转换为ServiceInfo对象
5. 将ServiceInfo对象放入到domMap中
6. 最后判断domMap不为空,赋值给serviceMap
class FailoverFileReader implements Runnable {

    @Override
    public void run() {
        Map<String, ServiceInfo> domMap = new HashMap<String, ServiceInfo>(16);

        BufferedReader reader = null;
        try {

            File cacheDir = new File(failoverDir);
            if (!cacheDir.exists() && !cacheDir.mkdirs()) {
                throw new IllegalStateException("failed to create cache dir: " + failoverDir);
            }

            File[] files = cacheDir.listFiles();
            if (files == null) {
                return;
            }

            for (File file : files) {
                if (!file.isFile()) {
                    continue;
                }
				// 如果是故障转移标志文件,则跳过
                if (file.getName().equals(UtilAndComs.FAILOVER_SWITCH)) {
                    continue;
                }

                ServiceInfo dom = new ServiceInfo(file.getName());

                try {
                    String dataString = ConcurrentDiskUtil
                        .getFileContent(file, Charset.defaultCharset().toString());
                    reader = new BufferedReader(new StringReader(dataString));

                    String json;
                    if ((json = reader.readLine()) != null) {
                        try {
                            dom = JacksonUtils.toObj(json, ServiceInfo.class);
                        } catch (Exception e) {
                            NAMING_LOGGER.error("[NA] error while parsing cached dom : {}", json, e);
                        }
                    }

                } catch (Exception e) {
                    NAMING_LOGGER.error("[NA] failed to read cache for dom: {}", file.getName(), e);
                } finally {
                    try {
                        if (reader != null) {
                            reader.close();
                        }
                    } catch (Exception e) {
                        //ignore
                    }
                }
                if (!CollectionUtils.isEmpty(dom.getHosts())) {
                    domMap.put(dom.getKey(), dom);
                }
            }
        } catch (Exception e) {
            NAMING_LOGGER.error("[NA] failed to read cache file", e);
        }
		
        // 读入缓存
        if (domMap.size() > 0) {
            serviceMap = domMap;
        }
    }
}

​ 但是这里还有一个问题就是serviceMap是哪里用到的,这个其实是我们之前读取实例时候用到的getServiceInfo方法

​ 其实这里就是一旦开启故障转移就会先调用failoverReactor.getService方法,此方法便是从serviceMap中获取ServiceInfo

public ServiceInfo getService(String key) {
    ServiceInfo serviceInfo = serviceMap.get(key);

    if (serviceInfo == null) {
        serviceInfo = new ServiceInfo();
        serviceInfo.setName(key);
    }

    return serviceInfo;
}

​ 调用serviceMap方法getServiceInfo方法就在ServiceInfoHolder中

// ServiceInfoHolder
public ServiceInfo getServiceInfo(final String serviceName, final String groupName, final String clusters) {
    NAMING_LOGGER.debug("failover-mode: {}", failoverReactor.isFailoverSwitch());
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    String key = ServiceInfo.getKey(groupedServiceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }
    return serviceInfoMap.get(key);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/371728.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CODESYS Install

CODESYS InstallCODESYS 由 Development System 和 Runtime System 两部分组成&#xff0c;Development System 就是一个 IDE&#xff0c;是免费使用的&#xff0c;可以在 CODESYS 官网下载并安装。程序写好以后&#xff0c;就要把它转移到硬件设备中运行。这时生成的程序自己是…

Python3-运算符

什么是运算符&#xff1f; 本章节主要说明 Python 的运算符。 举个简单的例子: 4 5 9 例子中&#xff0c;4 和 5 被称为操作数&#xff0c; 称为运算符。 Python 语言支持以下类型的运算符: 算术运算符 比较&#xff08;关系&#xff09;运算符 赋值运算符 逻辑运算符 位运…

nginx配置https域名

如果已安装nginx若没有配置ssl模块需要添加ssl模块。注意&#xff1a;默认是不会安装ssl模块的查看nginx是否有安装ssl模块./nginx -V若执行结果显示下图中的-with-http_ssl_module则说明nginx已经安装了ssl模块若没有安装则重新打包编译./configure --with-http_ssl_module编译…

主题模型实践

目录 一.TF-IDF 二.LSI 三.相似度 四.主题和主题分布 五. LDA计算的相似度 六.LDA过程 七.主题 八.主题和主题分布 九.数据处理流程 十.常用正则表达式 十一.代码 一.TF-IDF 二.LSI 三.相似度 四.主题和主题分布 五. LDA计算的相似度 六.LDA过程 七.主题 八.主题和主…

Kotlin 37. Android,Assets 以及 Res/raw 文件夹下存放调用文件介绍及对比

一起来学Kotlin&#xff1a;概念&#xff1a;24. Assets 以及 Res/raw 文件夹下存放调用文件介绍及对比 有些时候&#xff0c;我们需要将一些文件&#xff08;e.g. JSON, Text, mp3, pdf, txt, etc&#xff09;存放在某些特定文件夹下&#xff0c;方便调用。这里有两种可能性&…

emqx桥接配置+常见问题解决+jmeter压测emqx

一&#xff0c;桥接资源配置及规则配置 Emqx桥接配置流程 1&#xff0c;配置资源并测试连接通过 规则引擎——>资源——>新建——>选择MQTT Bridge——>填写参数测试连接 参数描述详见3.1资源配置 2&#xff0c;配置规则 2.1根据实际业务选择合适sql 规则引擎…

洛谷P5015 [NOIP2018 普及组] 标题统计 C语言/C++

[NOIP2018 普及组] 标题统计 题目描述 凯凯刚写了一篇美妙的作文&#xff0c;请问这篇作文的标题中有多少个字符&#xff1f; 注意&#xff1a;标题中可能包含大、小写英文字母、数字字符、空格和换行符。统计标题字 符数时&#xff0c;空格和换行符不计算在内。 输入格式 …

kubernates(k8s)全解

目录说明Kubernetes介绍应用部署方式演变kubernetes简介kubernetes组件kubernetes概念kubernetes集群环境搭建安装方式kubeadm二进制包集群类型安装要求最终目标准备环境环境初始化服务部署kubeadm中的命令(一般用不着)资源管理YAML语言介绍资源管理方式命令式对象管理kubectl命…

微服务架构设计介绍

软件架构是一个包含各种组织的系统组织&#xff0c;这些组件包括 Web服务器, 应用服务器, 数据库,存储, 通讯层), 它们彼此或和环境存在关系。系统架构的目标是解决利益相关者的关注点。 image Conway’s law: Organizations which design systems[...] are constrained to pro…

Ubuntu 20.04安装SQL Server

SQL Server 目前在 Red Hat Enterprise Server、SUSE Linux Enterprise Server 和 Ubuntu 上受支持。 还支持使用 Docker 在容器中运行。 Linux上安装SQL&#xff0c;不支持的功能或者服务 以前都是在Windows上安装SQL&#xff0c;其实SQL Server 的核心数据库引擎在 Linux 上…

【图像分类】卷积神经网络之AlexNet网络模型实现花卉图像识别(附代码和数据集,PyTorch框架)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手 共进,共创辉煌。 在上一篇博文中我们对AlexNet网络模型的结构进行了剖析,本篇博文,我们将使用PyTorch搭建AlexNet实现花卉识别,…

JavaScript 俄罗斯方块 - Canvas基础 画线

JavaScript 是前端核心, 掌握这门语言是步入前端高手行列必经之路, 现代前端当然别忘了还有TypeScript, 学习它需要面向对象(OOP)基础知识, 底层的浏览器原理、HTTP协议也必不可少, 本专栏从基础开始一步步带你实现俄罗斯方块小游戏, 让你从有趣的实战中学习JavaScript,事…

ubuntu安装gitlab

gitlab是什么?他有什么作用呢&#xff1f; gitlab是一个开源的git仓库管理软件&#xff0c;并提供web界面&#xff0c;方便管理git仓库。和github很相似&#xff0c;不过github暂时没有开源版本&#xff0c;项目必须托管到github官方网站&#xff0c;不能本地部署。很多公司考…

数据增强,扩充了数据集,增加了模型的泛化能力

数据增强&#xff08;Data Augmentation&#xff09;是在不实质性的增加数据的情况下&#xff0c;从原始数据加工出更多的表示&#xff0c;提高原数据的数量及质量&#xff0c;以接近于更多数据量产生的价值。 其原理是&#xff0c;通过对原始数据融入先验知识&#xff0c;加工…

【NFC音乐相册】简易制作

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; NFC音乐相册在前段时间火了一把&#xff0c;想必大家都听过了&#xff0c;最近我刷到了这个东西&#xff0c;闲来无事就弄了几个&#xff0c;这篇博客就记录下制作工序。 注&#xff1a;我所…

keepalive + nginx 来实现 对于nginx的高可用, 以及如何搭建主备模式

keepalive nginx 来实现 对于nginx的高可用, 以及如何搭建主备模式。 keeplived简介 Keepalived是用纯ANSI/ISO C编写的。该软件围绕一个中央I/O多路复用器进行连接&#xff0c;以提供实时网络设计。 1.1 Keepalived进程被分为3个不同进程 A.一个极简的父进程&#xff0c…

NLP大纲

第一章&#xff1a;概述 1. 什么是自然语言处理&#xff1f; 计算机具备人类的听、说、读、写、译、问、答、搜索、摘要、对话和聊天等能力 知识和常识进行推理和决策 支持客服、诊断、法律、教学等场景 2. 自然语言处理的主要任务有哪些&#xff1f; 分析、理解、转换、…

SpringBoot-运维实用篇

SpringBoot运维实用篇 1.SpringBoot程序的打包与运行 ​ 刚开始做开发学习的小伙伴可能在有一个知识上面有错误的认知&#xff0c;我们天天写程序是在Idea下写的&#xff0c;运行也是在Idea下运行的。 ​ 但是实际开发完成后&#xff0c;我们的项目是不可能运行在自己的电脑上…

图解LeetCode——剑指 Offer 46. 把数字翻译成字符串

一、题目 给定一个数字&#xff0c;我们按照如下规则把它翻译为字符串&#xff1a;0 翻译成 “a” &#xff0c;1 翻译成 “b”&#xff0c;……&#xff0c;11 翻译成 “l”&#xff0c;……&#xff0c;25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数&#x…

spring boot3 一、 spring alibaba cloud 整合 网关 nacos config

jm-apis服务jm-user-api用户服务jm-apis-common公共模块jm-apis-common-bean公共beanjm-apis-common-conf公共配置jm-apis-common-tool公共工具jm-dubbo-apisRPC服务jm-dubbo-user-api用户RPC服务 jianmu-springboot3-springalibabacloud pom.xml <?xml version"1.0&…