Nacos源码—5.Nacos配置中心实现分析二
大纲
1.关于Nacos配置中心的几个问题
2.Nacos如何整合SpringBoot读取远程配置
3.Nacos加载读取远程配置数据的源码分析
4.客户端如何感知远程配置数据的变更
5.集群架构下节点间如何同步配置数据
4.客户端如何感知远程配置数据的变更
(1)ConfigService对象使用介绍
(2)客户端注册监听器的源码
(3)回调监听器的方法的源码
(1)ConfigService对象使用介绍
ConfigService是一个接口,定义了获取配置、发布配置、移除配置等方法。ConfigService只有一个实现类NacosConfigService,Nacos配置中心源码的核心其实就是这个NacosConfigService对象。
步骤一:手动创建ConfigService对象
首先定义好基本的Nacos信息,然后利用NacosFactory工厂类来创建ConfigService对象。
public class Demo {public static void main(String[] args) throws Exception {//步骤一:配置信息String serverAddr = "124.223.102.236:8848";String dataId = "stock-service-test.yaml";String group = "DEFAULT_GROUP";Properties properties = new Properties();properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);//步骤一:获取配置中心服务ConfigService configService = NacosFactory.createConfigService(properties);}
}
步骤二:获取配置、发布配置
创建好ConfigService对象后,就可以使用ConfigService对象的getConfig()方法来获取配置信息,还可以使用ConfigService对象的publishConfig()方法来发布配置信息。
如下Demo先获取一次配置数据,然后发布新配置,紧接着重新获取数据。发现第二次获取的配置数据已发生变化,从而也说明发布配置成功了。
public class Demo {public static void main(String[] args) throws Exception {//步骤一:配置信息String serverAddr = "124.223.102.236:8848";String dataId = "stock-service-test.yaml";String group = "DEFAULT_GROUP";Properties properties = new Properties();properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);//步骤一:获取配置中心服务ConfigService configService = NacosFactory.createConfigService(properties);//步骤二:从配置中心获取配置String content = configService.getConfig(dataId, group, 5000);System.out.println("发布配置前" + content);//步骤二:发布配置configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());Thread.sleep(300L);//步骤二:从配置中心获取配置content = configService.getConfig(dataId, group, 5000);System.out.println("发布配置后" + content);}
}
步骤三:添加监听器
可以使用ConfigService对象的addListener()方法来添加监听器。通过dataId + group这两个参数,就可以注册一个监听器。当dataId + group对应的配置在服务端发生改变时,客户端的监听器就可以马上感知并对配置数据进行刷新。
public class Demo {public static void main(String[] args) throws Exception {//步骤一:配置信息String serverAddr = "124.223.102.236:8848";String dataId = "stock-service-test.yaml";String group = "DEFAULT_GROUP";Properties properties = new Properties();properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);//步骤一:获取配置中心服务ConfigService configService = NacosFactory.createConfigService(properties);//步骤二:从配置中心获取配置String content = configService.getConfig(dataId, group, 5000);System.out.println("发布配置前" + content);//步骤二:发布配置configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());Thread.sleep(300L);//步骤二:从配置中心获取配置content = configService.getConfig(dataId, group, 5000);System.out.println("发布配置后" + content);//步骤三:注册监听器configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("感知配置变化:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}});//阻断进程关闭Thread.sleep(Integer.MAX_VALUE);}
}
(2)客户端注册监听器的源码
Nacos客户端是什么时候为dataId + group注册监听器的?
在nacos-config下的spring.factories文件中,有一个自动装配的配置类NacosConfigAutoConfiguration,在该配置类中定义了一个NacosContextRefresher对象,而NacosContextRefresher对象会监听ApplicationReadyEvent事件。
在NacosContextRefresher的onApplicationEvent()方法中,会执行registerNacosListenersForApplications()方法,这个方法中会遍历每一个dataId + group注册Nacos监听器。
对于每一个dataId + group,则通过调用registerNacosListener()方法来进行Nacos监听器的注册,也就是最终调用ConfigService对象的addListener()方法来注册监听器。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {...@Beanpublic NacosContextRefresher nacosContextRefresher(NacosConfigManager nacosConfigManager, NacosRefreshHistory nacosRefreshHistory) {return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);}...
}public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {private final ConfigService configService;...@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {//many Spring contextif (this.ready.compareAndSet(false, true)) {this.registerNacosListenersForApplications();}}//register Nacos Listeners.private void registerNacosListenersForApplications() {if (isRefreshEnabled()) {//获取全部的配置for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {//判断当前配置是否需要刷新if (!propertySource.isRefreshable()) {continue;}String dataId = propertySource.getDataId();//注册监听器registerNacosListener(propertySource.getGroup(), dataId);}}}private void registerNacosListener(final String groupKey, final String dataKey) {String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() {@Overridepublic void innerReceive(String dataId, String group, String configInfo) {//监听器的回调方法处理逻辑refreshCountIncrement();//记录刷新历史nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);//发布RefreshEvent刷新事件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);}}...
}
(3)回调监听器的方法的源码
给每一个dataId + group注册Nacos监听器后,当Nacos服务端的配置文件发生变更时,就会回调监听器的方法,也就是会触发调用AbstractSharedListener的innerReceive()方法。然后调用applicationContext.publishEvent()发布RefreshEvent刷新事件,而发布的RefreshEvent刷新事件会被RefreshEventListener类来处理。
RefreshEventListener类不是Nacos中的类了,而是SpringCloud的类。它在处理刷新事件时,会销毁被@RefreshScope注解修饰的类的Bean,也就是会调用添加了@RefreshScope注解的类的destroy()方法。把Bean实例销毁后,后面需要用到这个Bean时才重新进行创建。重新进行创建的时候,就会获取最新的配置文件,从而完成刷新效果。
(4)总结
客户端注册Nacos监听器,服务端修改配置后,客户端刷新配置的流程:
5.集群架构下节点间如何同步配置数据
(1)Nacos控制台的配置管理模块
(2)变更配置数据时的源码
(3)集群节点间的配置数据变更同步
(4)服务端通知客户端配置数据已变更
(5)总结
(1)Nacos控制台的配置管理模块
在这个模块中,可以通过配置列表维护我们的配置文件,可以通过历史版本找到配置的发布记录,并且支持回滚操作。当编辑配置文件时,客户端可以及时感知变化并刷新其配置文件。当服务端通知客户端配置变更时,也会通知集群节点进行数据同步。
当用户在Nacos控制台点击确认发布按钮时,Nacos会大概进行如下处理:
一.修改配置文件数据
二.保存配置发布历史
三.通知并触发客户端监听事件进行配置文件变更
四.通知集群对配置文件进行变更
点击确认发布按钮时,会发起HTTP请求,地址为"/nacos/v1/cs/configs"。通过请求地址可知处理入口是ConfigController的publishConfig()方法。
(2)变更配置数据时的源码
ConfigController的publishConfig()方法中的两行核心代码是:一.新增或修改配置数据的PersistService的insertOrUpdate()方法,二.发布配置变更事件的ConfigChangePublisher的notifyConfigChange()方法。
一.新增或者修改配置数据
其中PersistService有两个实现类:一是EmbeddedStoragePersistServiceImpl,它是Nacos内置的Derby数据库。二是ExternalStoragePersistServiceImpl,它是Nacos外置数据库如MySQL。
在ExternalStoragePersistServiceImpl的insertOrUpdate()方法中,如果执行ExternalStoragePersistServiceImpl的updateConfigInfo()方法,那么会先查询对应的配置,然后更新配置,最后保存配置历史。
@RestController
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {private final PersistService persistService;...@PostMapping@Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,@RequestParam(value = "dataId") String dataId, @RequestParam(value = "group") String group,@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,@RequestParam(value = "content") String content, @RequestParam(value = "tag", required = false) String tag,@RequestParam(value = "appName", required = false) String appName,@RequestParam(value = "src_user", required = false) String srcUser,@RequestParam(value = "config_tags", required = false) String configTags,@RequestParam(value = "desc", required = false) String desc,@RequestParam(value = "use", required = false) String use,@RequestParam(value = "effect", required = false) String effect,@RequestParam(value = "type", required = false) String type,@RequestParam(value = "schema", required = false) String schema) throws NacosException {final String srcIp = RequestUtil.getRemoteIp(request);final String requestIpApp = RequestUtil.getAppName(request);srcUser = RequestUtil.getSrcUserName(request);//check typeif (!ConfigType.isValidType(type)) {type = ConfigType.getDefaultType().getType();}//check tenantParamUtils.checkTenant(tenant);ParamUtils.checkParam(dataId, group, "datumId", content);ParamUtils.checkParam(tag);Map<String, Object> configAdvanceInfo = new HashMap<String, Object>(10);MapUtils.putIfValNoNull(configAdvanceInfo, "config_tags", configTags);MapUtils.putIfValNoNull(configAdvanceInfo, "desc", desc);MapUtils.putIfValNoNull(configAdvanceInfo, "use", use);MapUtils.putIfValNoNull(configAdvanceInfo, "effect", effect);MapUtils.putIfValNoNull(configAdvanceInfo, "type", type);MapUtils.putIfValNoNull(configAdvanceInfo, "schema", schema);ParamUtils.checkParam(configAdvanceInfo);if (AggrWhitelist.isAggrDataId(dataId)) {LOGGER.warn("[aggr-conflict] {} attemp to publish single data, {}, {}", RequestUtil.getRemoteIp(request), dataId, group);throw new NacosException(NacosException.NO_RIGHT, "dataId:" + dataId + " is aggr");}final Timestamp time = TimeUtils.getCurrentTime();String betaIps = request.getHeader("betaIps");ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);configInfo.setType(type);if (StringUtils.isBlank(betaIps)) {if (StringUtils.isBlank(tag)) {//新增配置或者修改配置persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, true);//发布配置改变事件ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));} else {persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, true);//发布配置改变事件ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));}} else {//beta publishpersistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, true);//发布配置改变事件ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));}ConfigTraceService.logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), InetUtils.getSelfIP(), ConfigTraceService.PERSISTENCE_EVENT_PUB, content);return true;}...
}//External Storage Persist Service.
@SuppressWarnings(value = {"PMD.MethodReturnWrapperTypeRule", "checkstyle:linelength"})
@Conditional(value = ConditionOnExternalStorage.class)
@Component
public class ExternalStoragePersistServiceImpl implements PersistService {private DataSourceService dataSourceService;...@Overridepublic void insertOrUpdate(String srcIp, String srcUser, ConfigInfo configInfo, Timestamp time, Map<String, Object> configAdvanceInfo, boolean notify) {try {addConfigInfo(srcIp, srcUser, configInfo, time, configAdvanceInfo, notify);} catch (DataIntegrityViolationException ive) { // Unique constraint conflictupdateConfigInfo(configInfo, srcIp, srcUser, time, configAdvanceInfo, notify);}}@Overridepublic void updateConfigInfo(final ConfigInfo configInfo, final String srcIp, final String srcUser, final Timestamp time, final Map<String, Object> configAdvanceInfo, final boolean notify) {boolean result = tjt.execute(status -> {try {//查询已存在的配置数据ConfigInfo oldConfigInfo = findConfigInfo(configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());String appNameTmp = oldConfigInfo.getAppName();if (configInfo.getAppName() == null) {configInfo.setAppName(appNameTmp);}//更新配置数据updateConfigInfoAtomic(configInfo, srcIp, srcUser, time, configAdvanceInfo);String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags");if (configTags != null) {// delete all tags and then recreateremoveTagByIdAtomic(oldConfigInfo.getId());addConfigTagsRelation(oldConfigInfo.getId(), configTags, configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());}//保存到发布配置历史表insertConfigHistoryAtomic(oldConfigInfo.getId(), oldConfigInfo, srcIp, srcUser, time, "U");} catch (CannotGetJdbcConnectionException e) {LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);throw e;}return Boolean.TRUE;});}@Overridepublic ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;try {return this.jt.queryForObject("SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);} catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.return null;} catch (CannotGetJdbcConnectionException e) {LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);throw e;}}...
}
二.发布配置变更事件
执行ConfigChangePublisher的notifyConfigChange()方法发布配置变更事件时,最终会把事件添加到DefaultPublisher.queue阻塞队列中,完成事件发布。
NotifyCenter在其静态方法中,会创建DefaultPublisher并进行初始化。在执行DefaultPublisher的init()方法时,就会开启一个异步任务。该异步任务便会不断从阻塞队列DefaultPublisher.queue中获取事件,然后调用DefaultPublisher的receiveEvent()方法处理配置变更事件。
在DefaultPublisher的receiveEvent()方法中,会循环遍历事件订阅者。其中就会包括来自客户端,以及来自集群节点的两个订阅者。前者会通知客户端发生了配置变更事件,后者会通知各集群节点发生了配置变更事件。而且进行事件通知时,都会调用DefaultPublisher的notifySubscriber()方法。该方法会异步执行订阅者的监听逻辑,也就是subscriber.onEvent()方法。
具体的subscriber订阅者有:用来通知集群节点进行数据同步的订阅者AsyncNotifyService,用来通知客户端处理配置文件变更的订阅者LongPollingService。
事件发布机制的实现简单总结:发布者需要一个Set存放注册的订阅者,发布者发布事件时,需要遍历调用订阅者处理事件的方法。
public class ConfigChangePublisher {//Notify ConfigChange.public static void notifyConfigChange(ConfigDataChangeEvent event) {if (PropertyUtil.isEmbeddedStorage() && !EnvUtil.getStandaloneMode()) {return;}NotifyCenter.publishEvent(event);}
}//Unified Event Notify Center.
public class NotifyCenter {static {...try {// Create and init DefaultSharePublisher instance.INSTANCE.sharePublisher = new DefaultSharePublisher();INSTANCE.sharePublisher.init(SlowEvent.class, shareBufferSize); } catch (Throwable ex) {LOGGER.error("Service class newInstance has error : {}", ex);}ThreadUtils.addShutdownHook(new Runnable() {@Overridepublic void run() {shutdown();}});}//注册订阅者public static <T> void registerSubscriber(final Subscriber consumer) {...addSubscriber(consumer, subscribeType);}private static void addSubscriber(final Subscriber consumer, Class<? extends Event> subscribeType) {...EventPublisher publisher = INSTANCE.publisherMap.get(topic);//执行DefaultPublisher.addSubscriber()方法publisher.addSubscriber(consumer);}...//Request publisher publish event Publishers load lazily, calling publisher. Start () only when the event is actually published.public static boolean publishEvent(final Event event) {try {return publishEvent(event.getClass(), event);} catch (Throwable ex) {LOGGER.error("There was an exception to the message publishing : {}", ex);return false;}}//Request publisher publish event Publishers load lazily, calling publisher.private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {return INSTANCE.sharePublisher.publish(event);}final String topic = ClassUtils.getCanonicalName(eventType);EventPublisher publisher = INSTANCE.publisherMap.get(topic);if (publisher != null) {//执行DefaultPublisher.publish()方法return publisher.publish(event);}LOGGER.warn("There are no [{}] publishers for this event, please register", topic);return false;}...
}//The default event publisher implementation.
public class DefaultPublisher extends Thread implements EventPublisher {protected final ConcurrentHashSet<Subscriber> subscribers = new ConcurrentHashSet<Subscriber>();private BlockingQueue<Event> queue;...@Overridepublic void addSubscriber(Subscriber subscriber) {//注册事件订阅者subscribers.add(subscriber);}@Overridepublic boolean publish(Event event) {checkIsStart();//将事件添加到阻塞队列,则表示已完成事件发布boolean success = this.queue.offer(event);if (!success) {LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);receiveEvent(event);return true;}return true;}@Overridepublic void init(Class<? extends Event> type, int bufferSize) {setDaemon(true);setName("nacos.publisher-" + type.getName());this.eventType = type;this.queueMaxSize = bufferSize;this.queue = new ArrayBlockingQueue<Event>(bufferSize);start();}@Overridepublic synchronized void start() {if (!initialized) {//执行线程的run()方法,start just called oncesuper.start();if (queueMaxSize == -1) {queueMaxSize = ringBufferSize;}initialized = true;}}@Overridepublic void run() {openEventHandler();}void openEventHandler() {try {//This variable is defined to resolve the problem which message overstock in the queue.int waitTimes = 60;//To ensure that messages are not lost, enable EventHandler when waiting for the first Subscriber to registerfor (; ;) {if (shutdown || hasSubscriber() || waitTimes <= 0) {break;}ThreadUtils.sleep(1000L);waitTimes--;}for (; ;) {if (shutdown) {break;}final Event event = queue.take();receiveEvent(event);UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));}} catch (Throwable ex) {LOGGER.error("Event listener exception : {}", ex);}}//Receive and notifySubscriber to process the event.void receiveEvent(Event event) {final long currentEventSequence = event.sequence();//循环遍历事件的订阅者for (Subscriber subscriber : subscribers) {// Whether to ignore expiration eventsif (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire", event.getClass());continue;}//通知事件订阅者notifySubscriber(subscriber, event);}}@Overridepublic void notifySubscriber(final Subscriber subscriber, final Event event) {LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);final Runnable job = new Runnable() {@Overridepublic void run() {//异步执行订阅者的监听逻辑subscriber.onEvent(event);}};final Executor executor = subscriber.executor();if (executor != null) {executor.execute(job);} else {try {job.run();} catch (Throwable e) {LOGGER.error("Event callback exception : {}", e);}}}...
}
(3)集群节点间的配置数据变更同步
核心处理方法便是AsyncNotifyService的onEvent()方法。该方法首先会获取集群节点列表,然后遍历集群列表构造通知任务NotifySingleTask,接着把通知任务NotifySingleTask添加到队列queue当中,最后根据通知任务队列queue封装一个异步任务提交到线程池去处理,也就是异步任务AsyncTask的run()方法会处理通知任务NotifySingleTask。
在异步任务AsyncTask的run()方法中,会一直从queue中获取通知任务,以便将配置数据同步到对应的集群节点。具体就是在while循环中,首先获得通知任务中对应的集群节点的IP地址。然后判断该集群节点的IP是否在当前节点的配置中,并且是否是健康状态。如果该集群节点不健康,则放入队列并将队列提交给异步任务来延迟处理。如果该集群节点是健康状态,则通过HTTP方式发起配置数据的同步,地址是"/v1/cs/communication/dataChange"。
@Service
public class AsyncNotifyService {...@Autowiredpublic AsyncNotifyService(ServerMemberManager memberManager) {this.memberManager = memberManager;//Register ConfigDataChangeEvent to NotifyCenter.NotifyCenter.registerToPublisher(ConfigDataChangeEvent.class, NotifyCenter.ringBufferSize);//Register A Subscriber to subscribe ConfigDataChangeEvent.NotifyCenter.registerSubscriber(new Subscriber() {@Overridepublic void onEvent(Event event) {//配置中心数据变更,同步其他集群节点数据if (event instanceof ConfigDataChangeEvent) {ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;long dumpTs = evt.lastModifiedTs;String dataId = evt.dataId;String group = evt.group;String tenant = evt.tenant;String tag = evt.tag;//获取集群节点列表Collection<Member> ipList = memberManager.allMembers();Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();//遍历集群列表构造通知任务NotifySingleTask去同步数据for (Member member : ipList) {//把通知任务NotifySingleTask添加到队列queue当中queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(), evt.isBeta));}//根据通知任务队列Queue<NotifySingleTask>,封装一个异步任务AsyncTask,提交到线程池执行ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));}}@Overridepublic Class<? extends Event> subscribeType() {return ConfigDataChangeEvent.class;}});}...class AsyncTask implements Runnable {private Queue<NotifySingleTask> queue;private NacosAsyncRestTemplate restTemplate;public AsyncTask(NacosAsyncRestTemplate restTemplate, Queue<NotifySingleTask> queue) {this.restTemplate = restTemplate;this.queue = queue;}@Overridepublic void run() {executeAsyncInvoke();}private void executeAsyncInvoke() {while (!queue.isEmpty()) {//一直从queue队列中获取通知任务,以便将配置数据同步到对应的集群节点NotifySingleTask task = queue.poll();//获取通知任务中对应的集群节点的IP地址String targetIp = task.getTargetIP();if (memberManager.hasMember(targetIp)) {//start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify//判断该集群节点的ip是否在当前节点的配置中,并且是否是健康状态boolean unHealthNeedDelay = memberManager.isUnHealth(targetIp);if (unHealthNeedDelay) {//target ip is unhealthy, then put it in the notification list//如果该集群节点不健康,则放入另外一个队列,同样会将队列提交给异步任务,然后延迟处理ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,0, task.target);//get delay time and set fail count to the taskasyncTaskExecute(task);} else {//如果该集群节点是健康状态,则通过HTTP方式发起配置数据的同步Header header = Header.newInstance();header.addParam(NotifyService.NOTIFY_HEADER_LAST_MODIFIED, String.valueOf(task.getLastModified()));header.addParam(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP, InetUtils.getSelfIP());if (task.isBeta) {header.addParam("isBeta", "true");}AuthHeaderUtil.addIdentityToHeader(header);//通过HTTP方式发起配置数据的同步,请求的HTTP地址:/v1/cs/communication/dataChangerestTemplate.get(task.url, header, Query.EMPTY, String.class, new AsyncNotifyCallBack(task));}}}}}private void asyncTaskExecute(NotifySingleTask task) {int delay = getDelayTime(task);Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();queue.add(task);AsyncTask asyncTask = new AsyncTask(nacosAsyncRestTemplate, queue);//提交异步任务给线程池延迟执行ConfigExecutor.scheduleAsyncNotify(asyncTask, delay, TimeUnit.MILLISECONDS);}
}
当集群节点处理"/v1/cs/communication/dataChange"这个HTTP请求时,会调用CommunicationController的notifyConfigInfo()方法,接着调用DumpService的dump()方法将请求包装成DumpTask同步数据任务,然后调用TaskManager的addTask()方法将DumpTask同步数据任务放入map。
TaskManager的父类NacosDelayTaskExecuteEngine在初始化时,会开启一个异步任务执行ProcessRunnable的run()方法,也就是会不断从map中取出DumpTask同步数据任务,然后调用DumpProcessor的process()方法处理具体的配置数据同步逻辑。也就是查询数据库最新的配置,然后持久化配置数据到磁盘上,从而完成集群之间配置数据的同步。
@RestController
@RequestMapping(Constants.COMMUNICATION_CONTROLLER_PATH)
public class CommunicationController {private final DumpService dumpService;...@GetMapping("/dataChange")public Boolean notifyConfigInfo(HttpServletRequest request, @RequestParam("dataId") String dataId,@RequestParam("group") String group,@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,@RequestParam(value = "tag", required = false) String tag) {dataId = dataId.trim();group = group.trim();String lastModified = request.getHeader(NotifyService.NOTIFY_HEADER_LAST_MODIFIED);long lastModifiedTs = StringUtils.isEmpty(lastModified) ? -1 : Long.parseLong(lastModified);String handleIp = request.getHeader(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP);String isBetaStr = request.getHeader("isBeta");if (StringUtils.isNotBlank(isBetaStr) && trueStr.equals(isBetaStr)) {dumpService.dump(dataId, group, tenant, lastModifiedTs, handleIp, true);} else {dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);}return true;}...
}public abstract class DumpService {private TaskManager dumpTaskMgr;public DumpService(PersistService persistService, ServerMemberManager memberManager) {...this.processor = new DumpProcessor(this);this.dumpTaskMgr = new TaskManager("com.alibaba.nacos.server.DumpTaskManager");this.dumpTaskMgr.setDefaultTaskProcessor(processor);...}...public void dump(String dataId, String group, String tenant, long lastModified, String handleIp, boolean isBeta) {String groupKey = GroupKey2.getKey(dataId, group, tenant);dumpTaskMgr.addTask(groupKey, new DumpTask(groupKey, lastModified, handleIp, isBeta));}...
}public final class TaskManager extends NacosDelayTaskExecuteEngine implements TaskManagerMBean {...@Overridepublic void addTask(Object key, AbstractDelayTask newTask) {super.addTask(key, newTask);MetricsMonitor.getDumpTaskMonitor().set(tasks.size());}...
}public class NacosDelayTaskExecuteEngine extends AbstractNacosTaskExecuteEngine<AbstractDelayTask> {protected final ConcurrentHashMap<Object, AbstractDelayTask> tasks;//任务池public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {super(logger);tasks = new ConcurrentHashMap<Object, AbstractDelayTask>(initCapacity);processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));//开启延时任务processingExecutor.scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);}...@Overridepublic void addTask(Object key, AbstractDelayTask newTask) {lock.lock();try {AbstractDelayTask existTask = tasks.get(key);if (null != existTask) {newTask.merge(existTask);}//最后放入到ConcurrentHashMap中tasks.put(key, newTask);} finally {lock.unlock();}}...private class ProcessRunnable implements Runnable {@Overridepublic void run() {try {processTasks();} catch (Throwable e) {getEngineLog().error(e.toString(), e);}}}@Overridepublic Collection<Object> getAllTaskKeys() {Collection<Object> keys = new HashSet<Object>();lock.lock();try {keys.addAll(tasks.keySet());} finally {lock.unlock();}return keys;}protected void processTasks() {//获取tasks中所有的任务,然后进行遍历Collection<Object> keys = getAllTaskKeys();for (Object taskKey : keys) {//通过任务key,获取具体的任务,并且从任务池中移除掉AbstractDelayTask task = removeTask(taskKey);if (null == task) {continue;}//DumpService在初始化时会设置TaskManager的默认processor是DumpProcessor//根据taskKey获取NacosTaskProcessor延迟任务处理器:DumpProcessorNacosTaskProcessor processor = getProcessor(taskKey);if (null == processor) {getEngineLog().error("processor not found for task, so discarded. " + task);continue;}try {//ReAdd task if process failed//调用DumpProcessor.process()方法if (!processor.process(task)) {//如果失败了,会重试添加task回tasks这个map中retryFailedTask(taskKey, task);}} catch (Throwable e) {getEngineLog().error("Nacos task execute error : " + e.toString(), e);retryFailedTask(taskKey, task);}}}
}
(4)服务端通知客户端配置数据已变更
服务端通知客户端配置文件变更的方法是LongPollingService.onEvent()。
由前面客户端如何感知远程配置数据的变更可知,Nacos客户端启动时:会调用ConfigService的addListener()方法为每个dataId + group添加一个监听器。而NacosConfigService初始化时会创建ClientWorker对象,此时会开启多个长连接任务即执行LongPollingRunnable的run()方法。
执行LongPollingRunnable的run()方法时,会触发执行ClientWorker的checkUpdateDataIds()方法,该方法最后会调用服务端的"/v1/cs/configs/listener"接口,将当前客户端添加到LongPollingService的allSubs属性中。
这样当以后dataId + group的配置发生变更时,服务端会触发执行LongPollingService的onEvent()方法,然后遍历LongPollingService.allSubs属性通知客户端配置已变更。
客户端收到变更事件通知后,会将最新的配置刷新到容器中,同时将@RefreshScope注解修饰的Bean从缓存中删除。这样再次访问这些Bean,就会重新创建Bean,从而读取到最新的配置。
public class NacosConfigService implements ConfigService {//long polling.private final ClientWorker worker;...public NacosConfigService(Properties properties) throws NacosException {...this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);}...
}//Long polling.
public class ClientWorker implements Closeable {public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {...this.executor.scheduleWithFixedDelay(new Runnable() {@Overridepublic void run() {try {checkConfigInfo();} catch (Throwable e) {LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);}}}, 1L, 10L, TimeUnit.MILLISECONDS);}//Check config info.public void checkConfigInfo() {//Dispatch taskes.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.//执行长连接任务:LongPollingRunnable.run()executorService.execute(new LongPollingRunnable(i));}currentLongingTaskCount = longingTaskCount;}}...class LongPollingRunnable implements Runnable {...@Overridepublic void run() {...//check server configList<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);...}}//Fetch the dataId list from server.List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {...return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);}//Fetch the updated dataId list from server.List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {...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请求:/v1/cs/configs/listener,将客户端添加到LongPollingService.allSubs属性中HttpRestResult<String> result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(), readTimeoutMs);...} catch (Exception e) {...}return Collections.emptyList();}
}@RestController
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {private final ConfigServletInner inner;...@PostMapping("/listener")@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)public void listener(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {...//do long-pollinginner.doPollingConfig(request, response, clientMd5Map, probeModify.length());}...
}@Service
public class ConfigServletInner {...//轮询接口.public String doPollingConfig(HttpServletRequest request, HttpServletResponse response, Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {//Long polling.if (LongPollingService.isSupportLongPolling(request)) {longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);return HttpServletResponse.SC_OK + "";}...}...
}@Service
public class LongPollingService {//客户端长轮询订阅者final Queue<ClientLongPolling> allSubs;...public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) {...//添加订阅者ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));}class ClientLongPolling implements Runnable {@Overridepublic void run() {...allSubs.add(this);}...}...public LongPollingService() {allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();...NotifyCenter.registerSubscriber(new Subscriber() {@Overridepublic void onEvent(Event event) {if (isFixedPolling()) {// Ignore.} else {if (event instanceof LocalDataChangeEvent) {LocalDataChangeEvent evt = (LocalDataChangeEvent) event;//触发执行DataChangeTask.run()方法ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));}}}...});}class DataChangeTask implements Runnable {@Overridepublic void run() {try {ConfigCacheService.getContentBetaMd5(groupKey);//遍历订阅了配置变更事件的客户端for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {ClientLongPolling clientSub = iter.next();if (clientSub.clientMd5Map.containsKey(groupKey)) {...getRetainIps().put(clientSub.ip, System.currentTimeMillis());iter.remove(); // Delete subscribers' relationships.//发送服务端数据变更的响应给客户端clientSub.sendResponse(Arrays.asList(groupKey));}}} catch (Throwable t) {LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));}}}...
}
(5)总结
一.配置中心数据变更同步集群节点的整体逻辑
当在Nacos后台变更配置数据后:首先自身节点会把最新的配置数据更新到数据库中,并且添加变更历史。然后利用事件发布订阅机制来通知订阅者,其中订阅者AsyncNotifyService会通过HTTP方式来通知其他集群节点。当其他集群节点收到通知后,会重新查询数据库最新的配置数据。然后持久化到磁盘上,因为获取配置数据的接口是直接读磁盘文件的。集群节点的配置数据同步完成后,还要通知客户端配置数据已变更。
二.服务端通知客户端配置数据已变更
在客户端给dataId + group添加监听器后,会和服务端建立一个长轮询,所以另外一个订阅者LongPollingService会通过长轮询通知客户端。也就是会遍历每一个客户端,通过长轮询向客户端进行响应。最终会调用到客户端监听器的回调方法,从而去刷新客户端的配置Bean。
相关文章:
Nacos源码—5.Nacos配置中心实现分析二
大纲 1.关于Nacos配置中心的几个问题 2.Nacos如何整合SpringBoot读取远程配置 3.Nacos加载读取远程配置数据的源码分析 4.客户端如何感知远程配置数据的变更 5.集群架构下节点间如何同步配置数据 4.客户端如何感知远程配置数据的变更 (1)ConfigService对象使用介绍 (2)客…...
数智管理学(八)
四、未来管理学可能的新拓展方向 (一)人工智能与机器学习的融合形成智能决策管理职能 随着人工智能和机器学习技术的不断发展,它们将在管理学中得到更广泛的应用。传统决策方法难以快速处理海量数据并准确把握复杂的市场动态。人工智能与机…...
Compose Multiplatform iOS 稳定版发布:可用于生产环境,并支持 hotload
随着 Compose Multiplatform 1.8.0 的发布,iOS 版本也引来的第一个稳定版本,按照官方的原话:「iOS Is Stable and Production-Ready」 ,而 1.8.0 版本,也让 Kotlin 和 Compose 在移动端有了完整的支持。 在 2023 年 4 …...
spark基本介绍
一、Spark概述 Spark是一种基于内存的快速、通用、可拓展的大数据分析计算引擎。 Hadoop是一个分布式系统结构的基础架构。 二、Spark与Hadoop相比较的优势: 1. 处理速度:Hadoop:数据处理速度相对较慢 Spark:速度比Hadoop快很…...
DeepSeek智能时空数据分析(九):NL2SQL绘制河流名字-如何给轨迹添加说明文字
序言:时空数据分析很有用,但是GIS/时空数据库技术门槛太高 时空数据分析在优化业务运营中至关重要,然而,三大挑战仍制约其发展:技术门槛高,需融合GIS理论、SQL开发与时空数据库等多领域知识;空…...
管家婆实用贴-如何在Excel中清除空格
我们在使用管家婆软件时,经常会用到Excel表格导入导出数据,在使用Excel整理数据时,数据中的空格可能会导致计算和分析出现问题。无论是多余的前导空格、尾部空格还是单元格中的不必要空格,清除它们都是确保数据准确性的关键。今天…...
《软件项目管理》笔记一
软件项目管理概述 项目管理属于软件工程的组成之一,另外两部分为:软件开发,过程改进。 参考书如下: 1.1 项目与软件项目 1、项目: 为了创造一个唯一的产品或提供一个唯一的服务而进行 的临时性的努力。 2、项目的…...
前端线上错误日志收集与定位指南
想象一下:你的Web应用上线后,用户反馈“按钮点不动”或“页面白屏”,但你却无从下手!前端线上错误如JavaScript异常、网络失败,稍不注意就让用户流失,业务受损。令人抓狂的是,80%的错误悄无声息…...
可视化魔法指南
🎨 ECharts数据可视化魔法指南 🌟 ECharts:数据的艺术画笔 #mermaid-svg-ARwFHUrXBJ03Gpo9 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ARwFHUrXBJ03Gpo9 .error-icon{fill:#552222;}#mermaid-svg-ARwFHUr…...
使用ffmpeg截取MP3等音频片段
可以使用以下命令通过 ffmpeg 截取 MP3 音频文件的指定片段: ffmpeg的安装方法参考:linux 安装包方式安装ffmpeg,并在环境中设定或指定ffmpeg地址_linux 通过ffmpeg访问地址-CSDN博客 ffmpeg -ss [start_time] -i input.mp3 -to [end_time] -codec copy output.mp3 参数说…...
FFmpeg(7.1版本)编译生成ffplay
FFmpeg在编译的时候,没有生成ffplay,怎么办? 1. 按照上一篇文章:FFmpeg(7.1版本)在Ubuntu18.04上的编译_ffmpeg-7.1-CSDN博客 在build.sh脚本里配置了ffplay 但是,实际上却没有生成ffplay,会是什么原因呢? 2. 原因是编译ffplay的时候,需要一些依赖库 sudo apt-get i…...
CAN学习之--不使用收发器进行通讯测试
在实际调试或者学习CAN通讯过程中,在需要进行CAN调试的时候,但是手头有只有MCU的评估板,没有CAN的收发器,比如ATA6561、MCP2518这类芯片的时候,该怎么办呢? 因为我们知道,CAN收发器只是做信号的…...
律所项目管理全攻略:人力分配 / 案件管控 / 知识沉淀三维度解析(附专用工具清单)
引言:律所项目管理破局 ——从经验驱动到体系化运营 在法律服务行业数字化转型加速的背景下,律所项目管理能力已成为决定服务质量、客户满意度及团队效能的核心竞争力。从人力分配失衡导致的效率损耗,到案件流程模糊引发的客户信任危机&…...
Linux电源管理(7)_Wakeup events framework
原文链接:Linux电源管理(7)_Wakeup events framework 1. 前言 本文继续“Linux电源管理(6)_Generic PM之Suspend功能”中有关suspend同步以及PM wakeup的话题。这个话题,是近几年Linux kernel最具争议的话题之一,在国外Linux开发论坛&…...
Nvidia-smi 运行失败(Failed to initialize NVML: Driver/library version mismatch)
问题排查 在linux服务器上运行 nvidia-smi 命令,提示以下错误: Failed to initialize NVML: Driver/library version mismatch 首先查看内核驱动版本: cat /proc/driver/nvidia/version然后查看当前NVIDIA驱动版本: sudo dpkg …...
算法设计与分析实验题-序列对齐
基于 C 的序列最大对齐得分算法实现 在生物信息学和文本处理领域,序列对齐是一种常见的需求。本文将介绍如何使用 C 实现一个序列最大对齐得分算法,该算法可以计算两个序列在最优对齐方式下的最大得分。 问题描述 给定两个序列 S1 和 S2,我…...
第8章-1 查询性能优化-优化数据访问
上一篇:《 下一篇:《第7章-3 维护索引和表》》 在前面的章节中,我们介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于提高性能来说是必不可少的。但这些还不够——还需要合理地设计查询。如果查询写得很糟糕&a…...
每日一题洛谷P1025 [NOIP 2001 提高组] 数的划分c++
P1025 [NOIP 2001 提高组] 数的划分 - 洛谷 (luogu.com.cn) #include<iostream> using namespace std; int n, k; int res 0; void dfs(int num,int step,int sum) {//判断if (sum n) {if (step k) {res;return;}}if (sum > n || step k)return;//搜索for (int i …...
【python】使用Python和BERT进行文本摘要:从数据预处理到模型训练与生成
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着信息爆炸时代的到来,海量文本数据的高效处理与理解成为亟待解决的问题。文本摘要作为自然语言处理(NLP)中的关键任务,旨在自动生成…...
WHAT - Rust 智能指针
文章目录 常见的智能指针类型1. Box<T> — 堆上分配的数据2. Rc<T> — 引用计数的共享所有权(单线程)3. Arc<T> — 原子引用计数(多线程)4. RefCell<T> — 运行时可变借用(单线程)…...
用go从零构建写一个RPC(仿gRPC,tRPC)--- 版本1(Client端)
这里我们来实现这个RPC的client端 为了实现RPC的效果,我们调用的Hello方法,即server端的方法,应该是由代理来调用,让proxy里面封装网络请求,消息的发送和接受处理。而上一篇文章提到的服务端的代理已经在.rpc.go文件中…...
CentOS 安装 Zellij 终端复用器教程
CentOS 安装 Zellij 终端复用器教程 简介 Zellij 是一个现代化的终端复用器,使用 Rust 语言编写。它提供了类似 tmux 的功能,但具有更友好的用户界面和更现代化的特性。本教程将详细介绍如何在 CentOS 7.9 系统上安装 Zellij。 前置条件 CentOS 7.9 …...
基于 SpringBoot + Vue 的校园管理系统设计与实现
一、项目简介 本系统以校园组织管理为主线,结合用户权限分离机制与模块化设计,实现对“单位类别、单位、通知推送、投票信息、用户回复”等内容的全流程管理,广泛适用于教育局、高校及下属组织的信息管理工作。 🎯 项目亮点&…...
如何减少锁竞争并细化锁粒度以提高 Rust 多线程程序的性能?
在并发编程中,锁(Lock)是一种常用的同步机制,用于保护共享数据免受多个线程同时访问造成的竞态条件(Race Condition)。然而,不合理的锁使用会导致严重的性能瓶颈,特别是在高并发场景…...
【人工智能agent】--dify通过mcp协议调用工具
MCP Client 发起工具调用的实体,也就是 Dify 工作流或 Agent。它通过 Dify 平台提供的标准化接口(工具节点)来请求服务。 MCP Server / Host 提供实际服务的端点。在这个例子中,就是模拟 API 服务器 上的各个API (/api/pump/st…...
Review --- Redis
Redis redis是什么? Redis是一个开源的,使用C语言编写的,支持网络交互的,key-value数据结构存储系统,支持多种语言的一种非关系型数据库,它可以用作数据库(存储一些简单的数据,例如新闻点赞量),**缓存(秒…...
Sql刷题日志(day8)
一、笔试 1、right:提取字符串右侧指定数量的字符 right(string,length) /*string:要操作的字符串。length:要从右侧提取的字符数 */ 2、curdate():返回当前日期,格式通常为 YYYY-MM-DD 二、面试 1、自变量是不良体验反馈,因…...
【Science Advances】普林斯顿大学利用非相干光打造可重构纳米光子神经网络
(导读 ) 人工智能对计算性能需求剧增,电子微处理器发展受功耗限制。光学计算有望解决这些问题,光学神经网络(ONNs)成为研究热点,但现有 ONNs 因设计缺陷,在图像分类任务中精度远低于现代电子神经网络&#…...
2025-05-07 Unity 网络基础8——UDP同步异步通信
文章目录 1 UDP 概述1.1 通信流程1.2 TCP 与 UDP1.3 UDP 分包1.4 UDP 黏包 2 同步通信2.1 服务端2.2 客户端2.3 测试 3 异步通信3.1 Bgin / End 方法3.2 Async 方法 1 UDP 概述 1.1 通信流程 客户端和服务端的流程如下: 创建套接字 Socket。用 Bind() 方法将套…...
K8S - 金丝雀发布实战 - Argo Rollouts 流量控制解析
一、金丝雀发布概述 1.1 什么是金丝雀发布? 金丝雀发布(Canary Release)是一种渐进式部署策略,通过逐步将生产流量从旧版本迁移至新版本,结合实时指标验证,在最小化风险的前提下完成版本迭代。其核心逻辑…...
手持小风扇方案解说---【其利天下技术】
春去夏来,酷暑时节,小风扇成为外出必备的解暑工具,近年来,随着无刷电机的成本急剧下降,小风扇也逐步从有刷变无刷化了。 数量最大的如一箱无刷马达,其次三相低压无刷电机也大量被一些中高端风扇大量采用。…...
Qt开发:枚举的介绍和使用
文章目录 一、概述二、Qt 中定义和使用枚举2.1 普通枚举的定义方式2.2 使用枚举 三、配合 Qt 元对象系统使用枚举3.1 使用 Q_ENUM(Qt 5.5 及以上)3.2 示例:枚举值转字符串3.4 示例:字符串转枚举值 四、枚举与字符串相互转换五、枚…...
HarmonyOS运动开发:如何集成百度地图SDK、运动跟随与运动公里数记录
前言 在开发运动类应用时,集成地图功能以及实时记录运动轨迹和公里数是核心需求之一。本文将详细介绍如何在 HarmonyOS 应用中集成百度地图 SDK,实现运动跟随以及运动公里数的记录。 一、集成百度地图 SDK 1.引入依赖 首先,需要在项目的文…...
“胖都来”商标申请可以通过注册不!
近日“胖都来”被网友认为是蹭“胖东来”品牌流量在互联网上引起争议,看到许多自媒体说浙江这家公司已拿到“胖都来”的注册商标,普推知产商标老杨经检索后发现是没有的,只是申请受理。 对于商城类主要类别是在35类广告销售,核心是…...
【Django】中间件
Django 中间件是 Django 框架里一个轻量级、可插拔的组件,它能在全局范围内对 Django 的请求和响应进行处理。中间件处于 Django 的请求处理流程之中,在请求抵达视图函数之前以及视图函数返回响应之后执行特定操作。以下是关于 Django 中间件的详细介绍&…...
电子电器架构 --- 48V架构的一丢丢事情
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
什么是Blender?怎么获取下载Blender格式文件模型
glbxz.com glbxz.com 官方可以下载Blender格式文件模型 BlenderBlender 是一个免费的开源程序,用于建模和动画,最初由一家名为 Neo Geo 的动画工作室作为内部应用程序开发,后来作为自己的程序发布。这是一个称职的程序,近年来由于…...
Ubuntu安装pgsql
一、通过 APT 安装(推荐) 更新软件包列表 sudo apt update 安装 PostgreSQL 核心包及工具 sudo apt install postgresql postgresql-client postgresql-contrib • postgresql:数据库服务端 • postgresql-client:命令行…...
Qwen2-VL详解
一、引言 在人工智能领域,多模态大模型的发展备受关注。Qwen2-VL 作为一款先进的多模态模型,致力于克服现有方法在处理图像和视频数据时存在的不足,显著提升多模态信息的理解与交互能力。本文将全面且深入地阐述 Qwen2-VL 的创新理念、精妙的模型架构、严谨的训练流程、卓越…...
定长滑动窗口---初阶篇
目录 滑动窗口核心思想 定长滑动窗口套路 定长滑动窗口习题剖析 1456. 定长子串中元音的最大数目 643. 子数组最大平均数 I 1343. 大小为 K 且平均值大于等于阈值的子数组数目 2090. 半径为 k 的子数组平均值 2379. 得到 K 个黑块的最少涂色次数 2841. 几乎唯一子数组…...
以pytest_addoption 为例,讲解pytest框架中钩子函数的应用
钩子函数(Hook Function)的概念 钩子函数(Hook Function)是软件框架中预定义的回调接口,允许开发者在程序执行的特定阶段插入自定义逻辑,以扩展或修改框架的默认行为。在 pytest 中,钩子函数覆…...
数据智能重塑工业控制:神经网络在 MPC 中的四大落地范式与避坑指南
一、引言:工业控制的范式革命 在工业 4.0 的浪潮中,传统基于物理模型的控制方法(如 PID、线性二次型调节器 LQR)正面临前所未有的挑战。以石化行业为例,某炼油厂的催化裂化装置(FCCU)因反应机理…...
AB测试面试题
AB测试面试题 常考AB测试问答题(1)AB测试的优缺点是什么?(2)AB测试的一般流程/介绍一下日常工作中你是如何做A/B实验的?(3)第一类错误 vs 第二类错误 vs 你怎么理解AB测试中的第一、二类错误?(4)统计显著=实际显著?(5)AB测试效果统计上不显著?(6)实验组优于对…...
phpstudy升级新版apache
1.首先下载要升级到的apache版本,这里apache版本为Apache 2.4.63-250207 Win64下载地址:Apache VS17 binaries and modules download 2.将phpstudy中原始apache复制备份Apache2.4.39_origin 3.将1中下载apache解压, 将Apache24复制一份到ph…...
民宿管理系统6
普通管理员管理: 新增普通管理员: 前端效果: 前端代码: <body> <div class"layui-fluid"><div class"layui-row"><div class"layui-form"><div class"layui-f…...
【iOS】源码阅读(三)——内存对齐原理
文章目录 前言获取内存大小的三种常用方式sizeofclass_getInstanceSizemalloc_size 总结 前言 之前学习alloc相关源码,涉及到内存对齐的相关内容,今天笔者详细学习了一下相关内容并写了此篇博客。 获取内存大小的三种常用方式 获取内存大小的方式有很多…...
在 Ubuntu 中配置 Samba 实现「特定用户可写,其他用户只读」的共享目录
需求目标 所有认证用户可访问 Samba 共享目录 /path/to/home;**仅特定用户(如 developer)**拥有写权限;其他用户仅允许读取;禁止匿名访问。 配置步骤 1. 设置文件系统权限 将目录 /home3/guest 的所有权设为 develo…...
配置指定地址的conda虚拟Python环境
创建指定路径的 Conda 环境 在创建环境时,使用 --prefix 参数指定自定义路径: conda create --prefix/your/custom/path/my_env python3.8 说明: /your/custom/path/my_env:替换为你希望存放环境的路径(如 D:\projec…...
从彼得·蒂尔四象限看 Crypto「情绪变迁」:从密码朋克转向「标准化追求者」
作者:Techub 精选编译 撰文:Matti,Zee Prime Capital 编译:Yangz,Techub News 我又带着一篇受彼得蒂尔(Peter Thiel)启发的思想杂烩回来了。作为自封的「蒂尔学派」信徒,我常透过他…...
VS Code 常用插件
React Auto Import - ES6, TS, JSX, TSX Auto Rename Tag ES7 React/Redux/React-Native snippets Markdown Markdown All in One Markdown Preview Enhanced Other Prettier - Code formatter 格式化代码 Live Server 本地服务器实时预览与自动刷新...