【Nacos源码系列】Nacos服务发现的原理

news2024/10/5 12:54:49

文章目录

  • 服务发现是什么
  • 客户端服务发现
  • 服务端发现
  • 总结

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

上篇文章介绍了 Nacos服务注册的原理 ,本篇文章将从客户端和服务端的角度介绍Nacos服务发现的原理。

服务发现是什么

服务发现是一种机制,用于在分布式系统中动态地查找和识别可用的服务实例。它解决了微服务架构中服务之间的通信和调用的核心问题。

在传统的单体应用中,各个组件之间的通信往往是直接的函数调用或者数据库查询,因为它们都在同一个进程内部。而在微服务架构中,各个服务被拆分成独立的、自治的服务,可能分布在不同的主机或容器中。这就需要一种机制来帮助服务在网络环境下找到彼此。

服务发现机制通过在微服务架构中引入一个独立的服务注册中心,服务实例会将自己的网络地址、端口以及其他标识信息注册到注册中心中。其他服务可以通过查询注册中心来获取所需服务的具体网络位置,从而能够进行跨服务的通信和调用。

Nacos版本如下:

客户端服务端
版本Spring Cloud Alibaba 2021.0.1.01.4.1

客户端服务发现

对于Spring Cloud Alibaba Nacos想查看源头还是要从spring.factories文件开始。
spring.factories

我们来看客户端的服务发现自动配置类 NacosDiscoveryClientConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class })
@AutoConfigureAfter(NacosDiscoveryAutoConfiguration.class)
public class NacosDiscoveryClientConfiguration {

    //Nacos服务发现客户端
	@Bean
	public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
		return new NacosDiscoveryClient(nacosServiceDiscovery);
	}

	//定时任务,监听服务变化
	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled",
			matchIfMissing = true)
	public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager,
			NacosDiscoveryProperties nacosDiscoveryProperties,
			ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider) {
		return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties,
				taskExecutorObjectProvider);
	}

}

NacosDiscoveryClientConfiguration 类注入两个类 NacosDiscoveryClientNacosWatch

NacosWatch 用于监控服务实例的变化,这里先不过多介绍。

NacosDiscoveryClient 用于管理和维护服务实例的列表,以便客户端进行服务调用和负载均衡。它实现了 DiscoveryClient 接口,DiscoveryClient 是Spring Cloud提供的用于服务发现的客户端。

public class NacosDiscoveryClient implements DiscoveryClient {

	private static final Logger log = LoggerFactory.getLogger(NacosDiscoveryClient.class);

	/**
	 * Nacos Discovery Client Description.
	 */
	public static final String DESCRIPTION = "Spring Cloud Nacos Discovery Client";

	//服务发现类
	private NacosServiceDiscovery serviceDiscovery;

	//是否开启容忍失败获取
	@Value("${spring.cloud.nacos.discovery.failure-tolerance-enabled:false}")
	private boolean failureToleranceEnabled;

	public NacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
		this.serviceDiscovery = nacosServiceDiscovery;
	}

	@Override
	public String description() {
		return DESCRIPTION;
	}

    /**
     * 根据serviceId获取某个服务实例的列表
     * @param serviceId
     * @return
     */
	@Override
	public List<ServiceInstance> getInstances(String serviceId) {
		try {
		    //从注册中心获取服务实例列表
			return Optional.of(serviceDiscovery.getInstances(serviceId)).map(instances -> {
                                                //将服务实例放入缓存中
						ServiceCache.setInstances(serviceId, instances);
						return instances;}).get();
		}
		catch (Exception e) {
		    //如果容忍获取失败开启,获取失败后从本地缓存中返回实例列表
			if (failureToleranceEnabled) {
				return ServiceCache.getInstances(serviceId);
			}
			throw new RuntimeException(
					"Can not get hosts from nacos server. serviceId: " + serviceId, e);
		}
	}

    /**
     * 获取所有服务名称
     * @return
     */
	@Override
	public List<String> getServices() {
		try {
			return Optional.of(serviceDiscovery.getServices()).map(services -> {
						ServiceCache.set(services);
						return services;
					}).get();
		}
		catch (Exception e) {
			log.error("get service name from nacos server fail,", e);
            //如果容忍获取失败开启,获取失败后从本地缓存中返回服务名列表
			return failureToleranceEnabled ? ServiceCache.get() : Collections.emptyList();
		}
	}
}

NacosDiscoveryClient 在进行服务调用时,会调用 getInstances() 方法返回该服务的所有实例信息,实际通过 NacosServiceDiscovery#getInstances方法将 Instance 列表 转为 ServiceInstance 列表并返回。

getInstances() 方法中在服务最新实例列表获取成功后会放入 ServiceCache 缓存中,获取失败抛异常后会从 ServiceCache 缓存中获取实例列表。这就保证了当注册中心挂了以后,客户端在一定程度上保证了服务的正常调用。

public final class ServiceCache {

	private ServiceCache() {
	}

	private static List<String> services = Collections.emptyList();
	//key:服务id ,value:服务对应的实例列表
	private static Map<String, List<ServiceInstance>> instancesMap = new ConcurrentHashMap<>();

	public static void setInstances(String serviceId, List<ServiceInstance> instances) {
		instancesMap.put(serviceId, Collections.unmodifiableList(instances));
	}

	public static List<ServiceInstance> getInstances(String serviceId) {
		return Optional.ofNullable(instancesMap.get(serviceId)).orElse(Collections.emptyList());
	}

	public static void set(List<String> newServices) {
		services = Collections.unmodifiableList(newServices);
	}

	public static List<String> get() {
		return services;
	}
}

NacosServiceDiscovery 用于服务的发现,当一个微服务需要调用其他服务时,可以通过NacosServiceDiscovery类从Nacos注册中心中查询目标服务的信息,包括服务名称、IP地址、端口号等信息。

重点看下NacosServiceDiscovery#getInstances方法:

public class NacosServiceDiscovery {

	private NacosDiscoveryProperties discoveryProperties;

	private NacosServiceManager nacosServiceManager;

	public NacosServiceDiscovery(NacosDiscoveryProperties discoveryProperties,
			NacosServiceManager nacosServiceManager) {
		this.discoveryProperties = discoveryProperties;
		this.nacosServiceManager = nacosServiceManager;
	}

	/**
     * 返回某个服务的所有实例信息
	 * Return all instances for the given service.
	 * @param serviceId id of service
	 * @return list of instances
	 * @throws NacosException nacosException
	 */
	public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
		String group = discoveryProperties.getGroup();
		//获取服务的实例列表
		List<Instance> instances = namingService().selectInstances(serviceId, group,
				true);
		//把Instance实例信息转为ServiceInstance信息
		return hostToServiceInstanceList(instances, serviceId);
	}

	/**
     * 返回所有的服务名称
	 * Return the names of all services.
	 * @return list of service names
	 * @throws NacosException nacosException
	 */
	public List<String> getServices() throws NacosException {
		String group = discoveryProperties.getGroup();
		ListView<String> services = namingService().getServicesOfServer(1,
				Integer.MAX_VALUE, group);
		return services.getData();
	}

    //Instance转成ServiceInstance
	public static List<ServiceInstance> hostToServiceInstanceList(
			List<Instance> instances, String serviceId) {
		List<ServiceInstance> result = new ArrayList<>(instances.size());
		for (Instance instance : instances) {
			ServiceInstance serviceInstance = hostToServiceInstance(instance, serviceId);
			if (serviceInstance != null) {
				result.add(serviceInstance);
			}
		}
		return result;
	}

	//Instance转成ServiceInstance
	public static ServiceInstance hostToServiceInstance(Instance instance,
			String serviceId) {
		if (instance == null || !instance.isEnabled() || !instance.isHealthy()) {
			return null;
		}
		NacosServiceInstance nacosServiceInstance = new NacosServiceInstance();
		nacosServiceInstance.setHost(instance.getIp());
		nacosServiceInstance.setPort(instance.getPort());
		nacosServiceInstance.setServiceId(serviceId);

		Map<String, String> metadata = new HashMap<>();
		metadata.put("nacos.instanceId", instance.getInstanceId());
		metadata.put("nacos.weight", instance.getWeight() + "");
		metadata.put("nacos.healthy", instance.isHealthy() + "");
		metadata.put("nacos.cluster", instance.getClusterName() + "");
		if (instance.getMetadata() != null) {
			metadata.putAll(instance.getMetadata());
		}
		metadata.put("nacos.ephemeral", String.valueOf(instance.isEphemeral()));
		nacosServiceInstance.setMetadata(metadata);

		if (metadata.containsKey("secure")) {
			boolean secure = Boolean.parseBoolean(metadata.get("secure"));
			nacosServiceInstance.setSecure(secure);
		}
		return nacosServiceInstance;
	}

	private NamingService namingService() {
		return nacosServiceManager
				.getNamingService(discoveryProperties.getNacosProperties());
	}

}

当进行服务调用的时候,NacosServiceDiscovery#getInstances() 会从注册中心获取被调用服务的所有实例信息,获取到实例信息之后会通过hostToServiceInstanceList()方法把实例信息转为 ServiceInstance 列表,ServiceInstance类会存放服务实例的IP、端口号、元数据等信息。

ServiceInstance

getInstances()方法中可以看到,它是调用的NamingService#selectInstances()方法来获取实例信息的,接下来下NamingService#selectInstances()方法的实现:

private HostReactor hostReactor;

@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,boolean subscribe) throws NacosException {

    ServiceInfo serviceInfo;
    //如果是订阅方式,从本地缓存获取服务
    if (subscribe) {
        serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                StringUtils.join(clusters, ","));
    } else {
        //直接从服务端拉取服务实例列表
        serviceInfo = hostReactor
                .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                        StringUtils.join(clusters, ","));
    }
    return selectInstances(serviceInfo, healthy);
}

private List<Instance> selectInstances(ServiceInfo serviceInfo, boolean healthy) {
    List<Instance> list;
    if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
        return new ArrayList<Instance>();
    }

    Iterator<Instance> iterator = list.iterator();
    while (iterator.hasNext()) {
        Instance instance = iterator.next();
        //移除健康状况 != healthy的,或者不接受请求的,或者权重<= 0的
        if (healthy != instance.isHealthy() || !instance.isEnabled() || instance.getWeight() <= 0) {
            iterator.remove();
        }
    }
    return list;
}

第一个selectInstances(String, String, List<String>, boolean,boolean)方法会根据 subscribe 判断是否订阅了调用的服务,如果是则从本地缓存中获取服务信息,否则从服务端直接获取服务信息,通常情况下 subscribe 都为true。然后通过它的重载方法selectInstances(ServiceInfo, boolean)筛选出可用实例,并返回。

接下来主要就是看HostReactor#getServiceInfo()方法和HostReactor#getServiceInfoDirectlyFromServer()方法。

HostReactor#getServiceInfoDirectlyFromServer()方法很简单,就是直接从注册中心获取服务列表。

private final NamingProxy serverProxy;

public ServiceInfo getServiceInfoDirectlyFromServer(final String serviceName, final String clusters)
            throws NacosException {
    //从注册中心获取最新服务列表服务
    String result = serverProxy.queryList(serviceName, clusters, 0, false);
    if (StringUtils.isNotEmpty(result)) {
        return JacksonUtils.toObj(result, ServiceInfo.class);
    }
    return null;
}

再看下NamingProxy#queryList()方法:

public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
            throws NacosException {

    final Map<String, String> params = new HashMap<String, String>(8);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put("clusters", clusters);
    params.put("udpPort", String.valueOf(udpPort));
    params.put("clientIP", NetUtils.localIP());
    params.put("healthyOnly", String.valueOf(healthyOnly));
    //直接调用服务端instance/list接口
    return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}

queryList()方法会携带客户端服务名和版本号等信息,直接先服务端发起查询服务列表请求。

服务端请求信息

重点来看一下HostReactor#getServiceInfo()方法:

//服务信息缓存
private final Map<String, ServiceInfo> serviceInfoMap;
//待更新服务缓存
private final Map<String, Object> updatingMap;

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

    NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
    String key = ServiceInfo.getKey(serviceName, clusters);
    //快速失败开关是否打开
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }

    //从serviceInfoMap缓存中获取服务信息
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
    //如果serviceInfoMap缓存中没有,则创建一个服务并放入缓存中
    if (null == serviceObj) {
        //创建一个服务
        serviceObj = new ServiceInfo(serviceName, clusters);
        //放入缓存中
        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        //放入待更新服务缓存中
        updatingMap.put(serviceName, new Object());
        //从注册中心获取最新服务列表,立即更新集群中的服务信息
        updateServiceNow(serviceName, clusters);
        //集群信息更新完成,从待更新中删除
        updatingMap.remove(serviceName);

    } else if (updatingMap.containsKey(serviceName)) {
        //如果在待更新的服务缓存中
        //更新时间间隔如果大于UPDATE_HOLD_INTERVAL=5s
        if (UPDATE_HOLD_INTERVAL > 0) {
            // 等待UPDATE_HOLD_INTERVAL时间直到updateServiceNow方法更新完成
            synchronized (serviceObj) {
                try {
                    serviceObj.wait(UPDATE_HOLD_INTERVAL);
                } catch (InterruptedException e) {
                    NAMING_LOGGER
                            .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                }
            }
        }
    }
    //添加一个定时更新服务的任务
    scheduleUpdateIfAbsent(serviceName, clusters);

    return serviceInfoMap.get(serviceObj.getKey());
}

private void updateServiceNow(String serviceName, String clusters) {
    try {
        updateService(serviceName, clusters);
    } catch (NacosException e) {
        NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
    }
}

/**
* Update service now.
*
* @param serviceName service name
* @param clusters    clusters
*/
public void updateService(String serviceName, String clusters) throws NacosException {
    ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
    try {
        //获取集群中服务列表
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

        if (StringUtils.isNotEmpty(result)) {
            processServiceJson(result);
        }
    } finally {
        if (oldService != null) {
            synchronized (oldService) {
                oldService.notifyAll();
            }
        }
    }
}

接下来看一下scheduleUpdateIfAbsent()方法:

private final Map<String, ScheduledFuture<?>> futureMap = new HashMap<String, ScheduledFuture<?>>();

public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
    if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
        return;
    }
    synchronized (futureMap) {
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
            return;
        }
        //添加一个定时任务
        ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
        futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
    }
}

public synchronized ScheduledFuture<?> addTask(UpdateTask task) {
    //延迟1秒执行定时任务,UpdateTask定时任务每10秒执行一次
    return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}

scheduleUpdateIfAbsent()方法中会添加一个每10s执行一次的 UpdateTask 定时任务。 UpdateTask 类是 HostReactor 的一个内部类,它实现了 Runnable 接口。它用于实现服务实例的动态更新和维护,保证服务实例列表的及时更新和健康状态的维护,从而提高服务的可用性和稳定性。

至此客户端服务发现就结束了。

总结一下客户端的服务发现,消费者发起服务调用,通常会采用 subscribe 方式获取服务实例列表,也就是从本地缓存中获取实例列表,如果注册中心宕机,客户端也能调用到服务,提高了Nacos的可用性。

服务端发现

服务端发现主要就是看服务端收到客户端请求后会做哪些操作,

@GetMapping("/list")
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
    public ObjectNode list(HttpServletRequest request) throws Exception {

    String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    //检查服务名格式
    checkServiceNameFormat(serviceName);

    String agent = WebUtils.getUserAgent(request);
    String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
    String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
    int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
    String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
    boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));

    String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
    String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
    boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));

    //获取服务全部信息
    return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
            healthyOnly);
}

InstanceController#list()方法会对请求参数进行解析,然后传递给 doSrvIpxt()方法。doSrvIpxt()方法首先会从 ServiceManager 本地缓存中获取服务信息,如果获取不到直接返回;否则进行各种判断去查询服务实例的全部信息,然后再返回给客户端。

@Autowired
private ServiceManager serviceManager;

public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP,
            int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception {

    ClientInfo clientInfo = new ClientInfo(agent);
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    //从缓存中获取服务
    Service service = serviceManager.getService(namespaceId, serviceName);

    long cacheMillis = switchDomain.getDefaultCacheMillis();

    // now try to enable the push
    try {
        if (udpPort > 0 && pushService.canEnablePush(agent)) {

            pushService
                    .addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort),
                            pushDataSource, tid, app);
            cacheMillis = switchDomain.getPushCacheMillis(serviceName);
        }
    } catch (Exception e) {
        Loggers.SRV_LOG
                .error("[NACOS-API] failed to added push client {}, {}:{}", clientInfo, clientIP, udpPort, e);
        cacheMillis = switchDomain.getDefaultCacheMillis();
    }

    //缓存中没有,说明该服务不存在或者下线了
    if (service == null) {
        if (Loggers.SRV_LOG.isDebugEnabled()) {
            Loggers.SRV_LOG.debug("no instance to serve for service: {}", serviceName);
        }
        result.put("name", serviceName);
        result.put("clusters", clusters);
        result.put("cacheMillis", cacheMillis);
        result.replace("hosts", JacksonUtils.createEmptyArrayNode());
        return result;
    }
    //检查服务是否能用
    checkIfDisabled(service);

    //省略若干代码

    result.replace("hosts", hosts);
    if (clientInfo.type == ClientInfo.ClientType.JAVA
            && clientInfo.version.compareTo(VersionUtil.parseVersion("1.0.0")) >= 0) {
        result.put("dom", serviceName);
    } else {
        result.put("dom", NamingUtils.getServiceName(serviceName));
    }
    result.put("name", serviceName);
    result.put("cacheMillis", cacheMillis);
    result.put("lastRefTime", System.currentTimeMillis());
    result.put("checksum", service.getChecksum());
    result.put("useSpecifiedURL", false);
    result.put("clusters", clusters);
    result.put("env", env);
    result.replace("metadata", JacksonUtils.transferToJsonNode(service.getMetadata()));
    return result;
}

至此,服务端的服务发现就结束,也相对简单一些。

总结一下:Nacos注册中心会在本地缓存或持久化存储中查找指定服务名的实例列表,并返回给服务消费者。如果本地缓存中没有该服务的实例信息,Nacos注册中心会直接返回。

总结

最后总结一下服务发现的流程:

  1. 服务启动时,根据spring.factories文件自动注入 NacosDiscoveryClientConfiguration 客户端服务发现配置类,同时也会注入 DiscoveryClient 接口的实现类 NacosDiscoveryClient
  2. 当发生服务调用时会调用 NacosDiscoveryClient#getInstances()方法获取服务实例列表 ,然后调用NacosServiceDiscovery#getInstances()方法
    将实例列表转为 ServiceInstance 服务实例信息列表。
  3. NacosServiceDiscovery#getInstances()方法会调用 NamingService#selectInstances()方法去查询实例信息,查询到之后会放入 ServiceCache 服务缓存中,如果因为网络或服务端宕机等原因出现异常,如果缓存中有的话,会从 ServiceCache 返回该服务实例信息,这提高了Nacos服务调用的可用性;在NamingService#selectInstances()方法, 因为是subscribe=true,所以会调用 HostReactor#getServiceInfo() 方法。
  4. HostReactor#getServiceInfo() 方法中,会先从serviceInfoMap 缓存中获取服务信息,如果没有则会创建一个服务,然后会把创建的服务放到待更新缓存 updatingMap 中,接着就去更新服务实例信息,更新完成再从 updatingMap 中剔除。 如果 serviceInfoMap 缓存中能获取到服务信息,则会判断是否该服务是否在待更新缓存 updatingMap 中,在的话就会要等待更新任务完成。
  5. 最后在HostReactor#getServiceInfo() 方法中会调用scheduleUpdateIfAbsent()方法创建一个 UpdateTask 定时任务,定时任务延迟1s执行,之后会每隔10s执行一次。UpdateTask 任务主要工作就是用于实现服务实例的动态更新和维护,保证服务实例列表的及时更新和健康状态的维护,从而提高服务的可用性和稳定性。
  6. 在Nacos注册中心接收到服务实例查询请求后,会调用InstanceController#list()方法,该方法会处理请求参数,然后将参数传递给doSrvIpxt()方法,最后doSrvIpxt()方法会从缓存中获取服务信息,获取不到直接返回;否则会对服务进行各种条件判断,最后返回服务实例全部信息。
  7. 服务消费者收到实例信息后就会根据负载均衡算法选择一台服务提供者,并向其发起调用请求。

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

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

相关文章

微服务保护——Sentinel【实战篇二】

一、线程隔离 &#x1f349; 线程隔离有两种方式实现&#xff1a; 线程池隔离信号量隔离&#xff08;Sentinel默认采用&#xff09; 线程隔离&#xff08;舱壁模式&#xff09;&#x1f95d; 在添加限流规则时&#xff0c;可以选择两种阈值类型&#xff1a; QPS&#xff1a;…

LiveNVR监控流媒体Onvif/RTSP功能-支持无人机、IPC等设备RTMP推流转码分发H5无插件播放也支持GB28181输出

LiveNVR支持无人机、IPC等设备RTMP推流转码分发H5无插件播放也支持GB28181输出 1、无人机推流转国标2、获取RTMP推流地址2.1、RTMP推流地址格式2.2、推流地址示例 2、设备RTMP推流3、配置拉转RTMP3.1、直播流地址格式3.2、直播流地地址示例3.3、通道配置直播流地址 4、配置级联…

螺杆支撑座的加工工艺

螺杆支撑座是重要的传动元件&#xff0c;一般与滚珠螺杆搭配使用&#xff0c;滚珠螺杆的固定座可选择使用深沟球轴承C7精度&#xff0c;磨削螺杆的固定座可选择用角接触轴承的C5精度&#xff0c;C5的精度更高。 支撑侧没有精度&#xff0c;一般使用深沟球轴承&#xff0c;如果螺…

linux 系统编程-进程中的通信

目录 1 IPC 方法 2管道 2.1管道的概念 2.2 pipe 函数 2.3管道的读写行为 2.4 管道缓冲区大小 2.5 管道的优劣 2.6 FIFO 3.共享存储映射 3.1 文件进程间通信 3.2 存储映射 I/O 3.3 mmap 函数 3.4 munmap 函数 3.5 mmap 注意事项 3.6 mmap 父子进程通信 3.7 mmap …

JAVA 面试准备

这里写自定义目录标题 一、JAVA基础1.ArrayList2.HashMap3.Concurrenthashmap4.Stream5.synchronized6.线程池7.CompletableFuture8.Fork/join9.数组与链表的区别10.单例模式1.饿汉模式2.懒汉模式10.1、 为啥使用synchronized?10.2、 又为啥使用volatile?10.3、 那又又为啥用…

【MySQL进阶(一)】MySQL在Linux中的配置信息和数据备份工具

MySQL在Linux中安装的话可以看这篇博客&#xff1a;MySQL在Linux中的安装&#xff0c;我觉得总结的很好。 my.cnf 中的配置信息 当 MySQL 启动的时候&#xff0c;会从安装目录中加载软件数据&#xff0c;即使用 mysqld 工具初始化设置的 --basedir&#xff0c;会从数据目录中…

GaussDB云数据库配套工具UGO

目录 一、前言 二、数据库和应用迁移UGO定义 1、UGO定义 2、异构数据库迁移简图 三、数据库迁移的痛点 四、数据库和应用迁移UGO能力介绍 五、数据库和应用迁移UGO方案简图介绍 六、小结 一、前言 在数字化时代&#xff0c;企业面临着越来越多的数据库和应用迁移需求。…

SpringBoot解决跨域问题的几种方式

本文参考自:SpringBoot 解决跨域问题的 5 种方案!_springboot跨域问题解决方案_肥肥技术宅的博客-CSDN博客 SpringBoot解决ajax跨域问题-腾讯云开发者社区-腾讯云 跨域问题指的是不同站点之间,使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制,它的初衷…

认识C++继承

认识继承 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象程序设计的层次结构&#xff0c;体现了由…

Protobuf数据交互实战

"no one gonna make me down" 在之前呢&#xff0c;我们介绍了什么protobuf以及它的语法、数据类型。 一句老话说得好&#xff0c;"多说不练&#xff0c;假把式&#xff01;"。因此&#xff0c;本篇会选择以protobuf的语法&#xff0c;完成一个简易的通讯…

uniapp 微信小程序 自定义弹框+picker下拉选择列表+输入表单:拒绝-选择理由弹窗

效果&#xff1a; 1、template <!-- 拒绝-选择理由弹窗--> <view class"reason-popover" v-if"showReasonDialog"><view class"reason-modal"><view class"reason-title"><text>请选择拒绝理由<…

文心一言 VS 讯飞星火 VS chatgpt (64)-- 算法导论6.5 3题

文心一言 VS 讯飞星火 VS chatgpt &#xff08;64&#xff09;-- 算法导论6.5 3题 三、要求用最小堆实现最小优先队列&#xff0c;请写出 HEAP-MINIMUM、HEAP-EXTRACT-MIN、HEAP DECREASE-KEY 和 MIN-HEAP-INSERT 的伪代码。 文心一言&#xff1a; 以下是使用最小堆实现最小…

界面控件DevExpress WPF数据编辑器组件,让数据处理更灵活!(二)

界面控件DevExpress WPF编辑器库可以帮助用户提供直观的用户体验&#xff0c;具有无与伦比的运行时选项和灵活性。WPF数据编辑器提供了全面的掩码和数据验证支持&#xff0c;可以独立使用&#xff0c;也可以作为容器控件(如DevExpress WPF Grid和WPF TreeList)中的单元格编辑器…

ubantu 安装 sudo apt install ubuntu-restricted-extras. 如何 OK

在安装的时候&#xff0c;不知道如何 Ok 使用 回车键&#xff0c;空格键 均不行&#xff0c;使用 Tab 键 &#xff0c;然后再使用 回车键。 Configuring ttf-mscorefonts-installer 答案是使用 Tab 键。

【Python】pyecharts 模块 ③ ( 使用 pyecharts 模块绘制折线图 )

文章目录 一、使用 pyecharts 模块绘制折线图1、折线图绘制过程2、完整代码示例 pyecharts 画廊网站 : https://gallery.pyecharts.org/#/ 在该网站可查看官方示例 一、使用 pyecharts 模块绘制折线图 1、折线图绘制过程 首先 , 导入 折线图 Line 对象 , 该类定义在 pyecharts…

Go http.Get不需要defer res.Body.Close()

前戏&#xff1a; go net/http包&#xff0c;必须要手动关闭嘛&#xff1f;非也。线上程序为啥协程数量直线上升&#xff0c;因为使用的姿势不对&#xff0c;请换个姿势。 干货&#xff1a; 手动关闭&#xff0c;释放资源 defer res.Body.Close() &#xff08;这是一个好习…

LeetCode[剑指Offer51]数组中的逆序对

难度&#xff1a;Hard 题目&#xff1a; 在数组中的两个数字&#xff0c;如果前面一个数字大于后面的数字&#xff0c;则这两个数字组成一个逆序对。输入一个数组&#xff0c;求出这个数组中的逆序对的总数。 示例 1: 输入: [7,5,6,4] 输出: 5 限制&#xff1a; 0 < 数组…

C++【STL】queue和deque 容器详解

C【STL】queue和deque 容器详解 一级目录 二级目录 三级目录 1. 什么是queue容器&#xff1f; Queue是一种先讲先出( First In First Out,FIFO )的数据结构&#xff0c;它有两个出口。 queue模版类的定义在头文件中。 include 定义queue对象的示例代码如下∶ queue<i…

【华为c# OD机考参考答案】01---IPv4地址转换成整数

题目 1、题目 01---IPv4地址转换成整数2、解图思路 1、IP地址转为二进制 2、二进制转十进制 3、注意事项 1、IP地址的范围判断 2、空字符串判断 3、非法字符判断 4、考点 1、string的split 、convert等相关用法 2、正则表达式 3、进制转换 4、理解32位整数的意思 5、代码 判…

2023华为OD统一考试(B卷)题库清单(持续收录中)以及考点说明

目录 专栏导读2023 B卷 “新加题”&#xff08;100分值&#xff09;2023Q2 100分2023Q2 200分2023Q1 100分2023Q1 200分2022Q4 100分2022Q4 200分牛客练习题 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&…