Nacos系列(三):nacos实现统一配置管理

1、配置中心

1.1、传统配置方式的局限

  • 无法实现配置的动态更新,修改配置文件,需要重启服务
  • 配置集中式管理,服务节点一多,修改配置文件,需要同时修改多个文件
  • 配置内容的安全性和权限
  • 不用配置环境下配置管理,不方便维护

1.2、nacos配置中心简介

  • [1] nacos主要有confug service和Naming Service组成,Config service 为Nacos配置中心核心模块,实现 了配置的增删改查、版本管理、灰度管理、监听管理。
  • [2] nacos 入门案例
  • -客户端:创建工程,引入相关依赖
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

springboot工程有两种配置文件,application和bootstrap(yaml/properties);由于优先加载bootstrap,所有在工程中,我们使用bootstrap.properties

spring:
     cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #服务中心地址
      config: #配置中心模块配置
        server-addr: ${
     spring.cloud.nacos.discovery.server-addr}
        file-extension: properties #文件类型

配置文件获取

@RequestMapping
@RestController
@RefreshScope //声明可动态获取配置文件
public class IndexController {
     
    @Value("${hello}")
    private String hello;
    @GetMapping("index")
    public String index() {
     
        return hello;
    }
}

2、Nacos Config实现原理解析

Nacos称为配置中心通过open api和SDK提供四种主要功能

  • 获取配置文件:客户关从Nacos Config Server 中读取配置文件
SDK: public String getConfig(String dataId,String group,long timeoutMs)
API: /nacos/v1/cs/configs
  • 监听配置:订阅相关配置文件,当发生变化时可以收到相关的时间
SDK:public void addLlisterener(String dataId,String group,String content)
API:/nacos/v1/cs/configs
  • 发布配置:将配置保存到Nacos Config Server
SDK: public boolean pulicshConfig(String dataId,String group)
APi:/nacos/v1/cs/configs
  • 删除配置
SDK: public boolen removeConfig(String dataId,String group)
API:/nacos/v1/cs?configs

只要是两种行为,配置的crud和配置的动态监听

2.1、配置的CRUD

对于nacos配置来说,主要提供提供了集中化管理功能,然后对外提供操作配置文件的crud接口。

  1. 服务端提供配置的增删改查及持久化功能
  2. 对于客户端,从服务端获取相应的配置文件

2.2、动态监听之pull和push

当nacos配置文件发生变化时,需要让对应的服务动态感知到配置的变化,通过客户端监听来实现。

一般来说,客户端和服务端交互主要通过两种方式pull和push

  • pull表示客户端从客户端主动拉取数据
  • push表示服务端主动把数据推送到客户端
  1. 对于push模式,服务端需要维护与客户端的长连接,1.在客户端数量较多的情况下,服务端将耗费大量的内存来保存连接,2.并还需要心跳来检测连接的有效性
  2. 在pull模式下,客户端需要定时从客户端拉取一次数据,主要有两个缺点:1.定时任务必然存在一定的时间差,所以会造成延时;2.在配置不发生变化的情况下,会发生一些无效的pull
  3. Nacos采用的是特殊的pull模式,通过客户端发起pull,并采用长轮询方式。具体来说,客户端采用长轮询的方式发起pull请求,检查配置是否发生变化;
  • 1.变化,客户端直接获取最新的配置文件,
  • 2.如果配置文件未发生变化,服务端会hold住这个请求,加入allSubs队列,等待配置文件发生变化或者维持长连接直到29.5s后,将结果返回给客户端。

保证了客户端能够实时感知到服务端的配置变化;减少服务的压力

3.Spring Cloud 如何实现配置的加载

Spring Cloud 基于spring来扩展,获取context.getEnvironment.getProperty(“info”)来获取配置信息

首先Spring提供了Envirenment来表示spring应用程序配置信息,整合相关环境

spring启动时,将配置加载进environment中,当创建一个bean时从Environment中把一些属性值通过@value的形式注入业务代码

SpringCloud,实现的是配置统一管理的功能

  • 如何将远程服务 器上的配置信息加载进environment
  • 配置变更时,如何将新的配置更新到environment中,保证配置的动态刷新

3.1、PropertySourceBootstrapConfiguration

PropertySourceBootstrapConfiguration是一个启动环境配置类,该类中有个initialize方法,调用PropertySourceLocator.locate()来获取远程配置信息。为加载远配置的核心方法。

-PropertySourceBootstrapConfiguration基于springboot自动装配完成。

  1. springboot启动过程中,在springboot.run的方法进行环境准备,
/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
     
		
		try {
     
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			//环境准备
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			
			if (this.logStartupInfo) {
     
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
	
		}
		catch (Throwable ex) {
     
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}


		return context;
	}
  • prepareEnvironment方法中,会发布ApplicationEnvironmentPreparedEvent事件,通过事件监听实现对配置类初始化
	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
     
		// Create and configure the environment
        //事件发布
		listeners.environmentPrepared(environment);
		return environment;
	}
  • 其中BootstrapApplicationListener监听上发布的事件进行处理,将配置bean初始化
@Override
	public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
     
		ConfigurableEnvironment environment = event.getEnvironment();
		if (context == null) {
     
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
			event.getSpringApplication()
					.addListeners(new CloseContextOnFailureApplicationListener(context));
		}

	}
	//---
	private ConfigurableApplicationContext bootstrapServiceContext(
		builder.sources(BootstrapImportSelectorConfiguration.class);

	//spring boot自动装配,加载bean配置信息,实例化 初始化	bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);

		return context;
	}	
  • BootstrapImportSelectorConfigurationq 为配置类,该配置通过@Import导入BootstrapImportSelector实现自动装配
@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {
     

}

在BootstrapImportSelector类中的selectImports方法中,采用spring的spi机制,在classpaht路径下的META_INF/spring,facories,其中key为BootstrapConfiguration

//spring-cloud-context.jar
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.cloud.util.random.CachedRandomPropertySourceAutoConfiguration
//spring-cloud-alibaba-nacos-config.jar
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration

PropertySourceBootstrapConfiguration和NacosConfigBootstrapConfiguration

以上主要通过监听触发,利用spring spi机制完成bean的完成

3.2、PropertySourceLocator

准备完成后,开始进行配置的加载。回到spring启动类的run方法,prepareContext。

public ConfigurableApplicationContext run(String... args) {
     
		
		try {
     
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
、、、、
//刷新上下文准备
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
		}
		catch (Throwable ex) {
     
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}


		return context;
	}

prepareContext 开始进行上下文刷新的准备阶段,接着调用applyInitializers

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
			SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
     
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
//
		applyInitializers(context);
	}
//
	protected void applyInitializers(ConfigurableApplicationContext context) {
     
		for (ApplicationContextInitializer initializer : getInitializers()) {
     
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
					ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			initializer.initialize(context);
		}
	}

PropertySourceBootstrapConfiguration实现了ApplicationContextInitializer接口,接着调用了initialize方法。最终PropertySourceLocator,locate加载配置文件

4、Nacos config核心代解析

Spring Cloud alibaba Nacos Config 中通过context.getEnvironment().getProperties("info“”)获得Nacos Config服务器上的数据,最重要的实现类为NacosPropertySourceLocator,主要作用有两点

  • 初始化ConfigService,Nacos客户端提供的用户访问配置中心基本操作的类
  • 按照顺序分别加载共享配置‘、扩展配置、应用名称对应的配置
	@Override
	public PropertySource<?> locate(Environment env) {
     
		nacosConfigProperties.setEnvironment(env);
		ConfigService configService = nacosConfigManager.getConfigService();
		loadSharedConfiguration(composite);
		loadExtConfiguration(composite);
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

		return composite;
	}

调用方法链
loadApplicationConfiguration---->loadNacosDataIfPresent---->loadNacosPropertySource----->build----->loadNacosData
通过 configService.getConfig获取nacos配置文件

	private Map<String, Object> loadNacosData(String dataId, String group,
			String fileExtension) {
     
		String data = null;
		try {
     
			data = configService.getConfig(dataId, group, timeout);
			Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
					.parseNacosData(data, fileExtension);
			return dataMap == null ? EMPTY_MAP : dataMap;
		}
		catch (NacosException e) {
     
			log.error("get data from Nacos error,dataId:{}, ", dataId, e);
		}
		catch (Exception e) {
     
			log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
		}
		return EMPTY_MAP;
	}

最终配置文件获取configService.getConfig从nacos上加载配置进行填充。
那么事件订阅机制如何实现?NacoscontextRefresher类。里面实现ApplicationReadyEvent事件监听,在上下文准备完成触发这个事件

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
     
	@Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
     
		// many Spring context
		if (this.ready.compareAndSet(false, true)) {
     
			this.registerNacosListenersForApplications();
		}
	}
}

当监听收到事件后,会调用registerNacosListenersForApplications方法来说来实现Nacos监听的注册,代码如下:

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

上面代码是说当配置变更的时候发生回调,通过applicationContext.publishEvent发布事件通过RefreshEventListener进行事件监听,实现配置更新

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

4.1、NacosFactory.createConfigService长轮训时间监听

客户端实时更新配置文件采用长轮询方式,定时任务是在构建ConfigService 对象实例的时候启动的
-通过反射Class.forName来加载NacosConfigService
-通过反射来完成NacosConfigService类的实例化

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

4.2、NacosConfigService对象的构造

NacosConfigService构造方法主要是以下实现

  • 初始化一个HttpAgent,
  • ClientWorker是客户端的一个工作类,agent作为参数传入ClientWorker
    public NacosConfigService(Properties properties) throws NacosException {
     
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
     
            encode = Constants.ENCODE;
        } else {
     
            encode = encodeTmp.trim();
        }
        initNamespace(properties);
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }

4.3、ClientWorker

ClientWorker构建了两个定时任务调度的线程池,并启动一个定时器

  • 第一个线程池executor只拥有一个核心线城池,10ms就会执行一次checkConfigInfo()方法,检测配置信息
  • executorService做了初始化,方便后面使用
  public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
     
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        // Initialize the timeout parameter

        init(properties);

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

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

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

4.4、ClientWorker.checkConfigInfo

ClientWorker构造方法中10s执行一次定时任务,主要方法为checkConfigInfo。检查配置发生了变化

   public void checkConfigInfo() {
     
        // 分任务
        int listenerSize = cacheMap.get().size();
        // 向上取整为批数
        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;
        }
    }

4.5、LongPollingRunnable.run

LongPollingRunnable实现了Runnable,run方法启用多线程

  • checkLocalConfig检查本地配置
  • checkUpdateDataIds方法和在服务端建立长轮训机制,从服务端获取发生变更的数据
  • 遍历changedGroupKeys,调用getServerConfig方法,将对应配置信息更新到本地。
   @Override
        public void run() {
     

            List<CacheData> cacheDatas = new ArrayList<CacheData>();
            List<String> inInitializingCacheList = new ArrayList<String>();
            try {
     
                // check failover config
                for (CacheData cacheData : cacheMap.get().values()) {
     
                    if (cacheData.getTaskId() == taskId) {
     
                        cacheDatas.add(cacheData);
                        try {
     
                            checkLocalConfig(cacheData);
                            if (cacheData.isUseLocalConfigInfo()) {
     
                                cacheData.checkListenerMd5();
                            }
                        } catch (Exception e) {
     
                            LOGGER.error("get local config info error", e);
                        }
                    }
                }

                // check server config
                List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                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 {
     
                        String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                        CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                        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);
                    }
                }
                for (CacheData cacheData : cacheDatas) {
     
                    if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
     
                        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);
            }
        }
    }

4.6、研究下长连接

在LongPollingRunnable.run中checkUpdateDataIds基于长连接监听服务端配置的变化,最后根据key去服务端中获取数据。checkUpdateDataIds主要调用了checkUpdateDataIds方法

    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
     


        List<String> params = new ArrayList<String>(2);
        params.add(Constants.PROBE_MODIFY_REQUEST);
        params.add(probeUpdateString);

        List<String> headers = new ArrayList<String>(2);
        headers.add("Long-Pulling-Timeout");
        headers.add("" + timeout);

        // told server do not hang me up if new initializing cacheData added in
        if (isInitializingCacheList) {
     
            headers.add("Long-Pulling-Timeout-No-Hangup");
            headers.add("true");
        }

        if (StringUtils.isBlank(probeUpdateString)) {
     
            return Collections.emptyList();
        }

        try {
     
            // In order to prevent the server from handling the delay of the client's long task,
            // increase the client's read timeout to avoid this problem.

            long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
            //http请求,但其将超时时间设置了30s
            HttpResult result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params,
                agent.getEncode(), readTimeoutMs);

            if (HttpURLConnection.HTTP_OK == result.code) {
     
                setHealthServer(true);
                return parseUpdateDataIdResponse(result.content);
            } else {
     
                setHealthServer(false);
                LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", agent.getName(), result.code);
            }
        } catch (IOException e) {
     
            setHealthServer(false);
            LOGGER.error("[" + agent.getName() + "] [check-update] get changed dataId exception", e);
            throw e;
        }
        return Collections.emptyList();
    }

上面代码的核心是发送http-post请求至服务端“/configs/listener”接口实现长轮训请求

4.7、服务端长轮训的实现

通过客户端调用服务端/configs/listener 找到方法入口,在ConfigController中listener方法

  • 获取客户端需要监听的可能发生的配置服务
  • inner.doPollingConfig开始执行长轮训请求
@PostMapping("/listener")
	@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
	public void listener(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
     
		request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
		String probeModify = request.getParameter("Listening-Configs");
		if (StringUtils.isBlank(probeModify)) {
     
			throw new IllegalArgumentException("invalid probeModify");
		}

		probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

		Map<String, String> clientMd5Map;
		try {
     
			clientMd5Map = MD5Util.getClientMd5Map(probeModify);
		}
		catch (Throwable e) {
     
			throw new IllegalArgumentException("invalid probeModify");
		}

		// do long-polling
		inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
	}

  • 获取客户端需要监听的可能发生变化的配置
  • inner.doPollingConfig执行长轮训请求
    doPollingConfig为核心长训轮处理接口
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
                                  Map<String, String> clientMd5Map, int probeRequestSize)
        throws IOException {
     

        // 长轮询
        if (LongPollingService.isSupportLongPolling(request)) {
     
            longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
            return HttpServletResponse.SC_OK + "";
        }

        // else 兼容短轮询逻辑
        List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

        // 兼容短轮询result
        String oldResult = MD5Util.compareMd5OldResult(changedGroups);
        String newResult = MD5Util.compareMd5ResultString(changedGroups);

        String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
        if (version == null) {
     
            version = "2.0.0";
        }
        int versionNum = Protocol.getVersionNumber(version);

        /**
         * 2.0.4版本以前, 返回值放入header中
         */
        if (versionNum < START_LONG_POLLING_VERSION_NUM) {
     
            response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
            response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
        } else {
     
            request.setAttribute("content", newResult);
        }

        Loggers.AUTH.info("new content:" + newResult);

        // 禁用缓存
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-cache,no-store");
        response.setStatus(HttpServletResponse.SC_OK);
        return HttpServletResponse.SC_OK + "";
    }

上面代码会先判断是否长轮训

  • 获取客户端的请求超时时间,减去500ms后赋值给timeout
  • 判断isFixedPolling,如果为true,定时任务将会在30s后执行,否则在29.5s
  • 和服务端的数据进行md5比对,发生变化直接返回
  • scheduler.execute执行ClientLongPolling线程
    addLongPollingClient主要功能为把客户端的长轮训请求封装成ClinetPolling交给scheduler执行

4.8、ClientLongPolling

ClientLongPolling是一个线程,run代码如下

  • 通过 scheduler.execute启动定时任务,时间为2.95s
  • 将ClientLongPolling实例本身添加到allSubs队列中,主要维护一个长轮训
  • -定时任务结束后,先ClientLongPolling实例本身从 allSubs队列中移除
  • 通过md5比较客户端的groupKeys是否发生了变化,将变更结果返回给客户端
  class ClientLongPolling implements Runnable {
     

        @Override
        public void run() {
     
            asyncTimeoutFuture = scheduler.schedule(new Runnable() {
     
                @Override
                public void run() {
     
                    try {
     
                        getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                        /**
                         * 删除订阅关系
                         */
                        allSubs.remove(ClientLongPolling.this);

                        if (isFixedPolling()) {
     
                            LogUtil.clientLog.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.clientLog.info("{}|{}|{}|{}|{}|{}",
                                (System.currentTimeMillis() - createTime),
                                "timeout", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
                                "polling",
                                clientMd5Map.size(), probeRequestSize);
                            sendResponse(null);
                        }
                    } catch (Throwable t) {
     
                        LogUtil.defaultLog.error("long polling error:" + t.getMessage(), t.getCause());
                    }

                }

            }, timeoutTime, TimeUnit.MILLISECONDS);

            allSubs.add(this);
        }

长轮训功能已经实现,接下来关注下如何实时触发
LongPollingService继承了AbstractEventListener

    @Override
    public void onEvent(Event event) {
     
        if (isFixedPolling()) {
     
            // ignore
        } else {
     
            if (event instanceof LocalDataChangeEvent) {
     
                LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
                scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
            }
        }
    }

其将监听了LocalDataChangeEvent事件。通过程序池执行了DataChangeTask任务

  • 遍历allSubs中的客户端长轮训请求
  • 比较客户端的长轮训请求的groupKey,如果服务变更的配置发生变化
@Override
        public void run() {
     
            try {
     
                ConfigCacheService.getContentBetaMd5(groupKey);
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
     
                    ClientLongPolling clientSub = iter.next();
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
     
                        // 如果beta发布且不在beta列表直接跳过
                        if (isBeta && !betaIps.contains(clientSub.ip)) {
     
                            continue;
                        }

                        // 如果tag发布且不在tag列表直接跳过
                        if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
     
                            continue;
                        }

                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        iter.remove(); // 删除订阅关系
                        LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
                            (System.currentTimeMillis() - changeTime),
                            "in-advance",
                            RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
                            "polling",
                            clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
            } catch (Throwable t) {
     
                LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
            }
        }

你可能感兴趣的:(java)