Nacos服务注册总流程(源码分析)

文章目录

    • 服务注册
      • NacosClient找看源码入口
      • NacosClient服务注册源码
      • NacosServer处理服务注册

服务注册

服务注册 在线流程图

在这里插入图片描述



NacosClient找看源码入口

我们启动一个微服务,引入nacos客户端的依赖

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

并配置文件中指定nacosServer的地址,并运行该微服务,那么服务是如何注册进nacosServer中去的嘞?

server:
  port: 9002
spring:
  application:
    name: stock-service

  cloud:
    nacos:
      discovery:
        # 指定nacos server的地址
        server-addr: localhost:8848
        # 指定集群名称
        cluster-name: SZ
        metadata:
          version: v3

我们应该怎么找源码的入口嘞?

我们知道SpringCloud是基于SpringBoot的,我们这里引入了这个nacos客户端nacos-discovery的依赖,我们就直接去找这个jar包下的spring.factories文件

在这里插入图片描述

这个文件中记录了很多的自动配置类,如果不知道应该看哪一个的话,那么就优先看和我们导入maven依赖名字相近的类

在这里插入图片描述


进入到这个NacosDiscoveryAutoConfiguration自动配置类后会发现这里会往容器中注入三个bean,看bean的名字好像都是和nacos注册相关的,按照经验来说,带有AutoXXXX这类方法一般都比较重要,再加上第三个方法还用到了上面两个bean,所以核心方法很大概率就是这个第三个方法。

在这里插入图片描述


进入到该类的构造方法之后,我们一般可以查看这个类的继承与实现结构,这样可以让我们更充分的了解该类

在这里插入图片描述


从上图中可以发现,NacosAutoServiceRegistration这个类还实现了ApplicationListener接口,这个接口我们都知道是Spring发布事件相关的接口

// ApplicationListener接口
package org.springframework.context;

import java.util.EventListener;

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}

我们再找这个方法的具体实现,会发现NacosAutoServiceRegistration这个类它没有重写该方法,那么就要在它的父类中找该方法的实现

public void onApplicationEvent(WebServerInitializedEvent event) {
    this.bind(event);
}

// 方法调用,进入到bind(...)方法

public void bind(WebServerInitializedEvent event) {
    ApplicationContext context = event.getApplicationContext();
    // 有if、并且有返回、并且后面还有业务代码的一般情况下这都是一个分支代码,可以先跳过
    if (context instanceof ConfigurableWebServerApplicationContext) {
        if ("management".equals(((ConfigurableWebServerApplicationContext) context)
                                .getServerNamespace())) {
            return;
        }
    }
    
    // 一般情况下,start()  init()     begin()这种方法名命名的方法一般都是比较重要的代码
    this.port.compareAndSet(0, event.getWebServer().getPort());
    this.start();
}

根据看源码的经验来说:

  • 有if,并且有返回值的,并且后面还有业务代码的一般情况下这都是一个分支代码,可以先跳过
  • 一般情况下,start() init() begin()这中方法名命名的方法一般都是比较重要的代码

接下来再进入到start()方法中

public void start() {
    // if + return + 后面有业务逻辑,大概率是一个分支代码 可以先跳过。这里实际上也只是记录了一个日志
    if (!isEnabled()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Discovery Lifecycle disabled. Not starting");
        }
        return;
    }

    // only initialize if nonSecurePort is greater than 0 and it isn't already running
    // because of containerPortInitializer below
    if (!this.running.get()) {
        this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
        // 核心方法,可以发现在调用这个方法之前和调用方法之后都发布了一个事件
        // 正好我们要找的也就是注册相关的方法
        // 因为我们就是从NacosDiscoveryAutoConfiguration自动配置类的NacosAutoServiceRegistration这个和注册相关的bean进来的
        register();
        if (shouldRegisterManagement()) {
            registerManagement();
        }
        this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
        this.running.compareAndSet(false, true);
    }

}

接下来在点进register()方法,跳转几次之后就会进入到NamingService这个关键接口的registerInstance(...)方法中去




NacosClient服务注册源码

关键接口NamingService,它的registerInstance(...)方法就是服务注册。程序刚开始会进入到该接口的实现类NacosNamingServiceregisterInstance(String serviceName, String groupName, Instance instance)方法中

// 该方法 往NacosServer中注册一个微服务实例
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 调用该方法进行微服务注册,instance对象中保存着微服务的各种信息,比如ip、端口、访问权重、健康状态、是否上线等等
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

instance对象保存的内容如下图所示

在这里插入图片描述



public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {

    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                       instance);

    // 把instance对象中的数据取出来,封装成一个HashMap
    final Map<String, String> params = new HashMap<String, String>(16);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put(CommonParams.GROUP_NAME, groupName);
    params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
    params.put("ip", instance.getIp());
    params.put("port", String.valueOf(instance.getPort()));
    params.put("weight", String.valueOf(instance.getWeight()));
    params.put("enable", String.valueOf(instance.isEnabled()));
    params.put("healthy", String.valueOf(instance.isHealthy()));
    params.put("ephemeral", String.valueOf(instance.isEphemeral()));
    params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));

    // 发送一个post的http请求发送给NacosServer,进行服务注册
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}

在这里插入图片描述


再通过官方文档 / 接口详细文档就能发现这就是服务注册的接口

在这里插入图片描述



NacosServer处理服务注册

NacosClient向NacosServer发送了一个服务注册的post请求,url为/v1/ns/instance

NacosServer这个项目有很多很多的子工程,就拿下图所示,我应该如何去找嘞?

在这里插入图片描述


其实我们就可以双击shift键,直接搜索controller,一个一个文件夹找,或者是直接加上关键字,就比如我这里加上了instance这个关键字就找到了真正处理这个请求的controller,类名为InstanceController

在这里插入图片描述


还有一种方法就是,我们在NacosClient这边进行服务注册时会频繁的使用到NamingService,那我就在NacosServer端找有没有和Naming相关的子工程,最终发现还真就找到了

在这里插入图片描述



接下来是源码,首先直接查看该controller的post接口

@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {

    final String namespaceId = WebUtils
        .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);

    // 通过解析request请求参数,封装成instance对象
    final Instance instance = parseInstance(request);

    // 调用Service层方法进行服务实例注册
    serviceManager.registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

接下来进入到service层的registerInstance(...)方法逻辑

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {

    // 这里会去创建一个service,如果连该service对应的命名空间都不存在的话就会利用DCL双重锁检测机制去创建一个map<serviceName,service>
    // 再去添加service,添加命名空间对应的map<serviceName,service>中。命名空间对应的这个map是存在注册表serviceMap中的
    // 这里创建的只是一个空服务,它里面还没有instance服务实例
    // 在创建service过程中还会进行初始化,会添加一个延迟定时任务进行该服务下的所有实例的健康检查、修改服务健康状态、删除过时服务
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());

    // 这里再获取,就不应该为null了,如果为null也就该抛异常了
    // getService()会先根据命名空间取出一个Map,再从这个Map中根据serviceName取出service。service中包含instance微服务实例
    Service service = getService(namespaceId, serviceName);

    if (service == null) {
        throw new NacosException(NacosException.INVALID_PARAM,
                                 "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }

    // 进行服务实例instance添加操作
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}



createEmptyService(...)方法中,首先会去判断当前service是否存在,如果不存在则创建一个空的新的service,并且把这个service添加进namespaceId对应的Map中,如果在这个过程中,namespaceId命名空间不存在则使用DCL双重锁检测去创建一个map集合添加进注册表serviceMap中。接下来在对刚刚创建的service进行init()初始化操作,蛀牙就是开启一个延时的定时任务,定期对该服务中的实例进行健康检查,并修改不健康的状态或移除超时实例

public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
    createServiceIfAbsent(namespaceId, serviceName, local, null);
}

public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
    throws NacosException {
    // 先从服务注册表serviceMap中获取
    Service service = getService(namespaceId, serviceName);
    // 如果不存在就创建
    if (service == null) {

        Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
        service = new Service();
        service.setName(serviceName);
        service.setNamespaceId(namespaceId);
        service.setGroupName(NamingUtils.getGroupName(serviceName));
        // now validate the service. if failed, exception will be thrown
        service.setLastModifiedMillis(System.currentTimeMillis());
        service.recalculateChecksum();
        if (cluster != null) {
            cluster.setService(service);
            service.getClusterMap().put(cluster.getName(), cluster);
        }
        service.validate();

        // 这里采用了DCL双重锁检测机制进行的添加操作
        putServiceAndInit(service);
        if (!local) {
            addOrReplaceService(service);
        }
    }
}

private void putServiceAndInit(Service service) throws NacosException {
    // 将service添加进对应的NamespaceId中
    putService(service);
    // 服务进行初始化,主要就是添加延时定时任务,对实例进行健康检查
    service.init();
    consistencyService
        .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
    consistencyService
        .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
    Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

public void putService(Service service) {
    // DCL双重检测机制
    if (!serviceMap.containsKey(service.getNamespaceId())) {
        synchronized (putServiceLock) {
            if (!serviceMap.containsKey(service.getNamespaceId())) {
                serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
            }
        }
    }
    serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}

public void init() {
    // NacosClient客户端的心跳检测任务
    // 这里就会开启一个线程去执行任务,clientBeatCheckTask属性就是一个task,run()方法中会进行实例的健康检查,后面会详细介绍
    HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
    for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
        entry.getValue().setService(this);
        entry.getValue().init();
    }
}



接下来就进入到了addInstance(...)添加实例的方法中了,这个方法中主要做的事就是将当前service的所有实例通过一个key存入一个Map中,然后保存在一个dataStore这个Map中,同时再把这个key和相关的操作类型存入一个阻塞队列中,至此NacosClient发送的请求就处理完成了。之后会有单独一个线程异步处理进行真正的服务注册。

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
    throws NacosException {

    // 根据ephemeral的值来决定生成上面key,默认情况下NacosClient传递过来的都是true,一般微服务的实例都是临时实例,不是持久化实例
    // 如果是持久化实例就没有下面的ephemeral这个字符串拼接
    // key = 一些字符串常量 + “ephemeral” + namespaceId + “##” + serviceName
    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

    Service service = getService(namespaceId, serviceName);

    synchronized (service) {
        // 添加微服务的实例,并返回当前服务所有的实例
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

        Instances instances = new Instances();
        instances.setInstanceList(instanceList);

        // key主要就是命名空间+服务名,通过这个key添加进consistencyService中
        consistencyService.put(key, instances);
    }
}



// 上面的Instances实现了Record接口,这里其实也就是一个封装
public void put(String key, Record value) throws NacosException {
    //  mapConsistencyService(key)这里会根据是否是临时实例进而去调用不同实现类的put()方法,这里以临时实例举例
    mapConsistencyService(key).put(key, value);
}



public void put(String key, Record value) throws NacosException {
    // 将key和所有服务实例封装的Record对象封装成一个datum对象,并保存到一个map集合中。
    // 同时还有一个key和DataOperation枚举操作类型添加进阻塞队列的操作。
    // 后续肯定有一个线程从这个注释队列中取出数据,然后根据key把datum对象取出来
    onPut(key, value);
    distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                        globalConfig.getTaskDispatchPeriod() / 2);
}



public void onPut(String key, Record value) {

    if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
        Datum<Instances> datum = new Datum<>();
        datum.value = (Instances) value;
        datum.key = key;
        datum.timestamp.incrementAndGet();
        // 这里把key和datum存入了一个dataMap这个集合中
        // 这个datum对象中保存着我当前服务所有的服务实例instances
        dataStore.put(key, datum);
    }

    if (!listeners.containsKey(key)) {
        return;
    }

    // 将key和后面这个枚举参数封装成一个Pair对象,并添加进一个阻塞队列中
    notifier.addTask(key, DataOperation.CHANGE);
}


public void addTask(String datumKey, DataOperation action) {

    if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
        return;
    }
    if (action == DataOperation.CHANGE) {
        services.put(datumKey, StringUtils.EMPTY);
    }
    // 把key和dataOperation操作枚举添加进一个阻塞队列中,真正进行服务注册的是在下面的run()方法中
    tasks.offer(Pair.with(datumKey, action));
}

// 另一个线程异步进行处理,从阻塞队列中获取数据,真正进行服务注册操作
@Override
public void run() {
    Loggers.DISTRO.info("distro notifier started");

    // 该线程会一直死循环,从阻塞队列中取数据
    for (; ; ) {
        try {
            Pair<String, DataOperation> pair = tasks.take();
            handle(pair);
        } catch (Throwable e) {
            Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
        }
    }
}



接下来就是详细查看这个handle(pair)方法,查看另一个线程是如何进行服务注册的?

这里会从先判断操作类型,然后从handle()方法调用到onChange(...)方法;而在onChange(...)方法中首先会对服务实例的权重进行处理,再调用到updateIPs()方法;在updateIPs()方法中会对service中的cluster做一些处理,最终就会调用到updateIps(...)方法中,在这个方法中会通过CopyOnWrite思想真正更改保存instance实例的集合

// handle()方法调用到onChange(...)方法
private void handle(Pair<String, DataOperation> pair) {
    try {
        String datumKey = pair.getValue0();
        DataOperation action = pair.getValue1();

        services.remove(datumKey);

        int count = 0;

        if (!listeners.containsKey(datumKey)) {
            return;
        }

        for (RecordListener listener : listeners.get(datumKey)) {

            count++;

            try {
                if (action == DataOperation.CHANGE) {
                    // dataStore.get(datumKey)取出来的对象是Datum类型的对象,它其中保存在key+所有服务实例instances
                    // dataStore.get(datumKey).value 就是这个datumKey对应的所有服务实例instances
                    listener.onChange(datumKey, dataStore.get(datumKey).value);
                    continue;
                }

                if (action == DataOperation.DELETE) {
                    listener.onDelete(datumKey);
                    continue;
                }
            } catch (Throwable e) {
                ...
            }
        }
        ...
    } catch (Throwable e) {
        ...
    }
}


// 对服务实例的权重进行处理,再调用到updateIPs()方法,
public void onChange(String key, Instances value) throws Exception {

    Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);

    // 对instance服务实例的特殊权重做处理
    for (Instance instance : value.getInstanceList()) {

        if (instance == null) {
            // Reject this abnormal instance list:
            throw new RuntimeException("got null instance " + key);
        }

        if (instance.getWeight() > 10000.0D) {
            instance.setWeight(10000.0D);
        }

        if (instance.getWeight() < 0.01D && instance.getWeight() > 0.0D) {
            instance.setWeight(0.01D);
        }
    }

    updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));

    recalculateChecksum();
}


// 会对service中的cluster做一些处理,最终就会调用到updateIps(...)方法中
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
    Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
    // 遍历所有集群
    for (String clusterName : clusterMap.keySet()) {
        ipMap.put(clusterName, new ArrayList<>());
    }

    for (Instance instance : instances) {
        try {
            if (instance == null) {
                Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
                continue;
            }

            // 实例instance的默认集群名字
            if (StringUtils.isEmpty(instance.getClusterName())) {
                instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
            }

            if (!clusterMap.containsKey(instance.getClusterName())) {
                Loggers.SRV_LOG
                    .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
                          instance.getClusterName(), instance.toJson());
                Cluster cluster = new Cluster(instance.getClusterName(), this);
                cluster.init();
                getClusterMap().put(instance.getClusterName(), cluster);
            }

            List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
            // 一个新的集群名处理
            if (clusterIPs == null) {
                clusterIPs = new LinkedList<>();
                ipMap.put(instance.getClusterName(), clusterIPs);
            }

            // 实例添加进对应的集群集合中
            clusterIPs.add(instance);
        } catch (Exception e) {
            Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
        }
    }

    for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
        //make every ip mine
        List<Instance> entryIPs = entry.getValue();
        // 微服务实例真正的注册进服务注册表的方法
        clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
    }

    
    setLastModifiedMillis(System.currentTimeMillis());
     // 服务实例改变之后,会发布一个ServiceChangeEvent事件
    getPushService().serviceChanged(this);
    StringBuilder stringBuilder = new StringBuilder();

    for (Instance instance : allIPs()) {
        stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
    }

    Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
                         stringBuilder.toString());

}
/**
     * Update instance list.
     * 下面的方法中有CopyOnWrite的实现思想,真正存储实例的集合是ephemeralInstances,
     * 但是这里面基本上都是在围绕oldIpMap这个复制出来的副本集合进行相应的操作,最后拿最新的集合复制给ephemeralInstances
     *
     * @param ips       instance list
     * @param ephemeral whether these instances are ephemeral
     */
public void updateIps(List<Instance> ips, boolean ephemeral) {

    Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;

    // 先保存一份现有的实例列表
    HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());

    for (Instance ip : toUpdateInstances) {
        // 各个服务实例的ip和端口不一样,所以这里生成的key也不一样
        oldIpMap.put(ip.getDatumKey(), ip);
    }
    // 接下来的很多操作就是拿老的实例集合和新的实例集合做一些数据比对,如果是新产生的实例那么就进行添加操作,如果是已经存在了的实例则是修改操作
    // 最后将最终的结果赋值给ephemeralInstances或persistentInstances

    List<Instance> updatedIPs = updatedIps(ips, oldIpMap.values());
    if (updatedIPs.size() > 0) {
        for (Instance ip : updatedIPs) {
            Instance oldIP = oldIpMap.get(ip.getDatumKey());

            // do not update the ip validation status of updated ips
            // because the checker has the most precise result
            // Only when ip is not marked, don't we update the health status of IP:
            if (!ip.isMarked()) {
                ip.setHealthy(oldIP.isHealthy());
            }

            if (ip.isHealthy() != oldIP.isHealthy()) {
                // ip validation status updated
                Loggers.EVT_LOG.info("{} {SYNC} IP-{} {}:{}@{}", getService().getName(),
                                     (ip.isHealthy() ? "ENABLED" : "DISABLED"), ip.getIp(), ip.getPort(), getName());
            }

            if (ip.getWeight() != oldIP.getWeight()) {
                // ip validation status updated
                Loggers.EVT_LOG.info("{} {SYNC} {IP-UPDATED} {}->{}", getService().getName(), oldIP.toString(),
                                     ip.toString());
            }
        }
    }

    List<Instance> newIPs = subtract(ips, oldIpMap.values());
    if (newIPs.size() > 0) {
        Loggers.EVT_LOG
            .info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
                  getName(), newIPs.size(), newIPs.toString());

        for (Instance ip : newIPs) {
            HealthCheckStatus.reset(ip);
        }
    }

    List<Instance> deadIPs = subtract(oldIpMap.values(), ips);

    if (deadIPs.size() > 0) {
        Loggers.EVT_LOG
            .info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
                  getName(), deadIPs.size(), deadIPs.toString());

        for (Instance ip : deadIPs) {
            HealthCheckStatus.remv(ip);
        }
    }

    toUpdateInstances = new HashSet<>(ips);

    // 最终,微服务的实例会保存在下面这个集合中
    if (ephemeral) {
        ephemeralInstances = toUpdateInstances;
    } else {
        persistentInstances = toUpdateInstances;
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

工作两年后,我如何看待设计模式

在软件工程中&#xff0c;设计模式是经过反复验证的最佳实践&#xff0c;用于解决在软件设计中经常遇到的一类问题。它们为开发者提供了一种通用的解决方案和语言&#xff0c;使得复杂的编程问题得以简化&#xff0c;代码结构更加清晰&#xff0c;可维护性大大提高。简而言之&a…

PostgreSQL 如何优化存储过程的执行效率?

文章目录 一、查询优化1. 正确使用索引2. 避免不必要的全表扫描3. 使用合适的连接方式4. 优化子查询 二、参数传递1. 避免传递大对象2. 参数类型匹配 三、减少数据量处理1. 限制返回结果集2. 提前筛选数据 四、优化逻辑结构1. 分解复杂的存储过程2. 避免过度使用游标 五、事务处…

隐私计算实训营第二期第七课:XGB算法与SGB算法开发实践

隐私计算实训营第二期-第七课 第七课&#xff1a;XGB算法与SGB算法开发实践1 决策树模型1.1 决策树的训练和预测过程1.2 决策树的发展过程 2 GBDT模型2.1 Boosting核心思想2.2 GBDT原理 3 XGB模型3.1 XGB核心思想3.2 XGB优点 3 隐语纵向树模型3.1 数据纵向分割3.2 隐私保护的树…

本地部署到服务器上的资源路径问题

本地部署到服务器上的资源路径问题 服务器端的源代码的静态资源目录层级 当使用Thymeleaf时&#xff0c;在templates的目录下为返回的html页面&#xff0c;下面以两个例子解释当将代码部署到tomcat时访问资源的路径配置问题 例子一 index.html&#xff08;在templates的根目录…

EtherCAT转Profinet网关配置说明第二讲:上位机软件配置

EtherCAT协议转Profinet协议网关模块&#xff08;XD-ECPNS20&#xff09;&#xff0c;不仅可以实现数据之间的通信&#xff0c;还可以实现不同系统之间的数据共享。EtherCAT协议转Profinet协议网关模块&#xff08;XD-ECPNS20&#xff09;具有高速传输的特点&#xff0c;因此通…

安卓安全概述

安卓安全概述 1.Android系统概述2.Android系统安全概述3.Android系统的安全机制应用程序框架安全机制内核安全机制运行环境安全机制 4.Android反编译工具 1.Android系统概述 Android采用层次化系统架构&#xff0c;Google官方公布的标准架构如图所示&#xff0c;自顶而下划分为…

vue事件处理v-on或@

事件处理v-on或 我们可以使用v-on指令&#xff08;简写&#xff09;来监听DOM事件&#xff0c;并在事件触发时执行对应的Javascript。用法&#xff1a;v-on:click"methodName"或click"hander" 事件处理器的值可以是&#xff1a; 内敛事件处理器&#xff1…

【MindSpore学习打卡】应用实践-自然语言处理-基于RNN的情感分类:使用MindSpore实现IMDB影评分类

情感分类是自然语言处理&#xff08;NLP&#xff09;中的一个经典任务&#xff0c;广泛应用于社交媒体分析、市场调研和客户反馈等领域。本篇博客将带领大家使用MindSpore框架&#xff0c;基于RNN&#xff08;循环神经网络&#xff09;实现一个情感分类模型。我们将详细介绍数据…

【数据结构(邓俊辉)学习笔记】高级搜索树01——伸展树

文章目录 1. 逐层伸展1. 1 宽松平衡1. 2 局部性1. 3 自适应调整1. 4 逐层伸展1. 5 实例1. 6 一步一步往上爬1. 7 最坏情况 2. 双层伸展2.1 双层伸展2.2 子孙异侧2.3 子孙同侧2.4 点睛之笔2.5 折叠效果2.6 分摊性能2.7 最后一步 3 算法实现3.1 功能接口3.2 伸展算法3.3 四种情况…

uniapp H5页面设置跨域请求

记录一下本地服务在uniapp H5页面访问请求报跨域的错误 这是我在本地起的服务端口号为8088 ip大家可打开cmd 输入ipconfig 查看 第一种方法 在源码视图中配置 "devServer": {"https": false, // 是否启用 https 协议&#xff0c;默认false"port&q…

vb.netcad二开自学笔记5:ActiveX链接CAD的.net写法

一、必不可少的对象引用 使用activex需要在项目属性中勾选以下两个引用&#xff0c;若找不到&#xff0c;则浏览定位直接添加下面两个文件&#xff0c;可以看到位于cad的安装路径下&#xff0c;图中的3个mgd.dll也可以勾选。 C:\Program Files\Autodesk\AutoCAD 2024\Autodes…

(数据大屏)(Hadoop)基于SSM框架的学院校友管理系统的设计与实现+文档

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

Java线上接口耗时分析神器 Arthas

介绍 程序员的日常&#xff0c;总是离不开“调优”和“排查”。尤其当线上环境出现问题&#xff0c;性能瓶颈把人逼疯。这时候&#xff0c;你就需要一款像 Arthas 这样的神器来救场。 什么是 Arthas&#xff1f; 简单来说&#xff0c;Arthas 是阿里巴巴开源的 Java 诊断工具…

前端八股文 对$nextTick的理解

$nexttick是什么? 获取更新后的dom内容 为什么会有$nexttick ? vue的异步更新策略 (这也是vue的优化之一 要不然一修改数据就更新dom 会造成大量的dom更新 浪费性能) 这是因为 message &#xff08;data&#xff09;数据在发现变化的时候&#xff0c;vue 并不会立刻去更…

学习笔记——动态路由——IS-IS中间系统到中间系统(区域划分)

三、IS-IS区域划分 根据IS-IS路由器邻居关系&#xff0c;可以将IS-IS划分为两个区域——骨干区域和非骨干区域。&#xff08;注意&#xff0c;这里的区域不是上文中提到的Area ID&#xff09;由L2的IS-IS邻居构成的区域为骨干区域&#xff0c;由L1的IS-IS邻居构成的区域为非骨…

c与c++的内存管理

给出内存四个分区名字&#xff1a;栈区、堆区、全局区&#xff08;俗话也叫静态变量区&#xff09;、代码区&#xff08;也叫代码段&#xff09;&#xff08;代码段又分很多种&#xff0c;比如常量区&#xff09; 当然也会看到别的定义如&#xff1a; 两者都正确&#xff0c;记…

Adobe Acrobat添加时间戳服务器

文章目录 前言一、Adobe Acrobat添加时间戳服务器1.打开Adobe Acrobat软件2.点击【菜单】→ 【首选项】3.点击【安全性】→【更多】4.点击【新建】5.输入【名称】→【服务器URL】 前言 一、Adobe Acrobat添加时间戳服务器 1.打开Adobe Acrobat软件 2.点击【菜单】→ 【首选项…

广州佛山中山数据中心机房搬迁公司

随着数据中心的发展和迭代&#xff0c;必然面临数据中心搬迁。数据中心搬迁听来简单&#xff0c;其实涉及诸多方面&#xff0c;如信息迁移的安全性、业务的连续性、搬迁的规范性、方案的可行性、组织的统一性等。友力科技&#xff08;广州&#xff09;有限公司&#xff0c;自原…

IT专业入门,高考假期预习指南—初识产品经理BRD、MRD 和 PRD

七月来临&#xff0c;各省高考分数已揭榜完成。而高考的完结并不意味着学习的结束&#xff0c;而是新旅程的开始。对于有志于踏入IT领域的高考少年们&#xff0c;这个假期是开启探索IT世界的绝佳时机。作为该领域的前行者和经验前辈&#xff0c;你是否愿意为准新生们提供一份全…

Python + OpenCV 开启图片、写入储存图片

这篇教学会介绍OpenCV 里imread()、imshow()、waitKey() 方法&#xff0c;透过这些方法&#xff0c;在电脑中使用不同的色彩模式开启图片并显示图片。 imread() 开启图片 使用imread() 方法&#xff0c;可以开启图片&#xff0c;imread() 有两个参数&#xff0c;第一个参数为档…