Bootstrap

Nacos配置中心原理精华

1.nacos作为配置中心介绍

1.nacos不仅可以用作服务发现,同时也是微服务框架中配置管理中心。每个服务都有一堆属于自己的配置,例如:数据库、redis、分布式中间件,这些配置其实相同的内容只需要维护一份就行了,而且如果其中的某些属性发生了变化,每个微服务都需要进行修改,所以配置中心集中管理微服务配置尤为重要。

2.配置中心使用

1.引入nacos配置中心依赖

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

2.创建bootstrap.properties文件

# 配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

springcloud默认加载名称为bootstrap.properties的配置文件,我们将配置中心的地址配置后,再指定需要加载的配置文件,服务启动时就会从nacos服务端拉取对应配置信息。

3.配置动态刷新

    @Value("${common.age}")
    private String age;
    @Value("${common.name}")
    private String name;

1.可以使用@Value注解来引用配置,但是如果修改了配置,这个bean中的配置将不会修改,如果想让bean中的属性同步修改需要再添加一个属性 @RefreshScope。
问题及解决办法: @RefreshScope引入这个注解,则标注了这个bean的作用范围就是@Scope(“refresh”),它不同于单例Bean存在与单例池也不同于原型bean,每次使用时都新生成。这个范围的bean在创建后会存在于BeanLifecycleWrapperCache这个缓存。当配置发生修改后会调用RefreshScope#refreshAll() 进行缓存清理,新生成bean中就有了新属性值,如果在这个bean中使用@Scheduled定义了定时任务那么这个任务就会失效,需要主动调用一下这个bean才会触发定时任务。推荐一下用法

@RestController
@RefreshScope  //动态感知修改后的值
public class TestController implements ApplicationListener<RefreshScopeRefreshedEvent>{
    @Value("${common.age}")
     String age;
    @Value("${common.name}")
     String name;
    @GetMapping("/common")
    public String hello() {
        return name+","+age;
    }
    //触发@RefreshScope执行逻辑会导致@Scheduled定时任务失效
    @Scheduled(cron = "*/3 * * * * ?")  //定时任务每隔3s执行一次
    public void execute() {
        System.out.println("定时任务正常执行。。。。。。");
    }
    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        this.execute();
    }
}

通过监听事件的方式可以使定时任务不失效,因为在清除缓存bean时就会发布一个对应的监听事件,此时调用这个方法又会重新生成bean.

4.配置文件加载顺序

NacosPropertySourceLocator#locate

	public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
		ConfigService configService = nacosConfigManager.getConfigService();

		if (null == configService) {
			log.warn("no instance of config service found, can't load config from nacos");
			return null;
		}
		long timeout = nacosConfigProperties.getTimeout();
		nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
				timeout);
		String name = nacosConfigProperties.getName();

		String dataIdPrefix = nacosConfigProperties.getPrefix();
		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = name;
		}

		if (StringUtils.isEmpty(dataIdPrefix)) {
			dataIdPrefix = env.getProperty("spring.application.name");
		}

		CompositePropertySource composite = new CompositePropertySource(
				NACOS_PROPERTY_SOURCE_NAME);

		loadSharedConfiguration(composite);
		loadExtConfiguration(composite);
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

NacosPropertySourceLocator#loadApplicationConfiguration

	private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
		String fileExtension = properties.getFileExtension();
		String nacosGroup = properties.getGroup();
		// load directly once by default
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		// load with suffix, which have a higher priority than the default
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		// Loaded with profile, which have a higher priority than the suffix
		for (String profile : environment.getActiveProfiles()) {
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		}

	}

结论:

Spring Cloud Alibaba Nacos Config 目前提供了三种配置能力从 Nacos 拉取相关的配置。
A: 通过 spring.cloud.nacos.config.shared-configs 支持多个共享 Data Id 的配置
B: 通过 spring.cloud.nacos.config.ext-config[n].data-id 的方式支持多个扩展 Data Id 的配置
C: 通过内部相关规则(应用名、应用名+ Profile )自动生成相关的 Data Id 配置
当三种方式共同使用时,他们的一个优先级关系是:A < B < C
优先级从高到低:
${spring.application.name}-${profile}.${file-extension:properties}
${spring.application.name}.${file-extension:properties}
${spring.application.name}
extensionConfigs    一个微服务的多个配置,比如 nacos,mybatis
sharedConfigs       多个微服务公共配置,比如 redis

5.客户端拉取配置和服务端推送配置流程

在这里插入图片描述
客户端有定时轮询拉取机制,服务端有主动推动机制。实现了配置的变化的快速更新。
核心代码

5.1 客户端拉取配置

  • 1.客户端ClientWork实例化时构造方法中会启动定时任务checkConfigInfo()
    ClientWorker#ClientWorker
    public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
            final Properties properties) throws NacosException {
        this.configFilterChainManager = configFilterChainManager;
        
        init(properties);
        
        agent = new ConfigRpcTransportClient(properties, serverListManager);
        int count = ThreadUtils.getSuitableThreadCount(THREAD_MULTIPLE);
        ScheduledExecutorService executorService = Executors
                .newScheduledThreadPool(Math.max(count, MIN_THREAD_NUM), r -> {
                    Thread t = new Thread(r);
                    t.setName("com.alibaba.nacos.client.Worker");
                    t.setDaemon(true);
                    return t;
                });
        agent.setExecutor(executorService);
        agent.start();
        
    }
  • 2.agent.start()实际调用的是父类中的start方法
    ConfigTransportClient#start
    public void start() throws NacosException {
        securityProxy.login(this.properties);
        this.executor.scheduleWithFixedDelay(() -> securityProxy.login(properties), 0,
                this.securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
        startInternal();
    }
  • 3.利用阻塞队列+while循环实现轮询
        public void startInternal() {
            executor.schedule(() -> {
                while (!executor.isShutdown() && !executor.isTerminated()) {
                    try {
                        listenExecutebell.poll(5L, TimeUnit.SECONDS);
                        if (executor.isShutdown() || executor.isTerminated()) {
                            continue;
                        }
                        executeConfigListen();
                    } catch (Exception e) {
                        LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e);
                    }
                }
            }, 0L, TimeUnit.MILLISECONDS);
            
        }

5.2 服务端推送配置

  • 1.服务端发布配置 关键代码
    ConfigController#publishConfig
// 发布配置持久化
 persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, false);
// 发布配置变化事件 最终会通知客户端配置发生变化
ConfigChangePublisher.notifyConfigChange(
                    new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
  • 2.服务端通知客户端配置变化 关键代码
    RpcConfigChangeNotifier#configDataChanged
// 构造请求参数发送客户端  通知客户端具体配置文件(dataId)发生变化
ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant);
            
RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp, metaInfo.getAppName());
push(rpcPushRetryTask);
  • 3.客户端处理器处理服务端的通知
    .ConfigRpcTransportClient#initRpcClientHandler
/*
 * Register Config Change /Config ReSync Handler
 */
rpcClientInner.registerServerRequestHandler((request) -> {
    if (request instanceof ConfigChangeNotifyRequest) {
        ConfigChangeNotifyRequest configChangeNotifyRequest = (ConfigChangeNotifyRequest) request;
        LOGGER.info("[{}] [server-push] config changed. dataId={}, group={},tenant={}",
                rpcClientInner.getName(), configChangeNotifyRequest.getDataId(),
                configChangeNotifyRequest.getGroup(), configChangeNotifyRequest.getTenant());
        String groupKey = GroupKey
                .getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(),
                        configChangeNotifyRequest.getTenant());
        
        CacheData cacheData = cacheMap.get().get(groupKey);
        if (cacheData != null) {
            synchronized (cacheData) {
                cacheData.getLastModifiedTs().set(System.currentTimeMillis());
                cacheData.setSyncWithServer(false);
                notifyListenConfig();
            }
            
        }
        return new ConfigChangeNotifyResponse();
    }
    return null;
});
  • 4.客户端处理服务端通知,添加阻塞队列 和前面的客户端ClientWork使用的同一个阻塞队列
 public void notifyListenConfig() {
     listenExecutebell.offer(bellItem);
 }
  • 5.客户端处理事件监听
    ConfigRpcTransportClient#executeConfigListen
// 构造请求参数 参数中包含 list<dataId> 
ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(listenCaches);
// 向nacos-server发起调用 服务端返回具体哪些配置发生变化
(ConfigChangeBatchListenResponse) requestProxy(
                                rpcClient, configChangeListenRequest);
// 检查并且刷新客户端配置
refreshContentAndCheck(changeKey, !isInitializing);
  • 6.客户端拿到变化的配置后
    ClientWorker#refreshContentAndCheck
// 从nacos获取具体的配置变化信息
getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L,notify);
// 刷新缓存内容及MD5
cacheData.setContent(response.getContent());
// 比较MD5
cacheData.checkListenerMd5();
  • 7.回调客户端的监听器
    CacheData#safeNotifyListener
listener.receiveConfigInfo(contentTmp);

# NacosContextRefresher#registerNacosListener
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));
						}
					}
				});
		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);
		}
	}

6、事件监听机制和发布订阅机制区别

事件监听机制有以下角色:(用spring中的举例)
1.ApplicationEventPublisher 事件发布者(真正发布事件借助事件多播器)
2.ApplicationEventMulticaster 事件多播器(事件监听器注册在事件多播器上)
3.ApplicationListener 事件监听器
4.ApplicationEvent 事件
事件监听机制:事件多播器来发布事件,根据事件类型找到对应事件的监听器,调用监听器的onApplicationEvent方法。

发布订阅角色: (用nacos中的举例)
1.NotifyCenter 事件通知中心(借助事件发布者发布事件)
2.Event 事件
3.EventPublisher 事件发布者(事件订阅者注册在发布者上面)
4.Subscriber 事件订阅者
发布订阅机制:事件发布者发布事件将事件放入阻塞队列,事件发布者根据事件类型找到对应事件的订阅者,从阻塞队列获取事件调用订阅者的onEvent方法

;