1.开门见山

nacos使用引用的相关的jar包

<dependency>
            <groupId>com.alibaba.boot</groupId>
            <artifactId>nacos-config-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
        </dependency>

nacos配置中心中有个主要的分布式配置service接口就是ConfigService,对应的实现就是NacosConfigService,在真实的应用中,初始化的时候就需要实例化这个对象,然后加载到容器中.

初始化的地方在nacos-config-spring-autoconfigure的jar包里面,对应的入口是NacosConfigApplicationContextInitializer,早些版本的时候这个类是放在spring.factories中,后来给注释掉了.

nacos热更新mysql密码 nacos更新配置_初始化


现在这个Initializer是放置在NacosConfigEnvironmentProcessor里面进行调用的.

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        application.addInitializers(new ApplicationContextInitializer[]{new NacosConfigApplicationContextInitializer(this)});
        if (this.enable(environment)) {
            System.out.println("[Nacos Config Boot] : The preload log configuration is enabled");
            this.initLogConfig(environment);
        }
    }

在调用initializer的时候,会进行NacosConfigService的初始化工作.

public ConfigService createConfigService(Properties properties) throws NacosException {
        Properties copy = new Properties();
        copy.putAll(properties);
        String cacheKey = NacosUtils.identify(copy);
        ConfigService configService = (ConfigService)this.configServicesCache.get(cacheKey);
        if (configService == null) {
            configService = this.doCreateConfigService(copy);
            this.configServicesCache.put(cacheKey, configService);
        }
        return configService;
    }

最终会调用NacosFactory.createConfigService(Properties properties),通过NacosFactory进行实话service对象.
下面这个就是具体的实例化的代码

public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable var4) {
            throw new NacosException(-400, var4);
        }
    }
NacosConfigService初始化

下面在来看看它的初始化的具体内容

public NacosConfigService(Properties properties) throws NacosException {
        String encodeTmp = properties.getProperty("encode");
        if (StringUtils.isBlank(encodeTmp)) {
            this.encode = "UTF-8";
        } else {
            this.encode = encodeTmp.trim();
        }

        this.initNamespace(properties);
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        this.agent.start();
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }

在初始化的过程中,会把nacos的配置数据赋值到ServerListManager这个类当中,下面的跟server的交互的url前缀什么的都是通过这个类来获取到的.

1.初始化命名空间
2.把ServerHttpAgent包装成MetricsHttpAgent,两者都实现了HttpAgent.MetricsHttpAgent中添加了一些http请求的耗时记录.
3.创建ClientWorker

HttpAgent的具体方法:

public interface HttpAgent {
    void start() throws NacosException;

    HttpResult httpGet(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;

    HttpResult httpPost(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;

    HttpResult httpDelete(String var1, List<String> var2, List<String> var3, String var4, long var5) throws IOException;

    String getName();

    String getNamespace();

    String getTenant();

    String getEncode();
}
ClientWorker客户端的实例化

下面我们再看下ClientWorker的实例化过程

nacos热更新mysql密码 nacos更新配置_java_02

public void checkConfigInfo() {
        int listenerSize = ((Map)this.cacheMap.get()).size();
        int longingTaskCount = (int)Math.ceil((double)listenerSize / ParamUtil.getPerTaskConfigSize());
        if ((double)longingTaskCount > this.currentLongingTaskCount) {
            for(int i = (int)this.currentLongingTaskCount; i < longingTaskCount; ++i) {
                this.executorService.execute(new ClientWorker.LongPollingRunnable(i));
            }
            this.currentLongingTaskCount = (double)longingTaskCount;
        }
    }

在轮询线程里面,会调用checkConfigInfo()方法,会拿取一部分任务执行ClientWorker.LongPollingRunnable(i).

接下来在看下ClientWorker中的内部类LongPollingRunnable,上面的任务执行
就会调用对应的run方法,

CacheData cacheData = (CacheData)var3.next();
                    if (cacheData.getTaskId() == this.taskId) {
                        cacheDatas.add(cacheData);

                        try {
                            ClientWorker.this.checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception var13) {
                            ClientWorker.LOGGER.error("get local config info error", var13);
                        }
                    }

根据入参taskId获取对应的CacheData,然后校验本地配置,从代码上看,是根据命名空间,dataId,group,tenant组成的路径下,查看是否有对应的失败文件信息.

如果使用本地配置的话,然后校验MD5.

接着调用checkUpdateDataIds()检查变化的dataId列表,聚合不使用本地配置信息的dataId,然后调用checkUpdateConfigStr方法

nacos热更新mysql密码 nacos更新配置_List_03

nacos服务端listener的api

从上面的代码我们可以看到,是通过调用/v1/cs/configs/listener接口,来获取到服务端变化的dataId列表.

我们从github上面down下来nacos的源码,跟踪一下这个接口的具体流程.

nacos热更新mysql密码 nacos更新配置_nacos热更新mysql密码_04


最终会调用ConfigServletInner.doPollingConfig()轮询方法,判断的条件就是http的header请求参数里面是否会带有Long-Pulling-Timeout的参数,如果有就进入长轮询的流程里面,否则就只进行md5的校验,然后立即返回给客户端.

接下来我们看下长轮询的方法内容,这个是在LongPollingService类中,调用addLongPollingClient方法,进行长轮询的操作流程.

nacos热更新mysql密码 nacos更新配置_java_05


有一个isFixedPolling()的判断,这个底层数据是属于nacos元数据配置项里面的,要不是从config_info库里面加载,要不是从磁盘文件进行加载.

如果是true的话,只是计算timeout时间,主要是为了解决轮询超时问题,默认是10s.

如果是false,只校验配置文件的md5,然后返回给客户端.

接下来就把new的ClientLongPolling对象扔给ScheduledExecutorService任务线程里面去执行,最终执行LongPollingService的内部类ClientLongPolling的run方法.

public void run() {
            asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
                @Override
                public void run() {
                    try {
                        getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                        
                        // Delete subsciber's relations.
                        allSubs.remove(ClientLongPolling.this);
                        
                        if (isFixedPolling()) {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            List<String> changedGroups = MD5Util
                                    .compareMd5((HttpServletRequest) asyncContext.getRequest(),
                                            (HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
                            if (changedGroups.size() > 0) {
                                sendResponse(changedGroups);
                            } else {
                                sendResponse(null);
                            }
                        } else {
                            LogUtil.CLIENT_LOG
                                    .info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
                                            RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
                                            "polling", clientMd5Map.size(), probeRequestSize);
                            sendResponse(null);
                        }
                    } catch (Throwable t) {
                        LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
                    }
                    
                }
                
            }, timeoutTime, TimeUnit.MILLISECONDS);
            
            allSubs.add(this);
        }

在开启线程前,会调用allSubs.add(this);把上面创建的实例放到队列里面Queue allSubs.
在线程里面会重新把队列里面的给移除掉

// Delete subsciber's relations.
allSubs.remove(ClientLongPolling.this);

在轮询的情况下,会获取已经变化的dataId组合,然后把变化列表响应给客户端.

public static List<String> compareMd5(HttpServletRequest request, HttpServletResponse response,
            Map<String, String> clientMd5Map) {
        List<String> changedGroupKeys = new ArrayList<String>();
        String tag = request.getHeader("Vipserver-Tag");
        for (Map.Entry<String, String> entry : clientMd5Map.entrySet()) {
            String groupKey = entry.getKey();
            String clientMd5 = entry.getValue();
            String ip = RequestUtil.getRemoteIp(request);
            boolean isUptodate = ConfigCacheService.isUptodate(groupKey, clientMd5, ip, tag);
            if (!isUptodate) {
                changedGroupKeys.add(groupKey);
            }
        }
        return changedGroupKeys;
    }
客户端调用/listener接口获取更新列表后的操作
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);

获取变化列表之后,循环调用server的具体配置

try {
                        String content = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = (CacheData)((Map)ClientWorker.this.cacheMap.get()).get(GroupKey.getKeyTenant(dataId, group, tenant));
                        cache.setContent(content);
                        ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(content)});
                    } catch (NacosException var12) {
                        String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
                        ClientWorker.LOGGER.error(message, var12);
                    }

接下来我们看下ClientWorker.this.getServerConfig()这个方法,在方法里面,又调用了server端的另一个api接口,就是/v1/cs/configs,根据dataId group tenant获取到对应的配置内容,然后放置到CacheData中.

nacos后台变更数据

nacos热更新mysql密码 nacos更新配置_List_06


nacos热更新mysql密码 nacos更新配置_初始化_07


这是nacos后台数据变更后的请求数据,调用的是/nacos/v1/cs/configs,对照ConfigController的方法其实是publishConfig方法.

nacos热更新mysql密码 nacos更新配置_客户端_08


首先把变更后的数据同步到nacos的库里面.

然后发布了一个ConfigDataChangeEvent事件.

ConfigChangePublisher
                        .notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

nacos服务端相关配置变更是通过发布订阅的方式来进行的.

event接口

nacos热更新mysql密码 nacos更新配置_初始化_09


所有发布的事件都要继承这个抽象类.Subscriber是一个订阅的抽象类.

nacos热更新mysql密码 nacos更新配置_java_10


下面的这些都是实现了Subscriber的对象

nacos热更新mysql密码 nacos更新配置_nacos热更新mysql密码_11


nacos热更新mysql密码 nacos更新配置_客户端_12


所有的事件发布都是通过NotifyCenter类来进行的,包含注册订阅者都是通过这个类进行.

public static boolean publishEvent(final Event event) {
        try {
            return publishEvent(event.getClass(), event);
        } catch (Throwable ex) {
            LOGGER.error("There was an exception to the message publishing : {}", ex);
            return false;
        }
    }

这是发布事件的地方.
nacos后台发布ConfigDataChangeEvent事件之后会跳转到AsyncNotifyService的onEvent方法

public void onEvent(Event event) {
                // Generate ConfigDataChangeEvent concurrently
                if (event instanceof ConfigDataChangeEvent) {
                    ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
                    long dumpTs = evt.lastModifiedTs;
                    String dataId = evt.dataId;
                    String group = evt.group;
                    String tenant = evt.tenant;
                    String tag = evt.tag;
                    Collection<Member> ipList = memberManager.allMembers();
                    
                    // In fact, any type of queue here can be
                    Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
                    for (Member member : ipList) {
                        queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),
                                evt.isBeta));
                    }
                    ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));
                }
            }

遍历所有member,获取到对应addresss,这里我们可以看到new了一个NotifySingleTask

public NotifySingleTask(String dataId, String group, String tenant, String tag, long lastModified,
                String target, boolean isBeta) {
            super(dataId, group, tenant, lastModified);
            this.target = target;
            this.isBeta = isBeta;
            try {
                dataId = URLEncoder.encode(dataId, Constants.ENCODE);
                group = URLEncoder.encode(group, Constants.ENCODE);
            } catch (UnsupportedEncodingException e) {
                LOGGER.error("URLEncoder encode error", e);
            }
            if (StringUtils.isBlank(tenant)) {
                this.url = MessageFormat.format(URL_PATTERN, target, EnvUtil.getContextPath(), dataId, group);
            } else {
                this.url = MessageFormat
                        .format(URL_PATTERN_TENANT, target, EnvUtil.getContextPath(), dataId, group, tenant);
            }
            if (StringUtils.isNotEmpty(tag)) {
                url = url + "&tag=" + tag;
            }
            failCount = 0;
            // this.executor = executor;
        }

在new的时候构造的url

private static final String URL_PATTERN =
                "http://{0}{1}" + Constants.COMMUNICATION_CONTROLLER_PATH + "/dataChange" + "?dataId={2}&group={3}";
        
        private static final String URL_PATTERN_TENANT =
                "http://{0}{1}" + Constants.COMMUNICATION_CONTROLLER_PATH + "/dataChange"
                        + "?dataId={2}&group={3}&tenant={4}";

会调用…/dataChange的api接口,这个接口是在CommunicationController类里面

@GetMapping("/dataChange")
    public Boolean notifyConfigInfo(HttpServletRequest request, @RequestParam("dataId") String dataId,
            @RequestParam("group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "tag", required = false) String tag) {
        dataId = dataId.trim();
        group = group.trim();
        String lastModified = request.getHeader(NotifyService.NOTIFY_HEADER_LAST_MODIFIED);
        long lastModifiedTs = StringUtils.isEmpty(lastModified) ? -1 : Long.parseLong(lastModified);
        String handleIp = request.getHeader(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP);
        String isBetaStr = request.getHeader("isBeta");
        if (StringUtils.isNotBlank(isBetaStr) && trueStr.equals(isBetaStr)) {
            dumpService.dump(dataId, group, tenant, lastModifiedTs, handleIp, true);
        } else {
            dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);
        }
        return true;
    }

nacos后台数据变更的时候,会调用上面的api接口,数据变更后会dump到磁盘上去,包含getConfig的时候也是从磁盘上获取的.
dump数据的时候会发布LocalDataChangeEvent事件,而这个时间的监听者就是LongPollingService

NotifyCenter.registerSubscriber(new Subscriber() {
            
            @Override
            public void onEvent(Event event) {
                if (isFixedPolling()) {
                    // Ignore.
                } else {
                    if (event instanceof LocalDataChangeEvent) {
                        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                    }
                }
            }
            
            @Override
            public Class<? extends Event> subscribeType() {
                return LocalDataChangeEvent.class;
            }
        });

监听到事件以后会把DataChangeTask放到异步线程里面去执行.

nacos热更新mysql密码 nacos更新配置_初始化_13


从代码中我们可以看到,是遍历所有的订阅者,然后调用

clientSub.sendResponse(Arrays.asList(groupKey));

响应数据给客户端.

而这个队列中的所有订阅者就是客户端发起轮询的时候,保存进来的.

首先客户端会定时轮询查看是否有数据的变更,如果有,那么就更新本地配置文件.
当后台数据变更的时候,通过事件的发布订阅也会通过http的方式通知给客户端.
配置更新既有客户端的主动拉取,也有服务端的主动推送,两者兼有.