Talking about Nacos configuration center from Nacos client

Introducing nacos configuration center dependencies

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

There is such a configuration class in the imported dependency source code, which mainly includes these two Beans
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);
	}

Let's first look at the nacosContextRefresher method that passes two parameters, NacosConfigManager and NacosRefreshHistory. NacosConfigManager is a Bean initialized above. NacosRefreshHistory is a blind guess. It is the history record of nacos configuration file refresh, which saves the configuration records of the last 20 versions. .

一、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);

You can see that NacosContextRefresher implements the ApplicationListener interface, so the onApplicationEvent method will be called after our Bean initialization is completed:

@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);
		}
	}

where listenerMap is a ConcurrentHashMap

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

You can see that the registerNacosListener method actually looks at the listenerMap. If the key does not exist, create a Listener and put it in, return the Listener
and then call the configService.addListener method to add the Listener

Let’s take a look at what the addListener method does
NacosConfigService

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

Call the addTenantListeners method of worker. worker is an attribute in the NacosConfigService class.

private final ClientWorker worker;

The ClientWorker is already initialized when NacosConfigService is created, which will be discussed later.

Let’s go into the addTenantListeners method and take a look:
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);
        }
    }

Look at the addCacheDataIfAbsent method first:
ClientWorker:

// 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;
    }

As you can see, this method is to determine whether there is a corresponding CacheData in the cacheMap. If it exists, it will directly return the CacheData. If it does not exist, it will create a CacheData, put it in the cacheMap and return it.
Member variables in CacheData, as shown below:

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;

We can see that member variables include tenant, dataId, group, content, taskId, etc., and there are two more worthy of our attention:

  • listeners
  • md5

Okay, let’s go back to the cache.addListener(listener); method

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 are all the listeners associated with the CacheData, but they are not the saved original Listener objects, but the wrapped ManagerListenerWrap objects. In addition to holding the Listener objects, this object also holds a lastCallMd5 and lastContent properties.

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;
        }
        
    }

Another attribute md5 is the md5 value calculated based on the content of the current object.

We already know here that the Listener is finally added to a CopyOnWriteArrayList in the CacheData class.
So what is done in NacosContextRefresher, this is the end...

2. Next, let’s look at what the nacosConfigManager method of the NacosConfigAutoConfiguration configuration class does:

NacosConfigAutoConfiguration

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

Go into the constructor and take a look:

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);
        }
    }

You can see that reflection is used in the createConfigService method of ConfigFactory to create a subclass of ConfigService, NacosConfigService. Then we go to the construction method of NacosConfigService to see what is done inside.

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);
    }

During instantiation, two objects are mainly initialized. They are:

  • HttpAgent
  • ClientWorker

Go into the ClientWorker constructor and see what's inside

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);
    }

Two thread pools were created and assigned to executor and executorService respectively.

final ScheduledExecutorService executor;
    
final ScheduledExecutorService executorService;

The executor will periodically check the configuration information every 10ms after a delay of 1ms after startup.

Go into the checkConfigInfo method and take a look:

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;
        }
    }

The executorService thread pool executes the LongPollingRunnable task, and a taskId passed in the constructor method

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);
            }
        }
    }
Core 1: Check local configuration
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));
        }
    }

The local check mainly checks whether the local configuration is used, then looks for the persistent cache file, and then determines whether the last modification event of the file is consistent with the locally cached version to determine whether the change is caused by

By tracing the checkLocalConfig method, you can see that Nacos saves the cache configuration information in

~/nacos/config/fixed-{address}_8848_nacos/snapshot/DEFAULT_GROUP/{dataId}
In this file, let’s take a look at the content saved in this file, as shown below Shown:
Insert image description here

1. Trigger callback
Core 2: checkListenerMd5

Now that we have a general understanding of ConfigService, the last important question remains unanswered, which is when the Listener of ConfigService triggers the callback method receiveConfigInfo.

Now let's go back and think about it. In the scheduled task in ClientWorker, a long polling task is started: LongPollingRunnable. This task executes the cacheData.checkListenerMd5() method multiple times. So now let's take a look at this What exactly the method does is as follows:

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

It should be clear at this point. This method will check whether the current md5 of CacheData is consistent with the md5 values ​​​​saved in all Listeners held by CacheData. If they are inconsistent, a safe listener notification method will be executed: safeNotifyListener. What to notify Woolen cloth? We can make a bold guess that the user of the Listener should be notified that the configuration information that the Listener is concerned about has changed.

Now let us take a look at the safeNotifyListener method as shown below:


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);
    }

You can see that in the safeNotifyListener method, focus on the three lines of code in the comments: obtain the latest configuration information, call the Listener's callback method, and pass in the latest configuration information as parameters, so that users of the Listener can receive the changes After the configuration information is obtained, the md5 value of ListenerWrap is finally updated. As we guessed, the callback method of Listener is triggered in this method.

2. When will MD5 change?

So when did the md5 value of CacheData change? We can recall that in the task performed by LongPollingRunnable above, when obtaining the changed configuration information on the server side, the latest content data is written into CacheData. We can take a look at

Core 1.1:

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

It can be seen that in the long polling task, when the server configuration information changes, the client obtains the latest data, saves it in CacheData, and updates the md5 value of the CacheData, so when the next execution When checking the ListenerMd5 method, you will find that the md5 value held by the current listener is different from the md5 value of CacheData, which means that the configuration information of the server has changed. At this time, the latest data needs to be notified to the Listener's holder. Those who have.

核心3:
AbstractSharedListener

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

And our Listener is an anonymous Listener added to the new in listenerMap, so we will finally reach the innerReceive method:

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);
		}
	}

Event listener:
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;
	}

At this point, the complete process of the configuration center has been analyzed. It can be found that Nacos does not send the latest configuration information of the server to the client through push. Instead, the client maintains a long polling task and pulls it regularly. The changed configuration information is then pushed to the holder of the Listener with the latest data, and then the local environment configuration is updated.

3. Advantages of pulling

What is the advantage of the client pulling data from the server compared to the server pushing data to the client? Why is Nacos not designed to actively push data, but requires the client to pull it? If the push method is used, the server needs to maintain a long connection with the client, which consumes a lot of resources, and the validity of the connection also needs to be considered, for example, a heartbeat is needed to maintain the connection between the two. With the pull method, the client only needs to obtain the data from the server through a stateless http request.

4. Summary

Insert image description here

Now, let’s briefly review the implementation principle of the configuration center from the perspective of the Nacos client.

First, we assume that everything is normal on the Nacos server. After the Nacos client is started,

The first step is to create a new ConfigService instance based on the server information we configured. Its implementation is the NacosConfigService mentioned in our article;

In the second step, you can obtain the configuration and register the configuration listener through the corresponding interface.

Considering the problem of server failure, the client will save the latest data in a local cache file after obtaining it. In the future, it will give priority to obtaining the value of the configuration information from the file. If it cannot obtain it, it will directly pull it from the server and save it. into cache;

In fact, the real work is the ClientWorker class; the client checks the data of the configuration items it monitors through a scheduled long polling. Once the data on the server changes, it will obtain a list of dataIDs from the server.

The client obtains the latest data from the server based on the dataID list and saves the latest data in a CacheData object. During the polling process, if it decides to use local configuration, it will compare the MD5 value of the current CacheData with all listeners. The MD5 values ​​held by the users are equal. If they are not equal, the receiveConfigInfo callback will be triggered on the Listener bound to the CacheData to notify the user that this configuration information has changed;

おすすめ

転載: blog.csdn.net/RookiexiaoMu_a/article/details/125346337