Dubbo 3.x源码(16)—Dubbo服务发布导出源码(5)

news2024/11/16 7:41:22

基于Dubbo 3.1,详细介绍了Dubbo服务的发布与引用的源码。

此前我们学习了Dubbo 3.x源码(15)—Dubbo服务发布导出源码(4),也就是Dubbo远程服务导出export方法的上半部分,也就是doLocalExport源码,将会得到一个Exporter。

现在我们继续学习,在导出远程服务得到Exporter之后,继续通过Registry将其注册到远程注册中心的源码。

  1. Dubbo 3.x源码(11)—Dubbo服务的发布与引用的入口
  2. Dubbo 3.x源码(12)—Dubbo服务发布导出源码(1)
  3. Dubbo 3.x源码(13)—Dubbo服务发布导出源码(2)
  4. Dubbo 3.x源码(14)—Dubbo服务发布导出源码(3)
  5. Dubbo 3.x源码(15)—Dubbo服务发布导出源码(4)
  6. Dubbo 3.x源码(16)—Dubbo服务发布导出源码(5)

文章目录

  • 1 register注册服务到注册中心
  • 2 getRegistry获取注册表
    • 2.1 AbstractRegistryFactory#getRegistry获取注册表
      • 2.1.1 createRegistry创建注册表
    • 2.2 ServiceDiscoveryRegistry的构建
  • 3 register注册服务
    • 3.1 ServiceDiscoveryRegistry应用级服务注册
    • 3.2 ZookeeperRegistry接口级服务注册
  • 4 总结

1 register注册服务到注册中心

此前我们学习过,在远程服务导出协议(RegistryProtocol、InterfaceCompatibleRegistryProtocol)的export方法中,在本地导出服务得到Exporter之后,还需要将其注册到远程注册中心,这样consumer端才能从注册中心获取到服务的相关信息。

// url to registry
/*
 * 基于Dubbo SPI机制根据注册中心url加载具体的注册中心操作类,service-discovery-registry对应着ServiceDiscoveryRegistry
 */
final Registry registry = getRegistry(registryUrl);
//获取需要注册的服务url 内部包含服务的ip、port、pid、服务接口、版本、分组、接口内部的方法名等信息
//dubbo://192.168.31.84:20880/org.apache.dubbo.demo.GreetingService?anyhost=true&application=demo-provider&application.version=1&background=false&delay=5000&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=greeting&interface=org.apache.dubbo.demo.GreetingService&methods=hello&pid=10920&revision=1.0.0&service-name-mapping=true&side=provider&timeout=5000&timestamp=1666621305141&version=1.0.0
final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

// decide if we need to delay publish (provider itself and registry should both need to register)
//决定我们是否需要延迟发布(提供者本身和注册中心都需要注册)
boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
/*
 * 2 向注册中心注册服务
 */
if (register) {
    register(registry, registeredProviderUrl);
}

2 getRegistry获取注册表

Registry是服务注册中心操作类,Registry同样是基于Dubbo SPI机制根据协议查找的,例如service-discovery-registry服务发现协议对应着ServiceDiscoveryRegistry,而registry服务发现协议由于getRegistryUrl方法中就被替换为了真实的协议地址,因此registryUrl可能对应着不同协议,因此Registry的返回也各不相同,例如nacos协议对应着NacosRegistry,zookeeper协议对应着ZookeeperRegistry。

/**
 * 根据调用者的地址获取注册表的实例
 *
 * @param registryUrl 注册中心url
 * @return 注册表
 */
protected Registry getRegistry(final URL registryUrl) {
    //获取RegistryFactory的自适应实现 RegistryFactory$Adaptive
    RegistryFactory registryFactory = ScopeModelUtil.getExtensionLoader(RegistryFactory.class, registryUrl.getScopeModel()).getAdaptiveExtension();
    //根据url的协议获取对应的RegistryFactory实现,然后调用RegistryFactory实现的getRegistry方法获取注册表
    return registryFactory.getRegistry(registryUrl);
}

2.1 AbstractRegistryFactory#getRegistry获取注册表

该方法是获取注册表的模版方法,属于AbstractRegistryFactory,该方法中,将会根据url获取注册表缓存key,例如service-discovery-registry://47.94.229.245:2181/org.apache.dubbo.registry.RegistryService,或者zookeeper://47.94.229.245:2181/org.apache.dubbo.registry.RegistryService,然后尝试从registries缓存中根据key获取注册表实例,如果找打就直接返回,否则调用createRegistry方法根据url创建一个注册中心,最后将创建的注册中心存入缓存并返回。

/**
 * AbstractRegistryFactory的方法
 * <p>
 * 获取注册表的模版方法
 *
 * @param url 注册中心url
 */
@Override
public Registry getRegistry(URL url) {
    if (registryManager == null) {
        throw new IllegalStateException("Unable to fetch RegistryManager from ApplicationModel BeanFactory. " +
            "Please check if `setApplicationModel` has been override.");
    }
    //应用销毁状态的判断
    Registry defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed();
    if (null != defaultNopRegistry) {
        return defaultNopRegistry;
    }
    //添加interface参数值为RegistryService全路径名、移除timestamp参数、移除export、refer属性
    url = URLBuilder.from(url)
        .setPath(RegistryService.class.getName())
        .addParameter(INTERFACE_KEY, RegistryService.class.getName())
        .removeParameter(TIMESTAMP_KEY)
        .removeAttribute(EXPORT_KEY)
        .removeAttribute(REFER_KEY)
        .build();
    //注册表缓存key,例如service-discovery-registry://47.94.229.245:2181/org.apache.dubbo.registry.RegistryService
    String key = createRegistryCacheKey(url);
    Registry registry = null;
    boolean check = url.getParameter(CHECK_KEY, true) && url.getPort() != 0;

    // Lock the registry access process to ensure a single instance of the registry  锁定注册表访问过程,以确保注册表的单个实例
    registryManager.getRegistryLock().lock();
    try {
        // double check
        // fix https://github.com/apache/dubbo/issues/7265.
        defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed();
        if (null != defaultNopRegistry) {
            return defaultNopRegistry;
        }
        //从registries缓存中根据key获取注册表实例
        registry = registryManager.getRegistry(key);
        //如果有就直接返回
        if (registry != null) {
            return registry;
        }

        // create registry by spi/ioc
        //基于url创建注册中心
        registry = createRegistry(url);
        if (check && registry == null) {
            throw new IllegalStateException("Can not create registry " + url);
        }
        //存入缓存
        if (registry != null) {
            registryManager.putRegistry(key, registry);
        }
    } catch (Exception e) {
        if (check) {
            throw new RuntimeException("Can not create registry " + url, e);
        } else {
            // 1-11 Failed to obtain or create registry (service) object.
            LOGGER.warn("1-11", "", "",
                "Failed to obtain or create registry ", e);
        }
    } finally {
        // Release the lock 解锁
        registryManager.getRegistryLock().unlock();
    }
    //返回注册表
    return registry;
}

2.1.1 createRegistry创建注册表

在应用级服务注册表ServiceDiscoveryRegistry的创建过程中,此时才会将service-discovery-registry服务发现协议替换为真实的注册中心协议,然后创建一个ServiceDiscoveryRegistry返回。

public class ServiceDiscoveryRegistryFactory extends AbstractRegistryFactory {

    @Override
    protected Registry createRegistry(URL url) {
        //初始的url:service-discovery-registry://xxx.xxx.xxx.xxx:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registry1&application=demo-provider&application.version=1&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=42244&registry=zookeeper&timeout=20001
        if (UrlUtils.hasServiceDiscoveryRegistryProtocol(url)) {
            //获取url的registry参数值,默认dubbo
            String protocol = url.getParameter(REGISTRY_KEY, DEFAULT_REGISTRY);
            //将服务发现协议替换为真实的注册中心协议,并且删除url的registry参数
            url = url.setProtocol(protocol).removeParameter(REGISTRY_KEY);
        }
        //替换后的url zookeeper://xxx.xxx.xxx.xxx:2181/org.apache.dubbo.registry.RegistryService?REGISTRY_CLUSTER=registry1&application=demo-provider&application.version=1&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=41479&timeout=20001
        return new ServiceDiscoveryRegistry(url, applicationModel);
    }

}

对于zookeeper注册中心来说,则是直接创建一个ZookeeperRegistry返回,内部包含了一个zkClient客户端。

@Override
public Registry createRegistry(URL url) {
    return new ZookeeperRegistry(url, zookeeperTransporter);
}

常见注册表的关系如下:
image.png

2.2 ServiceDiscoveryRegistry的构建

ServiceDiscoveryRegistry的构造器中:

  1. 首先会调用父类FailbackRegistry的构造器。
  2. 然后根据url中的注册中心协议构建一个ServiceDiscovery,例如ZookeeperServiceDiscovery,其包含服务发现的通用操作。
  3. 最后获取服务名映射 MetadataServiceNameMapping,可通过服务接口名查找到对应的服务应用的名字。
public ServiceDiscoveryRegistry(URL registryURL, ApplicationModel applicationModel) {
    //父类构造器
    super(registryURL);
    //根据url中的注册中心协议构建一个ServiceDiscovery,例如ZookeeperServiceDiscovery,其包含服务发现的通用操作
    this.serviceDiscovery = createServiceDiscovery(registryURL);
    //获取服务名映射 MetadataServiceNameMapping,可通过服务接口名查找到对应的服务应用的名字
    this.serviceNameMapping = (AbstractServiceNameMapping) ServiceNameMapping.getDefaultExtension(registryURL.getScopeModel());
    super.applicationModel = applicationModel;
}

FailbackRegistry的构造器如下:

  1. 首先会调用父类AbstractRegistry的构造器。
  2. 然后获取重试时间间隔,取url参数retry.period,默认5000ms。
  3. 最后创建一个用于失败重试的时间轮。因为重试任务不会非常多,一圈128个刻度就足够了。
public FailbackRegistry(URL url) {
    //调用父类AbstractRegistry构造器
    super(url);
    //重试时间间隔,取url参数retry.period,默认5000ms
    this.retryPeriod = url.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD);

    // since the retry task will not be very much. 128 ticks is enough.
    //用于失败重试的时间轮。因为重试任务不会非常多,一圈128个刻度就足够了。
    retryTimer = new HashedWheelTimer(new NamedThreadFactory("DubboRegistryRetryTimer", true), retryPeriod, TimeUnit.MILLISECONDS, 128);
}

AbstractRegistry的构造器如下,主要是本地缓存文件的支持以及加载本地缓存。

protected AbstractRegistry(URL url) {
    //设置registryUrl属性
    setUrl(url);
    //获取注册表管理器
    registryManager = url.getOrDefaultApplicationModel().getBeanFactory().getBean(RegistryManager.class);
    //获取url参数file.cache,是否支持本地缓存,默认true
    localCacheEnabled = url.getParameter(REGISTRY_LOCAL_FILE_CACHE_ENABLED, true);
    //获取注册表缓存执行器sharedScheduledExecutor
    registryCacheExecutor = url.getOrDefaultFrameworkModel().getBeanFactory()
        .getBean(FrameworkExecutorRepository.class).getSharedScheduledExecutor();
    //如果支持本地缓存
    if (localCacheEnabled) {
        // Start file save timer
        //获取url参数save.file,是否同步存储,默认false
        syncSaveFile = url.getParameter(REGISTRY_FILESAVE_SYNC_KEY, false);
        //注册中心缓存文件名 /{user.home}/.dubbo/dubbo-registry-{dubbo.application.name}-{ip}-{post}.cache
        String defaultFilename = System.getProperty(USER_HOME) + DUBBO_REGISTRY + url.getApplication() +
            "-" + url.getAddress().replaceAll(":", "-") + CACHE;
        //获取url参数file,指定的文件名,默认defaultFilename
        String filename = url.getParameter(FILE_KEY, defaultFilename);
        File file = null;

        if (ConfigUtils.isNotEmpty(filename)) {
            //创建文件对象
            file = new File(filename);
            //不存在则创建
            if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists()) {
                if (!file.getParentFile().mkdirs()) {

                    IllegalArgumentException illegalArgumentException = new IllegalArgumentException(
                        "Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");

                    if (logger != null) {
                        // 1-9 failed to read / save registry cache file.

                        logger.error("1-9", "cache directory inaccessible",
                            "Try adjusting permission of the directory.",
                            "failed to create directory", illegalArgumentException);
                    }

                    throw illegalArgumentException;
                }
            }
        }

        this.file = file;

        // When starting the subscription center,
        // we need to read the local cache file for future Registry fault tolerance processing.
        //加载缓存文件内容到properties集合中,方便注册表容错管理
        loadProperties();
        //推送变更
        notify(url.getBackupUrls());
    }
}

3 register注册服务

该方法内部实际上是调用registry的register方法完成注册。参数url是注册的服务提供者url,内部包含服务提供者的协议、ip、port、pid、服务接口、版本、分组、接口内部的方法名等信息。

/**
 * RegistryProtocol的方法
 * <p>
 * 向注册中心注册服务
 *
 * @param registry              服务注册中心操作类,例如service-discovery-registry对应着ServiceDiscoveryRegistry
 * @param registeredProviderUrl 注册的服务提供者url, 内部包含服务的ip、port、pid、服务接口、版本、分组、接口内部的方法名等信息
 */
private void register(Registry registry, URL registeredProviderUrl) {
    //调用registry的register方法完成注册
    registry.register(registeredProviderUrl);
}

我们下面来看看接口级和应用级别的服务注册的源码,假设真实注册中心都是zookeeper,此时接口级服务注册使用的真实注册表ZookeeperRegistry,而应用级服务注册使用的是ServiceDiscoveryRegistry,但是url已经变成了真实zookeeper协议的url。

3.1 ServiceDiscoveryRegistry应用级服务注册

ServiceDiscoveryRegistry内部最终是调用ServiceDiscovery的register方法完成注册,我们前面就说过,serviceDiscovery中包含服务发现的通用操作。假设注册中心协议为zookeeper,那么serviceDiscovery就是ZookeeperServiceDiscovery。

@Override
public final void register(URL url) {
    //只注册提供者
    if (!shouldRegister(url)) { // Should Not Register
        return;
    }
    //执行注册
    doRegister(url);
}

@Override
public void doRegister(URL url) {
    // fixme, add registry-cluster is not necessary anymore
    url = addRegistryClusterKey(url);
    //调用serviceDiscovery的register方法完成注册
    serviceDiscovery.register(url);
}

ZookeeperServiceDiscovery的register方法实际上是AbstractServiceDiscovery的register方法实现,内部调用metadataInfo的addService方法。

addService方法会根据服务提供者url创建ServiceInfo并添加到services集合中,随后将url加入到exportedServiceURLs缓存中,最后将更新标识位updated改为true。

/**
 * AbstractServiceDiscovery的方法
 *
 * @param url  服务提供者url
 */
@Override
public void register(URL url) {
    //根据url添加MetadataInfo
    metadataInfo.addService(url);
}

/**
 * MetadataInfo的方法
 * @param url 服务提供者url
 */
public synchronized void addService(URL url) {
    // fixme, pass in application mode context during initialization of MetadataInfo.
    if (this.loader == null) {
        this.loader = url.getOrDefaultApplicationModel().getExtensionLoader(MetadataParamsFilter.class);
    }
    //元数据参数过滤器
    List<MetadataParamsFilter> filters = loader.getActivateExtension(url, "params-filter");
    // generate service level metadata
    //生成服务级别元数据
    ServiceInfo serviceInfo = new ServiceInfo(url, filters);
    //存入services缓存,mathKey例如:greeting/org.apache.dubbo.demo.GreetingService:1.0.0:dubbo  value是serviceInfo
    this.services.put(serviceInfo.getMatchKey(), serviceInfo);
    // extract common instance level params
    extractInstanceParams(url, filters);
    //初始化exportedServiceURLs
    if (exportedServiceURLs == null) {
        exportedServiceURLs = new ConcurrentSkipListMap<>();
    }
    //加入到exportedServiceURLs缓存,  key例如:greeting/org.apache.dubbo.demo.GreetingService:1.0.0  value是url的SortedSet<URL>集合
    addURL(exportedServiceURLs, url);
    //设置更新标志为true
    updated = true;
}

注意,上面的方法仅仅是将数据发布到内存中,还没有真正的注册到注册中心。那么,应用级服务注册信息什么时候真正的同步到注册中心的呢?

实际上在DefaultModuleDeployer#startSync方法中,在经过了exportServices服务导出和referServices服务引用之后的onModuleStarted方法中,此时才会进行应用级服务数据的真正注册。

我们这里直接来看看注册后在zookeeper上的数据,可以看到节点路径是应用级数据services/{applicationName}/{ip:port},每一个应用实例才会注册一个节点,节点类型是临时节点,节点内包括服务名称、id、地址、端口、端口、有效载荷(包含存储元数据)等信息。

image.png

3.2 ZookeeperRegistry接口级服务注册

传统的接口级别服务注册使用注册中心协议本身对应的注册表来实现,例如zookeeper协议将会使用ZookeeperRegistry,而他的register方法实际上调用的父类FailbackRegistry实现的register方法。

FailbackRegistry#register方法首先将url加入到registered缓存中,然后从注册失败的缓存failedRegistered中和移除注册失败的缓存failedUnregistered中移除该url。核心方法是调用doRegister方法,向服务器端发送注册请求,该方法由具体的子类实现。

/**
 * FailbackRegistry的方法
 * 
 * 注册服务提供者url
 */
@Override
public void register(URL url) {
    if (!acceptable(url)) {
        logger.info("URL " + url + " will not be registered to Registry. Registry " + this.getUrl() + " does not accept service of this protocol type.");
        return;
    }
    //调用父类AbstractRegistry的register方法,将url加入到registered缓存中
    super.register(url);
    //从注册失败的缓存failedRegistered中移除该url
    removeFailedRegistered(url);
    //从移除注册失败的缓存failedUnregistered中移除该url
    removeFailedUnregistered(url);
    try {
        // Sending a registration request to the server side
        /*
         * 向服务器端发送注册请求,该方法由具体的子类实现
         */
        doRegister(url);
    } catch (Exception e) {
        Throwable t = e;

        // If the startup detection is opened, the Exception is thrown directly.
        boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
            && url.getParameter(Constants.CHECK_KEY, true)
            && (url.getPort() != 0);
        boolean skipFailback = t instanceof SkipFailbackWrapperException;
        if (check || skipFailback) {
            if (skipFailback) {
                t = t.getCause();
            }
            throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
        } else {
            logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
        }

        // Record a failed registration request to a failed list, retry regularly
        addFailedRegistered(url);
    }
}

我们来看看ZookeeperRegistry的doRegister方法实现,根据url构建节点路径,/dubbo/{servicePath}/providers/{urlString},例如: /dubbo/org.apache.dubbo.demo.TripleService/providers/tri%3A%2F%2F192.168.1.7%3A50051%2Forg.apache.dubbo.demo.TripleService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26application.version%3D1%26background%3Dfalse%26delay%3D5000%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.TripleService%26methods%3Dhello%26pid%3D2624%26revision%3D1.0.0%26service-name-mapping%3Dtrue%26side%3Dprovider%26timeout%3D5000%26timestamp%3D1666705770994%26version%3D1.0.0,节点的值是服务提供者的节点ip。

注意此节点是一个临时节点,当服务关闭时节点删除,值是服务提供者的节点ip。

/**
 * ZookeeperRegistry的方法
 *
 * 注册接口级别的服务提供者url到zookeeper
 * @param url 服务提供者url
 */
@Override
public void doRegister(URL url) {
    try {
        checkDestroyed();
        //根据url构建节点路径,/dubbo/{servicePath}/providers/{url}
        //例如: /dubbo/org.apache.dubbo.demo.TripleService/providers/tri%3A%2F%2F192.168.1.7%3A50051%2Forg.apache.dubbo.demo.TripleService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26application.version%3D1%26background%3Dfalse%26delay%3D5000%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.TripleService%26methods%3Dhello%26pid%3D2624%26revision%3D1.0.0%26service-name-mapping%3Dtrue%26side%3Dprovider%26timeout%3D5000%26timestamp%3D1666705770994%26version%3D1.0.0
        //注意此节点是一个临时节点
        zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true));
    } catch (Throwable e) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
    }
}

对于接口级的服务注册,在zookeeper中的节点样式如下。

image.png

每个服务的每个接口都有一个单独的url节点,这些url一般来说除了ip和端口不同,其他配置都是相同的,因此存在大量的数据冗余,增加了注册中心的压力,而dubbo3引入的应用级服务注册发现,其目的之一就是解决接口级的服务注册发现的数据冗余问题,后面会讲到。

4 总结

本次我们学习了在导出远程服务得到Exporter之后,继续通过Registry将其注册到远程注册中心的源码,大概逻辑:

  1. 基于Dubbo SPI机制根据注册中心url加载具体的注册中心操作类Registry,应用级服务导出协议service-discovery-registry对应着ServiceDiscoveryRegistry,接口级服务导出协议则会获取真实注册中心协议对应的Registry。
  2. 通过调用Registry#register方法向远程注册中心注册服务提供者url。对于接口级服务导出协议会直接注册到注册中心,而对于应用级服务导出协议则仅仅是存入到本地内存中,在后面才会将服务信息真正的注册。

到这里,我们的Dubbo服务导出过程中的Protocol#export方法的源码就基本学习完毕了(还差一个 exported方法),还记得之前学的内容吗?这个方法实际上是在ServiceConfig#doExportUrl方法中调用的哦!

之前的内容忘了也没关系,下一章我们将会学习最后的exported()方法,并且会对Dubbo服务导出的整体源码流程进行一个比较全面的总结。

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

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

相关文章

LLM之RAG理论(七)| 高提升RAG检索的四种方法

​ RAG两大核心组件&#xff1a;检索和生成。本文将总结四种提高RAG系统检索质量的技术&#xff1a;1&#xff09;子问题查询引擎&#xff08;来自LlamaIndex&#xff09;&#xff0c;2&#xff09;RAG-Fusion、3&#xff09;RAG-end2end和4&#xff09;LoRA微调。 一、L…

司铭宇老师:门店经理培训:如何成为一位卓越的门店经理

门店经理培训&#xff1a;如何成为一位卓越的门店经理 在激烈的市场竞争中&#xff0c;门店经理作为门店的灵魂人物&#xff0c;肩负着提升门店业绩、维护品牌形象、带领团队成长等重要职责。本文将为您解析如何成为一位卓越的门店经理&#xff0c;助力您的职业生涯迈向新高峰…

5个程序员可以接私活的平台和一些建议

22年之前我从没有接触过程序员外包接单&#xff0c;也没有任何的私活接单经验&#xff0c;就纯纯看自己瞎摸索&#xff0c;通过Google搜索&#xff0c;在各类程序员私活接单平台上摸爬滚打&#xff0c;硬是杀出一条血路&#xff0c;从一开始的年入3k到现在每月稳定收入1w&#…

【排序算法】C语言实现归并排序,包括递归和迭代两个版本

文章目录 &#x1f680;前言&#x1f680;归并排序介绍及其思想&#x1f680;递归实现&#x1f680;迭代实现 &#x1f680;前言 大家好啊&#xff01;阿辉接着更新排序算法&#xff0c;今天要讲的是归并排序&#xff0c;这里阿辉将讲到归并排序的递归实现和迭代实现&#xff…

AI服务器行业分析:预计2023年全球市场规模将达211亿美元

AI服务器需求暴增&#xff0c;机构指出&#xff0c;AI时代浪潮汹涌&#xff0c;海量数据催生庞大的算力需求&#xff0c;带动AI服务器需求量与日俱增&#xff0c;用于服务器内、外部数据传输等接口芯片也随之攀升。 ChatGPT大火后&#xff0c;各大科技企业纷纷发力AI大模型&…

Vue的生命周期方法

beforeCreate 在实例初始化之后&#xff0c;数据观测&#xff08;data observe&#xff09;和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。 created 实例已经创建完成之后被调用。在这一步&#xff0c;实…

坚持刷题 |对称二叉树

文章目录 题目考察点代码实现实现总结扩展用迭代的方式判断是否为对称二叉树递归和迭代的对比可能的扩展提问 坚持刷题&#xff0c;老年痴呆追不上我&#xff0c;今天真的好累&#xff0c;就不难为自己了&#xff0c;刷个简单级别的吧&#xff1a;对称二叉树 题目 101.对称二叉…

大数据开发之SparkSQL

第 1 章&#xff1a;spark sql概述 1.1 什么是spark sql 1、spark sql是spark用于结构化数据处理的spark模块 1&#xff09;半结构化数据&#xff08;日志数据&#xff09; 2&#xff09;结构化数据&#xff08;数据库数据&#xff09; 1.2 为什么要有sparksql hive on s…

如何开发一款独立游戏?

如何开发一款独立游戏&#xff1f; 2023年&#xff0c;Steam平台共发行了14533款游戏&#xff0c;我们可以清晰地看到独立游戏市场的蓬勃发展和不断增长的活力。我们从SteamDB数据统计网站获悉&#xff0c;2023年Steam平台的游戏发行数量比2022年的12562款增加了1971款&#xf…

理想汽车大模型算法工程师面试,被问的瑟瑟发抖。。。。

最近我们技术群的一位小伙伴&#xff0c;分享了他面试理想汽车大模型算法工程师的经历与经验。 今天整理后分享给大家&#xff0c;如果你对这块感兴趣&#xff0c;可以文末加入我们的技术&面试讨论群 一面&#xff08;1.5h&#xff0c;感觉有点难&#xff09; 自我介绍&…

推荐收藏!48道数据分析师高频面试题汇总!

大家好&#xff0c;最近很多小伙伴私信我&#xff0c;讲一下数据分析的面试题&#xff0c;今天给大家整理了48道数据分析师面试时被频繁问到的题目&#xff0c;找数据分析岗位的同学一定要码住认真看。 想了解最新的面试动态、最新高频考点、技术交流的同学&#xff0c;可以文…

Windows和Linux访问不了GitHub的解决方法

一、Windows访问不了GitHub 问题描述 使用Windows访问GitHub时&#xff0c;出现如下情况&#xff0c;显示无法访问。 解决方案&#xff1a; 打开域名查询网站&#xff1a;https://tool.chinaz.com/dns 输入GitHub的域名&#xff0c;点击立即检测。 出现如下页面&#xff0c…

RK3399平台开发系列讲解(USB篇)BusHound 工具使用介绍

🚀返回专栏总目录 文章目录 一、BusHound简介二、BusHound的下载三、BusHound设备窗口四、BUSHound发送命令窗口沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 BusHound软件是由美国perisoft公司研制的一种专用于PC机各种总线数据包监视和控制的开发工具软件,其名…

2024年学鸿蒙开发有前途吗?

随着科技的不断发展和智能设备的普及&#xff0c;鸿蒙系统作为华为自主研发的操作系统&#xff0c;正逐渐受到市场的关注。2024年&#xff0c;学鸿蒙开发是否有前途&#xff0c;成为了很多开发者和学生关心的问题。本文将从多个角度分析鸿蒙系统的发展前景&#xff0c;以及学习…

JavaScript入门分享

文章目录 一、JavaScript简介二、第一个JavaScript案例三、在浏览器中执行JavaScript代码四、JavaScript的输出方法五、JavaScript的语法六、JavaScript的数据类型七、JavaScript的定义变量/函数八、热门文章 一、JavaScript简介 JavaScript是一种高级编程语言&#xff0c;用于…

esxi配置NTP自动对时与手动对时

目录 背景解法配置NTP服务器立即与NTP服务器同步时间 附&#xff1a;几个常用的NTP服务器列表 背景 VMware ESXi 6.7运行了一段时间后偶然发现系统时间与标准时间有5分钟左右的差异&#xff0c;于是研究了下如何自动对时以及用命令行立即对时。 解法 配置NTP服务器 首先在管…

手把手教你使用MDK仿真调试

当今的嵌入式系统开发领域中&#xff0c;高效的调试工具对于工程师来说至关重要。它们能够极大地减少开发周期中的错误追踪时间&#xff0c;并加速产品的上市时间。MDK作为业界领先的嵌入式开发工具之一&#xff0c;其内置的调试功能被广大开发者所赞誉。这些功能不仅提供了对代…

PostgreSQL 是不是大小写敏感

如果你踩过 MySQL 的大坑的话就知道&#xff1a;MySQL 在 Windows 下不区分大小写&#xff0c;但在 Linux 下默认是区分大小写。 如果你稍加不注意就会出现在本机开发的程序运行一切正常&#xff0c;发布到服务器行就出现表名找不到的问题。 这是我们前一个项目遇到的巨大问题…

Python中函数的参数有哪些?

目录 ①必选参数 ②默认参数 ③可变参数 ④关键字参数 ⑤命名关键字参数 学习python函数部分的时候&#xff0c;发现除了正常定义的必选参数外&#xff0c;还使用默认参数、可变参数、关键字参数和命名关键字参数&#xff0c;在这里理清楚这些参数具体是怎么回事吧&#x…

nvm 配置淘宝镜像失效,以及安装node后 npm-v 无效

win11 nvm版本 1.1.4 和1.1.7和1.1.12&#xff08;目前最新版本24年 一月二十三日&#xff09; 以上nvm版本都会出现一下问题&#xff0c; 从https://github.com/coreybutler/nvm-windows/releases 下载nvm安装包如下图 傻瓜式安装后&#xff0c;不用去配置环境变量&#…