从Nacos客户端谈Nacos配置中心

引入nacos配置中心依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

导入的依赖源码中有这样一个配置类,其中主要有这两个Bean
NacosConfigAutoConfiguration

@Bean
	public NacosConfigManager nacosConfigManager(
			NacosConfigProperties nacosConfigProperties) {
    
    
		return new NacosConfigManager(nacosConfigProperties);
	}

@Bean
	public NacosContextRefresher nacosContextRefresher(
			NacosConfigManager nacosConfigManager,
			NacosRefreshHistory nacosRefreshHistory) {
    
    
		// Consider that it is not necessary to be compatible with the previous
		// configuration
		// and use the new configuration if necessary.
		return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
	}

我们先看nacosContextRefresher这个方法传递了两个参数,NacosConfigManager,NacosRefreshHistory,其中NacosConfigManager就是上面初始化的一个Bean,NacosRefreshHistory盲猜一波,是nacos配置文件刷新的历史记录,里面保存了最近20个版本的配置记录。

一、NacosContextRefresher

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
    
    

	private final static Logger log = LoggerFactory
			.getLogger(NacosContextRefresher.class);

	private static final AtomicLong REFRESH_COUNT = new AtomicLong(0);

	private NacosConfigProperties nacosConfigProperties;

	private final boolean isRefreshEnabled;

	private final NacosRefreshHistory nacosRefreshHistory;

	private final ConfigService configService;

	private ApplicationContext applicationContext;

	private AtomicBoolean ready = new AtomicBoolean(false);

	private Map<String, Listener> listenerMap = new ConcurrentHashMap<>(16);

可以看到NacosContextRefresher实现了ApplicationListener接口,所以在我们的Bean初始化完成之后会调用onApplicationEvent方法:

@Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
    
    
		// many Spring context
		if (this.ready.compareAndSet(false, true)) {
    
    
			// 注册nacos监听器
			this.registerNacosListenersForApplications();
		}
	}

private void registerNacosListenersForApplications() {
    
    
		if (isRefreshEnabled()) {
    
    
			for (NacosPropertySource propertySource : NacosPropertySourceRepository
					.getAll()) {
    
    
				if (!propertySource.isRefreshable()) {
    
    
					continue;
				}
				// 获取配置文件的dataId,这个字眼很眼熟吧
				String dataId = propertySource.getDataId();
				// 进去这里看看
				registerNacosListener(propertySource.getGroup(), dataId);
			}
		}
	}


private void registerNacosListener(final String groupKey, final String dataKey) {
    
    
		String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
		// listenerMap中如果不存在此key,就创建一个Listener放进去,返回Listener
		Listener listener = listenerMap.computeIfAbsent(key,
				lst -> new AbstractSharedListener() {
    
    
					@Override
					public void innerReceive(String dataId, String group,
							String configInfo) {
    
    
						refreshCountIncrement();
						nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
						// todo feature: support single refresh for listening
						applicationContext.publishEvent(
								new RefreshEvent(this, null, "Refresh Nacos config"));
						if (log.isDebugEnabled()) {
    
    
							log.debug(String.format(
									"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
									group, dataId, configInfo));
						}
					}
				});
		try {
    
    
			// 添加Listener
			configService.addListener(dataKey, groupKey, listener);
		}
		catch (NacosException e) {
    
    
			log.warn(String.format(
					"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
					groupKey), e);
		}
	}

其中listenerMap是一个ConcurrentHashMap

private Map<String, Listener> listenerMap = new ConcurrentHashMap<>(16);

可以看到registerNacosListener方法其实就是看listenerMap中如果不存在此key,就创建一个Listener放进去,返回Listener
然后调用configService.addListener方法添加Listener

我们再看看addListener方法干了些什么
NacosConfigService

@Override
    public void addListener(String dataId, String group, Listener listener) throws NacosException {
    
    
        worker.addTenantListeners(dataId, group, Arrays.asList(listener));
    }

调用worker的addTenantListeners方法,worker是NacosConfigService类中的一个属性

private final ClientWorker worker;

而ClientWorker是在NacosConfigService被创建的时候就已经初始化了的,这个稍后再说。

我们进去addTenantListeners方法看看:
ClientWorker:

public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
            throws NacosException {
    
    
        group = null2defaultGroup(group);
        String tenant = agent.getTenant();
        // 获取一个CacheData
        CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
        for (Listener listener : listeners) {
    
    
        	// listener最后被放进了CacheData 
            cache.addListener(listener);
        }
    }

先看addCacheDataIfAbsent方法:
ClientWorker:

扫描二维码关注公众号,回复: 17247612 查看本文章
// cacheMap 是ClientWorker的一个属性,是一个ConcurrentHashMap
private final ConcurrentHashMap<String, CacheData> cacheMap = new ConcurrentHashMap<String, CacheData>();

// 真正的方法
public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant) throws NacosException {
    
    
		// key就是dataId, group, tenant组装成的一串字符串
        String key = GroupKey.getKeyTenant(dataId, group, tenant);
        CacheData cacheData = cacheMap.get(key);
        // 如果存在cacheData 就直接返回
        if (cacheData != null) {
    
    
            return cacheData;
        }
    	// cacheMap中不存在则创建一个CacheData,放入cacheMap再返回
        cacheData = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant);
        // multiple listeners on the same dataid+group and race condition
        CacheData lastCacheData = cacheMap.putIfAbsent(key, cacheData);
        if (lastCacheData == null) {
    
    
            //fix issue # 1317
            if (enableRemoteSyncConfig) {
    
    
                String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                cacheData.setContent(ct[0]);
            }
            int taskId = cacheMap.size() / (int) ParamUtil.getPerTaskConfigSize();
            cacheData.setTaskId(taskId);
            lastCacheData = cacheData;
        }
        
        // reset so that server not hang this check
        lastCacheData.setInitializing(true);
        
        LOGGER.info("[{}] [subscribe] {}", agent.getName(), key);
        MetricsMonitor.getListenConfigCountMonitor().set(cacheMap.size());
        
        return lastCacheData;
    }

可以看到,这个方法里面就是判断cacheMap 中有没有对应的CacheData,如果存在则直接返回CacheData,不存在则创建一个CacheData,放入cacheMap再返回。
CacheData 中的成员变量,如下所示:

private final String name;
    
    private final ConfigFilterChainManager configFilterChainManager;
    
    public final String dataId;
    
    public final String group;
    
    public final String tenant;
    //监听器
    
    private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
    
    private volatile String md5;
    
    /**
     * whether use local config.
     */
    private volatile boolean isUseLocalConfig = false;
    
    /**
     * last modify time.
     */
    private volatile long localConfigLastModified;
    
    private volatile String content;
    
    private int taskId;
    
    private volatile boolean isInitializing = true;
    
    private String type;

我们可以看到,成员变量包括tenant ,dataId,group,content,taskId等,还有两个值得我们关注的:

  • listeners
  • md5

好,我们回到上面看cache.addListener(listener);方法

public void addListener(Listener listener) {
    
    
        if (null == listener) {
    
    
            throw new IllegalArgumentException("listener is null");
        }
        // 将Listener包装成ManagerListenerWrap
        ManagerListenerWrap wrap =
                (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
                        : new ManagerListenerWrap(listener, md5);
       	// 添加到CopyOnWriteArrayList
        if (listeners.addIfAbsent(wrap)) {
    
    
            LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group,
                    listeners.size());
        }
    }

listeners 是该 CacheData 所关联的所有 listener,不过不是保存的原始的 Listener 对象,而是包装后的 ManagerListenerWrap 对象,该对象除了持有 Listener 对象,还持有了一个 lastCallMd5 和lastContent属性。

private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
private static class ManagerListenerWrap {
    
    
        
        final Listener listener;
        
        //关注
        String lastCallMd5 = CacheData.getMd5String(null);
        
        String lastContent = null;
        
        ManagerListenerWrap(Listener listener) {
    
    
            this.listener = listener;
        }
        
        ManagerListenerWrap(Listener listener, String md5) {
    
    
            this.listener = listener;
            this.lastCallMd5 = md5;
        }
        
        ManagerListenerWrap(Listener listener, String md5, String lastContent) {
    
    
            this.listener = listener;
            this.lastCallMd5 = md5;
            this.lastContent = lastContent;
        }
        
    }

另外一个属性 md5 就是根据当前对象的 content 计算出来的 md5 值。

到这里我们已经知道,Listener最后是添加到CacheData类中的一个CopyOnWriteArrayList中。
那NacosContextRefresher里面做了些什么事情到此就告一段落…

二、接下来我们看NacosConfigAutoConfiguration配置类的nacosConfigManager方法做了什么:

NacosConfigAutoConfiguration

@Bean
	public NacosConfigManager nacosConfigManager(
			NacosConfigProperties nacosConfigProperties) {
    
    
		return new NacosConfigManager(nacosConfigProperties);
	}

进去构造方法里面看看:

public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
    
    
		this.nacosConfigProperties = nacosConfigProperties;
		// Compatible with older code in NacosConfigProperties,It will be deleted in the
		// future.
		// 创建ConfigService,进去看看
		createConfigService(nacosConfigProperties);
	}


static ConfigService createConfigService(
			NacosConfigProperties nacosConfigProperties) {
    
    
		if (Objects.isNull(service)) {
    
    
			synchronized (NacosConfigManager.class) {
    
    
				try {
    
    
					if (Objects.isNull(service)) {
    
    
						// 使用Nacos工厂创建一个ConfigService,进去看看
						service = NacosFactory.createConfigService(
								nacosConfigProperties.assembleConfigServiceProperties());
					}
				}
				catch (NacosException e) {
    
    
					log.error(e.getMessage());
					throw new NacosConnectionFailureException(
							nacosConfigProperties.getServerAddr(), e.getMessage(), e);
				}
			}
		}
		return service;
	}

NacosFactory

public static ConfigService createConfigService(Properties properties) throws NacosException {
    
    
        return ConfigFactory.createConfigService(properties);
    }

ConfigFactory

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 e) {
    
    
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }

可以看到在ConfigFactory的createConfigService方法中使用了反射创建了一个ConfigService的子类NacosConfigService ,那我们去NacosConfigService 的构造方法看看里面做了什么

NacosConfigService:

public class NacosConfigService implements ConfigService {
    
    
    
    private static final Logger LOGGER = LogUtils.logger(NacosConfigService.class);
    
    private static final long POST_TIMEOUT = 3000L;
    
    /**
     * http agent.
     */
    private final HttpAgent agent;
    
    /**
     * long polling.
     */
    private final ClientWorker worker;
    
    private String namespace;
    
    private final String encode;
    
    private final ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager();

	// 构造方法
	public NacosConfigService(Properties properties) throws NacosException {
    
    
        ValidatorUtils.checkInitParam(properties);
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
    
    
            this.encode = Constants.ENCODE;
        } else {
    
    
            this.encode = encodeTmp.trim();
        }
        initNamespace(properties);
        // 创建一个Http代理,很显然是用来跟Nacos服务端进行通信的
        this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        this.agent.start();
        // 这个才是真正干活儿的人
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }

实例化时主要是初始化了两个对象,他们分别是:

  • HttpAgent
  • ClientWorker

进去ClientWorker构造方法看看里面有什么

ClientWorker

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
            final Properties properties) {
    
    
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;
        
        // Initialize the timeout parameter
        
        init(properties);
        // 创建一个只有1个线程的定时任务线程池
        this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
    
    
            @Override
            public Thread newThread(Runnable r) {
    
    
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });
        //创建了一个保持长轮询的线程池
        this.executorService = Executors
                .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
    
    
                    @Override
                    public Thread newThread(Runnable r) {
    
    
                        Thread t = new Thread(r);
                        t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                        t.setDaemon(true);
                        return t;
                    }
                });
        // 延迟任务线程池来每隔10ms来检查配置信息的线程池
        this.executor.scheduleWithFixedDelay(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                	// 检查配置信息,重点!!!
                    checkConfigInfo();
                } catch (Throwable e) {
    
    
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

创建了两个线程池,分别赋值给了executor和executorService,

final ScheduledExecutorService executor;
    
final ScheduledExecutorService executorService;

其中executor 在启动之后延时1ms后,每隔10ms周期性的检查配置信息。

进去checkConfigInfo方法看看:

ClientWorker:

public void checkConfigInfo() {
    
    
        // 分任务(解决大数据量的传输问题)
        int listenerSize = cacheMap.size();
        // 向上取整为批数,分批次进行检查
        //ParamUtil.getPerTaskConfigSize() =3000
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
    
    
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
    
    
                // 要判断任务是否在执行 这块需要好好想想。 任务列表没有顺序。所以在改变的时候可能会有问题。
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

而executorService线程池则执行LongPollingRunnable任务,构造方法传递的一个taskId

LongPollingRunnable

class LongPollingRunnable implements Runnable {
    
    
        
        private final int taskId;
        
        public LongPollingRunnable(int taskId) {
    
    
            this.taskId = taskId;
        }
        
        @Override
        public void run() {
    
    
            // 创建一个List
            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
    
    
                // check failover config
                for (CacheData cacheData : cacheMap.values()) {
    
    
                    if (cacheData.getTaskId() == taskId) {
    
    
                    	// 将对应的cacheData添加至刚才创建的List<CacheData> ,这里面放的是旧的CacheData
                        cacheDatas.add(cacheData);
                        try {
    
    
                        	// 核心1:检查本地配置
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
    
    
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
    
    
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }
                
               	// 检查Nacos服务端发生变化的配置文件dataId
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                if (!CollectionUtils.isEmpty(changedGroupKeys)) {
    
    
                    LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
                }
                // 进行遍历
                for (String groupKey : changedGroupKeys) {
    
    
                    String[] key = GroupKey.parseKey(groupKey);
                    String dataId = key[0];
                    String group = key[1];
                    String tenant = null;
                    if (key.length == 3) {
    
    
                        tenant = key[2];
                    }
                    try {
    
    
                    	// 根据dataId获取服务端的配置文件,里面使用到了Http agent
                        String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                        // 核心1.1
                        // ct[0]就是配置文件内容,在这里面做了两件事情
                        // 1:更新了配置文件的内容 2:更新了配置文件的MD5值
                        cache.setContent(ct[0]);
                        if (null != ct[1]) {
    
    
                            cache.setType(ct[1]);
                        }
                        LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                                agent.getName(), dataId, group, tenant, cache.getMd5(),
                                ContentUtils.truncateContent(ct[0]), ct[1]);
                    } catch (NacosException ioe) {
    
    
                        String message = String
                                .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                        agent.getName(), dataId, group, tenant);
                        LOGGER.error(message, ioe);
                    }
                }
               	// 遍历旧的CacheData
                for (CacheData cacheData : cacheDatas) {
    
    
                    if (!cacheData.isInitializing() || inInitializingCacheList
                            .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
    
    
                       	// 核心2:检查配置文件MD5是否发生变化,重点!!!
                        cacheData.checkListenerMd5();
                        cacheData.setInitializing(false);
                    }
                }
                inInitializingCacheList.clear();
                
                executorService.execute(this);
                
            } catch (Throwable e) {
    
    
                
                // If the rotation training task is abnormal, the next execution time of the task will be punished
                LOGGER.error("longPolling error : ", e);
                executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
            }
        }
    }
核心1:检查本地配置
private void checkLocalConfig(CacheData cacheData) {
    
    
        final String dataId = cacheData.dataId;
        final String group = cacheData.group;
        final String tenant = cacheData.tenant;
        //本地缓存文件
        File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
        //不使用本地配置,但是持久化文件存在,需要读取文件加载至内存
        if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
    
    
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);
            
            LOGGER.warn(
                    "[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
                    agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
            return;
        }
        
        // If use local config info, then it doesn't notify business listener and notify after getting from server.
        //使用本地配置,但是持久化文件不存在 
        if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
    
    
            cacheData.setUseLocalConfigInfo(false);
            LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
                    dataId, group, tenant);
            return;
        }
        
        // 有变更
        //使用本地配置,持久化文件存在,缓存跟文件最后修改时间不一致
        if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
                .lastModified()) {
    
    
            String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
            final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
            cacheData.setUseLocalConfigInfo(true);
            cacheData.setLocalConfigInfoVersion(path.lastModified());
            cacheData.setContent(content);
            LOGGER.warn(
                    "[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
                    agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
        }
    }

本地检查主要是通过是否使用本地配置,继而寻找持久化缓存文件,再通过判断文件的最后修改事件与本地缓存的版本是否一致来判断是否由变更

通过跟踪 checkLocalConfig 方法,可以看到 Nacos 将缓存配置信息保存在了

~/nacos/config/fixed-{address}_8848_nacos/snapshot/DEFAULT_GROUP/{dataId}
这个文件中,我们看下这个文件中保存的内容,如下图所示:
在这里插入图片描述

1. 触发回调
核心2:checkListenerMd5

现在我们对 ConfigService 有了大致的了解了,现在剩下最后一个重要的问题还没有答案,那就是 ConfigService 的 Listener 是在什么时候触发回调方法 receiveConfigInfo 的。

现在让我们回过头来想一下,在 ClientWorker 中的定时任务中,启动了一个长轮询的任务:LongPollingRunnable,该任务多次执行了 cacheData.checkListenerMd5() 方法,那现在就让我们来看下这个方法到底做了些什么,如下所示:

void checkListenerMd5() {
    
    
        for (ManagerListenerWrap wrap : listeners) {
    
    
            if (!md5.equals(wrap.lastCallMd5)) {
    
    
                safeNotifyListener(dataId, group, content, type, md5, wrap);
            }
        }
    }

到这里应该就比较清晰了,该方法会检查 CacheData 当前的 md5 与 CacheData 持有的所有 Listener 中保存的 md5 的值是否一致,如果不一致,就执行一个安全的监听器的通知方法:safeNotifyListener,通知什么呢?我们可以大胆的猜一下,应该是通知 Listener 的使用者,该 Listener 所关注的配置信息已经发生改变了。

现在让我们来看一下 safeNotifyListener 方法,如下所示:


private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
            final String md5, final ManagerListenerWrap listenerWrap) {
    
    
        final Listener listener = listenerWrap.listener;
        
        Runnable job = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
    
    
                    if (listener instanceof AbstractSharedListener) {
    
    
                        AbstractSharedListener adapter = (AbstractSharedListener) listener;
                        adapter.fillContext(dataId, group);
                        LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
                    }
                    // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                    Thread.currentThread().setContextClassLoader(appClassLoader);
                    
                    ConfigResponse cr = new ConfigResponse();
                    cr.setDataId(dataId);
                    cr.setGroup(group);
                    cr.setContent(content);

					//重点关注,在这里调用
                    //重点关注,在这里调用
                    //重点关注,在这里调用
                    configFilterChainManager.doFilter(null, cr);
                    String contentTmp = cr.getContent();
                    // 核心3:在这里面将最新的配置文件信息更新到本地环境的配置,这样在使用的时候就是使用最新的配置值了
                    listener.receiveConfigInfo(contentTmp);
                    
                    // compare lastContent and content
                    if (listener instanceof AbstractConfigChangeListener) {
    
    
                        Map data = ConfigChangeHandler.getInstance()
                                .parseChangeData(listenerWrap.lastContent, content, type);
                        ConfigChangeEvent event = new ConfigChangeEvent(data);
                        ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
                        listenerWrap.lastContent = content;
                    }
                    
                    listenerWrap.lastCallMd5 = md5;
                    LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
                            listener);
                } catch (NacosException ex) {
    
    
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
                            name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
                } catch (Throwable t) {
    
    
                    LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
                            group, md5, listener, t.getCause());
                } finally {
    
    
                    Thread.currentThread().setContextClassLoader(myClassLoader);
                }
            }
        };
        
        final long startNotify = System.currentTimeMillis();
        try {
    
    
            if (null != listener.getExecutor()) {
    
    
                listener.getExecutor().execute(job);
            } else {
    
    
                job.run();
            }
        } catch (Throwable t) {
    
    
            LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
                    group, md5, listener, t.getCause());
        }
        final long finishNotify = System.currentTimeMillis();
        LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
                name, (finishNotify - startNotify), dataId, group, md5, listener);
    }

可以看到在 safeNotifyListener 方法中,重点关注下注释中的三行代码:获取最新的配置信息,调用 Listener 的回调方法,将最新的配置信息作为参数传入,这样 Listener 的使用者就能接收到变更后的配置信息了,最后更新 ListenerWrap 的 md5 值。和我们猜测的一样, Listener 的回调方法就是在该方法中触发的。

2. Md5何时变更

那 CacheData 的 md5 值是何时发生改变的呢?我们可以回想一下,在上面的 LongPollingRunnable 所执行的任务中,在获取服务端发生变更的配置信息时,将最新的 content 数据写入了 CacheData 中,我们可以看下

核心1.1:

public void setContent(String content) {
    
    
        this.content = content;
        this.md5 = getMd5String(this.content);
    }

可以看到是在长轮询的任务中,当服务端配置信息发生变更时,客户端将最新的数据获取下来之后,保存在了 CacheData 中,同时更新了该 CacheData 的 md5 值,所以当下次执行 checkListenerMd5 方法时,就会发现当前 listener 所持有的 md5 值已经和 CacheData 的 md5 值不一样了,也就意味着服务端的配置信息发生改变了,这时就需要将最新的数据通知给 Listener 的持有者。

核心3:
AbstractSharedListener

@Override
    public final void receiveConfigInfo(String configInfo) {
    
    
    	// 调用innerReceive方法
        innerReceive(dataId, group, configInfo);
    }

而我们的Listener是在添加listenerMap中new 的一个匿名Listener,所以最后会到innerReceive方法:

private void registerNacosListener(final String groupKey, final String dataKey) {
    
    
		String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
		Listener listener = listenerMap.computeIfAbsent(key,
				lst -> new AbstractSharedListener() {
    
    
					@Override
					public void innerReceive(String dataId, String group,
							String configInfo) {
    
    
						refreshCountIncrement();
						// 添加刷新记录
						nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
						// 发布一个事件,这里面去刷新了本地环境的配置!!!
						applicationContext.publishEvent(
								new RefreshEvent(this, null, "Refresh Nacos config"));
						if (log.isDebugEnabled()) {
    
    
							log.debug(String.format(
									"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
									group, dataId, configInfo));
						}
					}
				});
		try {
    
    
			configService.addListener(dataKey, groupKey, listener);
		}
		catch (NacosException e) {
    
    
			log.warn(String.format(
					"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
					groupKey), e);
		}
	}

事件监听者:
RefreshEventListener

@Override
	public void onApplicationEvent(ApplicationEvent event) {
    
    
		if (event instanceof ApplicationReadyEvent) {
    
    
			handle((ApplicationReadyEvent) event);
		}
		else if (event instanceof RefreshEvent) {
    
    
			handle((RefreshEvent) event);
		}
	}

public void handle(RefreshEvent event) {
    
    
		if (this.ready.get()) {
    
     // don't handle events before app is ready
			log.debug("Event received " + event.getEventDesc());
			// 刷新配置!!!进去看看
			Set<String> keys = this.refresh.refresh();
			log.info("Refresh keys changed: " + keys);
		}
	}

ContextRefresher


public synchronized Set<String> refresh() {
    
    
		// 在这里更新,进去看看
		Set<String> keys = refreshEnvironment();
		this.scope.refreshAll();
		return keys;
	}

public synchronized Set<String> refreshEnvironment() {
    
    
		Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
		// !!!!!!
		updateEnvironment();
		Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
		return keys;
	}

至此配置中心的完整流程已经分析完毕了,可以发现,Nacos 并不是通过推的方式将服务端最新的配置信息发送给客户端的,而是客户端维护了一个长轮询的任务,定时去拉取发生变更的配置信息,然后将最新的数据推送给 Listener 的持有者,然后更新本地环境配置。

3. 拉的优势

客户端拉取服务端的数据与服务端推送数据给客户端相比,优势在哪呢,为什么 Nacos 不设计成主动推送数据,而是要客户端去拉取呢?如果用推的方式,服务端需要维持与客户端的长连接,这样的话需要耗费大量的资源,并且还需要考虑连接的有效性,例如需要通过心跳来维持两者之间的连接。而用拉的方式,客户端只需要通过一个无状态的 http 请求即可获取到服务端的数据。

4. 总结

在这里插入图片描述

现在,我们来简单复盘一下Nacos客户端视角下的配置中心实现原理

首先我们假设Nacos服务端一切正常,Nacos客户端启动以后

第一步是根据我们配置的服务端信息,新建 ConfigService 实例,它的实现就是我们文中提到的NacosConfigService;

第二步可以通过相应的接口获取配置和注册配置监听器,

考虑到服务端故障的问题,客户端将最新数据获取后会保存在本地的 缓存文件中,以后会优先从文件中获取配置信息的值,如果获取不到,会直接从服务器拉去,并保存到缓存中;

其实真正干活的就是ClientWorker类;客户端是通过一个定时的长轮询来检查自己监听的配置项的数据的,一旦服务端的数据发生变化时,会从服务端获取到dataID的列表,

客户端根据dataID列表从服务端获取到最新的数据,并将最新的数据保存在一个 CacheData 对象中,在轮询过程中,如果决定使用本地配置,就会比较当前CacheData 的MD5值是否和所有监听者所持有的MD5值相等,如果不相等,此时就会对该 CacheData 所绑定的 Listener 触发 receiveConfigInfo 回调,来通知使用者此配置信息已经变更;

猜你喜欢

转载自blog.csdn.net/RookiexiaoMu_a/article/details/125346337