【Nacos】Nacos配置中心客户端配置更新源码分析

news2024/11/26 23:44:21

在这里插入图片描述

上文我们说了服务启动的时候从远程Nacos服务端拉取配置,这节我们来说下Nacos服务端配置的变动怎么实时通知到客户端,首先需要注册监听器。

注册监听器

NacosContextRefresher类会监听应用启动发布的ApplicationReadyEvent事件,然后进行配置监听器的注册。

com.alibaba.cloud.nacos.refresh.NacosContextRefresher#onApplicationEvent

public void onApplicationEvent(ApplicationReadyEvent event) {
	// many Spring context
	if (this.ready.compareAndSet(false, true)) {
		this.registerNacosListenersForApplications();
	}
}

registerNacosListenersForApplications()方法里会进行判断,如果自动刷新机制是开启的,则进行监听器注册。上文我们说到了会将拉到的配置缓存到NacosPropertySourceRepository中, 这儿就从缓存中获取所有的配置,然后循环进行监听器注册(如果配置文件中配置refresh字段为 false,则不注册监听器)。
com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListenersForApplications

private void registerNacosListenersForApplications() {
	if (isRefreshEnabled()) {
		for (NacosPropertySource propertySource : NacosPropertySourceRepository
			 .getAll()) {
			if (!propertySource.isRefreshable()) {
				continue;
			}
			String dataId = propertySource.getDataId();
			registerNacosListener(propertySource.getGroup(), dataId);
		}
	}
}

我们可以看到,监听器是以dataId+groupId+namespace为维度进行注册的,后续配置更新时会回调此监听器。

监听器的逻辑主要就三步:

  1. REFRESH_COUNT++,在之前的loadNacosPropertySource()方法有用到
  2. 往NacosRefreshHistory#records中添加一条刷新记录
  3. 发布一个RefreshEvent事件,该事件是SpringCloud提供的,主要就是用来做环境变更刷新用的

com.alibaba.cloud.nacos.refresh.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);
															// 发布RefreshEvent事件
															// 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);
	}
}

监听器的注册操作又委托到了ConfigService。
com.alibaba.nacos.client.config.NacosConfigService#addListener

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

监听器的注册在ClientWorker中处理,这块会创建一个CacheData对象,该对象主要就是用来管理监听器的,也是非常重要的一个类。
com.alibaba.nacos.client.config.impl.ClientWorker#addTenantListeners

public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
	throws NacosException {
	group = blank2defaultGroup(group);
	String tenant = agent.getTenant();
	CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
	for (Listener listener : listeners) {
		cache.addListener(listener);
	}
}

CacheData中中药字段如下:

// 可对配置进行拦截处理,可用于配置加密解密
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;

addCacheDataIfAbsent()方法中会将刚才创建的CacheData缓存到ClientWorker中的一个Map中,后续会用到。

com.alibaba.nacos.client.config.impl.ClientWorker#addCacheDataIfAbsent

public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant) throws NacosException {
	String key = GroupKey.getKeyTenant(dataId, group, tenant);
	CacheData cacheData = cacheMap.get(key);
	if (cacheData != null) {
		return cacheData;
	}

	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) {
			ConfigResponse response = getServerConfig(dataId, group, tenant, 3000L);
			cacheData.setContent(response.getContent());
		}
		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;
}

至此,在服务启动后向每一个需要支持热更新的配置都注册了一个监听器,用来监听远程配置的变动,以及做相应的处理。

获取更新的配置

ClientWorker是在ConfigService的构造方法中创建的。

ClientWorker的构造函数里会去创建两个线程池,executor会每隔10ms进行一次配置变更的检查,executorService主要是用来处理长轮询请求的。

com.alibaba.nacos.client.config.impl.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);

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

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

checkConfigInfo()负责提交长轮询任务。
com.alibaba.nacos.client.config.impl.ClientWorker#checkConfigInfo

public void checkConfigInfo() {
	// Dispatch tasks.
	int listenerSize = cacheMap.size();
	// Round up the longingTaskCount.
	int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
	if (longingTaskCount > currentLongingTaskCount) {
		for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
			// The task list is no order.So it maybe has issues when changing.
			executorService.execute(new LongPollingRunnable(i));
		}
		currentLongingTaskCount = longingTaskCount;
	}
}

长轮询任务的执行过程。
com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable#run

public void run() {
	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) {
				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);
		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从服务端查询最新的配置
				ConfigResponse response = getServerConfig(dataId, group, tenant, 3000L);
				CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
				cache.setContent(response.getContent());
				cache.setEncryptedDataKey(response.getEncryptedDataKey());
				if (null != response.getConfigType()) {
					cache.setType(response.getConfigType());
				}
				LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
							agent.getName(), dataId, group, tenant, cache.getMd5(),
							ContentUtils.truncateContent(response.getContent()), response.getConfigType());
			} 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))) {
				// 校验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);
	}
}

checkUpdateDataIds()该方法中,会将所有的dataId按定义格式拼接出一个字符串,构造一个长轮询请求,发给服务端,Long-Pulling-Timeout 超时时间默认30s,如果服务端没有配置变更,则会保持该请求直到超时,有配置变更则直接返回有变更的dataId列表。
com.alibaba.nacos.client.config.impl.ClientWorker#checkUpdateDataIds

List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
	StringBuilder sb = new StringBuilder();
	for (CacheData cacheData : cacheDatas) {
		if (!cacheData.isUseLocalConfigInfo()) {
			sb.append(cacheData.dataId).append(WORD_SEPARATOR);
			sb.append(cacheData.group).append(WORD_SEPARATOR);
			if (StringUtils.isBlank(cacheData.tenant)) {
				sb.append(cacheData.getMd5()).append(LINE_SEPARATOR);
			} else {
				sb.append(cacheData.getMd5()).append(WORD_SEPARATOR);
				sb.append(cacheData.getTenant()).append(LINE_SEPARATOR);
			}
			if (cacheData.isInitializing()) {
				// It updates when cacheData occurs in cacheMap by first time.
				// 添加要初始化的cacheData
				inInitializingCacheList
					.add(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant));
			}
		}
	}
	boolean isInitializingCacheList = !inInitializingCacheList.isEmpty();
	// 检验服务器端的配置
	return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
}

checkUpdateConfigStr()会发起HTTP接口/v1/cs/configs/listener的调用。
com.alibaba.nacos.client.config.impl.ClientWorker#checkUpdateConfigStr

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

	Map<String, String> params = new HashMap<String, String>(2);
	params.put(Constants.PROBE_MODIFY_REQUEST, probeUpdateString);
	Map<String, String> headers = new HashMap<String, String>(2);
	// 使用长轮询
	headers.put("Long-Pulling-Timeout", "" + timeout);

	// told server do not hang me up if new initializing cacheData added in
	if (isInitializingCacheList) {
		headers.put("Long-Pulling-Timeout-No-Hangup", "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);
		// 调用远程的监听接口
		HttpRestResult<String> result = agent
			.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(),
					  readTimeoutMs);

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

checkListenerMd5()主要就是判断两个md5是不是相同,不同则调用safeNotifyListener()处理。
com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5

void checkListenerMd5() {
	for (ManagerListenerWrap wrap : listeners) {
		if (!md5.equals(wrap.lastCallMd5)) {
			// 配置有变化通知监听器
			safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
		}
	}
}

safeNotifyListener()方法主要就是调用监听器的receiveConfigInfo()方法,然后更新监听器包装器中的lastContent、lastCallMd5字段。
com.alibaba.nacos.client.config.impl.CacheData#safeNotifyListener

private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
								final String md5, final String encryptedDataKey, 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);
				cr.setEncryptedDataKey(encryptedDataKey);
				configFilterChainManager.doFilter(null, cr);
				String contentTmp = cr.getContent();
				/**
                     * @see com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener(java.lang.String, java.lang.String)
                     */
				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);
}

监听器要执行的方法我们上面也已经讲过了,主要就是发布RefreshEvent事件。至此,Nacos的处理流程已经结束了,RefreshEvent事件主要由 SpringCloud相关类来处理。

RefreshEvent事件处理

RefreshEvent事件会由RefreshEventListener来处理。

org.springframework.cloud.endpoint.event.RefreshEventListener#onApplicationEvent

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

委托给ContextRefresher来刷新容器中的配置。
org.springframework.cloud.endpoint.event.RefreshEventListener#handle(org.springframework.cloud.endpoint.event.RefreshEvent)

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

org.springframework.cloud.context.refresh.ContextRefresher#refresh

public synchronized Set<String> refresh() {
	Set<String> keys = refreshEnvironment();
	this.scope.refreshAll();
	return keys;
}

refreshEnvironment()会去刷新Spring环境变量,实际上是交给addConfigFilesToEnvironment()方法去做的刷新,具体刷新思想就是重新创建一个新的Spring容器,然后将这个新容器中的环境信息设置到原有的Spring环境中。拿到所有变化的配置项后,发布一个环境变化的 EnvironmentChangeEvent事件。

org.springframework.cloud.context.refresh.ContextRefresher#refreshEnvironment

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

/* For testing. */ ConfigurableApplicationContext addConfigFilesToEnvironment() {
	ConfigurableApplicationContext capture = null;
	try {
		StandardEnvironment environment = copyEnvironment(
			this.context.getEnvironment());
		SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
			.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
			.environment(environment);
		// Just the listeners that affect the environment (e.g. excluding logging
		// listener because it has side effects)
		builder.application()
			.setListeners(Arrays.asList(new BootstrapApplicationListener(),
										new ConfigFileApplicationListener()));
		capture = builder.run();
		if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
			environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
		}
		MutablePropertySources target = this.context.getEnvironment()
			.getPropertySources();
		String targetName = null;
		for (PropertySource<?> source : environment.getPropertySources()) {
			String name = source.getName();
			if (target.contains(name)) {
				targetName = name;
			}
			if (!this.standardSources.contains(name)) {
				if (target.contains(name)) {
					target.replace(name, source);
				}
				else {
					if (targetName != null) {
						target.addAfter(targetName, source);
						// update targetName to preserve ordering
						targetName = name;
					}
					else {
						// targetName was null so we are at the start of the list
						target.addFirst(source);
						targetName = name;
					}
				}
			}
		}
	}
	finally {
		ConfigurableApplicationContext closeable = capture;
		while (closeable != null) {
			try {
				closeable.close();
			}
			catch (Exception e) {
				// Ignore;
			}
			if (closeable.getParent() instanceof ConfigurableApplicationContext) {
				closeable = (ConfigurableApplicationContext) closeable.getParent();
			}
			else {
				break;
			}
		}
	}
	return capture;
}

org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll

public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

@Value注解的属性要实现热更新就需要配合@RefreshScope注解,被@RefreshScope注解的对象的作用域为RefreshScope,这种对象不是存在Spring容器的一级缓存中,而是存在GenericScope对象的cache属性中,当配置变更时会清空缓存在cache属性的对象,这样Bean下次使用时就会被重新创建,从而从Environment中获取最新的配置。
org.springframework.cloud.context.scope.GenericScope#destroy()

public void destroy() {
	List<Throwable> errors = new ArrayList<Throwable>();
	// 清空缓存
	Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
	for (BeanLifecycleWrapper wrapper : wrappers) {
		try {
			Lock lock = this.locks.get(wrapper.getName()).writeLock();
			lock.lock();
			try {
				wrapper.destroy();
			}
			finally {
				lock.unlock();
			}
		}
		catch (RuntimeException e) {
			errors.add(e);
		}
	}
	if (!errors.isEmpty()) {
		throw wrapIfNecessary(errors.get(0));
	}
	this.errors.clear();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/336189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

现在00后也这么卷?部门刚来的00后软件测试工程师已经要把我卷崩溃了...

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;刚开年我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪18K&#xff0c;都快接近我了。后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 最…

sqlmap对http请求头扫描,爬取数据库数据

做安全测试时&#xff0c;先用appscan扫描目标网站&#xff0c;爆出sql盲注的风险 然后使用sqlmap专业工具来扫描分析漏洞 GitHub - sqlmapproject/sqlmap: Automatic SQL injection and database takeover toolAutomatic SQL injection and database takeover tool - GitHub …

Mysql 部署 MGR 集群

0. 参考文章 官方文档&#xff1a; MySQL :: MySQL 8.0 Reference Manual :: 18.2 Getting Started 博客&#xff1a; MGR 单主模式部署教程&#xff08;基于 MySQL 8.0.28&#xff09; - 墨天轮 (modb.pro) mysql MGR单主模式的搭建 - 墨天轮 (modb.pro) MySQL 5.7 基于…

Vue2之完整基础介绍和指令与过滤器

Vue2之基础介绍和指令与过滤器一、简介1、概念2、vue的两个特性2.1 数据驱动视图2.2 双向数据绑定3、MVVM二、vue基础用法1、导入vue.js的script脚本文件2、在页面中声明一个将要被vue所控制的DOM区域3、创建vm实例对象&#xff08;vue实例对象&#xff09;4、样例完整代码三、…

Oracle Dataguard(主库为 Oracle rac 集群)配置教程(02)—— Oracle RAC 主库的相关操作

Oracle Dataguard&#xff08;主库为 Oracle rac 集群&#xff09;配置教程&#xff08;02&#xff09;—— Oracle RAC 主库的相关操作 / 本专栏详细讲解 Oracle Dataguard&#xff08;Oracle 版本为11g&#xff0c;主库为双节点 Oracle rac 集群&#xff09;的配置过程。主要…

数据库 与 数据仓库的本质区别是什么?

当用计算机来处理数据的时候, 数据就需要存储和管理了。早期的数据, 就是用一个文件来实现的, 即是文件系统。随着处理的数据量增大, 发展到用数据库来管理和存储数据了。 数据库包括多媒体数据库、对象关系数据库和关系数据库。关系数据库管理系统,已经成为了事实上通用的数据…

FANUC机器人UI[4]_CSTOPI循环停止信号使用时的注意事项

FANUC机器人UI[4]_CSTOPI循环停止信号使用时的注意事项 前面已经和大家介绍了关于FANUC机器人的UOP信号的具体功能,具体可参考以下链接中的内容: FANUC机器人UOP信号(UI+UO)功能详细介绍 本次关于FANUC机器人的UI[4] CSTOPI循环停止信号使用时的注意事项进行一个补充说明。…

C语言-程序环境和预处理(14.2)

目录 预处理详解 1.预定义符号 2. #define 2.1 #define定义标识符 2.2 #define 定义宏 2.3 #define 替换规则 注意事项&#xff1a; 2.4 #和## 2.5 带副作用的宏参数 2.6 宏和函数对比 3. #undef 4. 条件编译 4.1 单分支条件编译 4.2 多分支条件编译 4.3 判断是…

基础篇:01-微服务概述

1.单体应用与微服务架构区别 如上图左侧为单体应用架构。在传统单体应用中&#xff0c;所有功能模块都在一个工程中编码、部署&#xff0c;即使是集群部署&#xff0c;也只是单体应用的水平复制。 如上图右侧为微服务架构。在微服务架构的项目中&#xff0c;每个应用会按照领域…

浅谈保护数据的加密策略

加密是一种将信息从可读格式转换为混乱字符串的技术。这样做可以防止数据传输中的机密数据泄露。文档、文件、消息和所有其他形式的网络通信都可以加密。加密策略和身份验证服务的结合&#xff0c;还能保障企业机密信息只对授权用户开启访问权限。常见的数据加密包括以下两种&a…

定期备份日志并发送至存储服务器指定路径脚本

根据需求编写一个日志定时备份并发送至存储服务器的脚本定期把三天前的日志文件备份&#xff0c;打包发送至日志备份服务器指定目录&#xff08;修改对应路径拿走即用&#xff09;vim qingli.sh#!/bin/bash#定义星期几week$(date |awk NR1{print $4})num${week}#日志源目录log&…

Android MVI框架搭建与使用

MVI框架搭建与使用前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle二、网络请求① 生成数据类② 接口类③ 网络请求工具类三、意图与状态① 创建意图② 创建状态四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂五、UI① 列表适配器②…

【3D目标检测】基于伪雷达点云的单目3D目标检测方法研宄

目录概述细节基准模型点云置信度生成网络特征聚合 DGCNN概述 本文是基于单目图像的3D目标检测方法&#xff0c;是西安电子科技大学的郭鑫宇学长的硕士学位论文。 【2021】【单目图像的3D目标检测方法】 细节 基准模型 作者还是按照伪雷达点云算法的流程设计的&#xff0c;并…

多传感器融合定位十四-基于图优化的定位方法

多传感器融合定位十四-基于图优化的定位方法1. 基于图优化的定位简介1.1 核心思路1.2 定位流程2. 边缘化原理及应用2.1 边缘化原理2.2 从滤波角度理解边缘化3. 基于kitti的实现原理3.1 基于地图定位的滑动窗口模型3.2 边缘化过程4. lio-mapping 介绍4.1 核心思想4.2 具体流程4.…

lamada表达式、stream、collect整理

lamada表达式格式 格式&#xff1a;( parameter-list ) -> { expression-or-statements } 实例&#xff1a;简化匿名内部类的写法 原本写法&#xff1a; public class LamadaTest { public static void main(String[] args) { new Thread(new Runnable() { …

基于PYTHON django四川旅游景点推荐系统

摘 要基于四川旅游景点推荐系统的设计与实现是一个专为四川旅游景点为用户打造的旅游网站。该课题基于网站比较流行的Python 语言系统架构,B/S三层结构模式&#xff0c;通过Maven项目管理工具进行Jar包版本的控制。本系统用户可以发布个人游记&#xff0c;查看景点使用户达到良…

树莓派安装虚拟键盘matchbox-keyboard,解决虚拟键盘乱码问题,解决MIPI DSI触摸屏触控漂移问题

安装虚拟键盘&#xff0c;解决乱码问题 当我们买了触摸屏后&#xff0c;会发现没有键盘&#xff0c;还是无法输入&#xff0c;因此需要虚拟键盘 如果你的语言和地区是中文&#xff0c;那么安装虚拟键盘后可能显示乱码&#xff0c;所以还需要安装中文字体 sudo apt install ttf…

音视频开发—FFMpeg编码解码

FFMpeg 作为音视频领域的开源工具&#xff0c;它几乎可以实现所有针对音视频的处理&#xff0c;本文主要利用 FFMpeg 官方提供的 SDK 实现音视频最简单的几个实例&#xff1a;编码、解码、封装、解封装、转码、缩放以及添加水印。 接下来会由发现问题&#xff0d;&#xff1e;分…

Elasticsearch5.5.1 自定义评分插件开发

文本相似度插件开发&#xff0c;本文基于Elasticsearch5.5.1&#xff0c;Kibana5.5.1 下载地址为&#xff1a; Past Releases of Elastic Stack Software | Elastic 本地启动两个服务后&#xff0c;localhost:5601打开Kibana界面&#xff0c;点击devTools&#xff0c;效果图…

koa ts kick off 搭建项目的基本架子

koa ts kick off 使用ts开发koa项目的基本架子&#xff0c;便于平时随手调研一些技术 项目结构 ├── src │ ├── controller //controller层 │ ├── service //service层 │ ├── routes.ts //路由 │ └── index.ts //项目入…