全网第一篇把Nacos配置中心客户端讲明白的

入口

我们依旧拿ConfigExample作为入口

public class ConfigExample {public static void main(String[] args) throws NacosException, InterruptedException {String serverAddr = "localhost";String dataId = "test";String group = "DEFAULT_GROUP";Properties properties = new Properties();properties.put("serverAddr", serverAddr);ConfigService configService = NacosFactory.createConfigService(properties);String content = configService.getConfig(dataId, group, 5000);System.out.println(content);configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("receive:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}});Thread.sleep(300000);boolean isPublishOk = configService.publishConfig(dataId, group, "content");System.out.println(isPublishOk);Thread.sleep(3000);content = configService.getConfig(dataId, group, 5000);System.out.println(content);//        boolean isRemoveOk = configService.removeConfig(dataId, group);
//        System.out.println(isRemoveOk);
//        Thread.sleep(3000);
//
//        content = configService.getConfig(dataId, group, 5000);
//        System.out.println(content);Thread.sleep(300000);}
}

NacosFactory.createConfigService

套路和之前差不多, 加速过
image.png
image.png
会走到NacosConfigService的构造方法里面
image.png

NacosConfigService.getConfig

image.png

   private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {group = blank2defaultGroup(group);// todo 检查参数ParamUtils.checkKeyParam(dataId, group);ConfigResponse cr = new ConfigResponse();// todo // 设置配置信息cr.setDataId(dataId);cr.setTenant(tenant);cr.setGroup(group);// use local config first// todo 这里有个失败转移的配置。如果能读到失败转移的配置信息,则直接返回了。原因的话英文注释写的很清楚了// 优先使用失败转移,设计的目的是当server挂后,又需要修改配置,就可以读本地目录String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);if (content != null) {LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));cr.setContent(content);String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);cr.setEncryptedDataKey(encryptedDataKey);configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;}try {// todo 通过客户端远程拉取配置信息ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);cr.setContent(response.getContent());cr.setEncryptedDataKey(response.getEncryptedDataKey());configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;} catch (NacosException ioe) {if (NacosException.NO_RIGHT == ioe.getErrCode()) {throw ioe;}LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",worker.getAgentName(), dataId, group, tenant, ioe.toString());}LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}",worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));// todo     // 非鉴权失败的异常的,可以从本地快照中获取配置,如果有的话content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);cr.setContent(content);String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);cr.setEncryptedDataKey(encryptedDataKey);configFilterChainManager.doFilter(null, cr);content = cr.getContent();return content;}

总结一下做了几件事:

  1. 支持故障转移从本地读取配置
  2. 正常情况下从server获取配置
  3. 非鉴权失败的异常,可以从本地快照中获取配置

ClientWorker.getServiceConfig

image.png
这里的agent会被ClientWorker里面内部类的ConfigRpcTransportClient�继承,并且重写,也就是说最终会调用到ConfigRpcTransportClient.queryConfig方法

image.png

通过GrpcSdkClient往server发送请求,获取配置

NacosConfigService.pushConfig

这个比较简单,和上面逻辑类似
最终也是调用ClientWorker.publishConfig -> agent.pushConfig(实际为ClientWorker的内部类ConfigRpcTransportClient)
image.png

NacosConfigService.addListener

image.png
image.png
image.png
这里一共做了几件事:

  1. 创建CacheData,这里有一个很重要的cacheMap.size()/ParamUtil.getPerTaskConfigSize(默认是是3000),也就说对cache进行一个分组,比如size为1/3000,2/3000,这里的taskId永远为0,后面在定时任务调度,批量往server端请求的时候会用到
  2. 往cache里面放listener
  3. 设置syncWithServer为false
  4. agent.notifyListenConfig:ConfigRpcTransportClient.notifyListenConfig 这个比较重要

image.png
它会往这个阻塞队列里面放一个Object,为什么要放呢?那肯定有地方要取,ClientWorker在启动的时候,会有一个定时任务不断从这个阻塞队列中取,如果取到就执行

ClientWorker

NacosConfigService在创建的同时会创建ClientWorker, ClientWorker其实就是它的打手😄
image.png
image.png
这个agent又是ClientWorker的打手,当调用到agent.start的时候,最终会调用到ClientWorker的内部类image.png的startInternal方法,

ClientWorker#ConfigRpcTransportClient.startInternal方法

image.png
这个方法不断在从ListenExecutebell获取,如果说一直获取不到,就超时,就进入executeConfigListen, 结合前面的notifyListenConfig其实就是给这里一个信号,触发executeConfigListen执行

ClientWorker#ConfigRpcTransportClient.executeConfigListen

 public void executeConfigListen() {// todo 存放含有listen的cacheDataMap<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);// todo 存放不含邮listen的cacheDataMap<String, List<CacheData>> removeListenCachesMap = new HashMap<String, List<CacheData>>(16);long now = System.currentTimeMillis();// todo // 当前时间减去上次全量同步时间是否大于5分钟boolean needAllSync = now - lastAllSyncTime >= ALL_SYNC_INTERNAL;for (CacheData cache : cacheMap.get().values()) {synchronized (cache) {// todo !!!!!!!这里,一般不会走这里,不要被误导了,我也是debug多次才发现//check local listeners consistent.if (cache.isSyncWithServer()) {// todo // 一致则检查md5值,若md5值和上一个不一样,则说明变动了,需要通知监听器cache.checkListenerMd5();// todo // 是否到全量同步时间了,未到则直接跳过if (!needAllSync) {continue;}}if (!CollectionUtils.isEmpty(cache.getListeners())) {// todo 如果有监听器并且缓存数据并非使用本地的,则把这些缓存数据加入到需要监听的列表listenCachesMap中//get listen  configif (!cache.isUseLocalConfigInfo()) {List<CacheData> cacheDatas = listenCachesMap.get(String.valueOf(cache.getTaskId()));if (cacheDatas == null) {cacheDatas = new LinkedList<CacheData>();listenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);}cacheDatas.add(cache);}} else if (CollectionUtils.isEmpty(cache.getListeners())) {// todo 即删除, 放入removeListenCachesMapif (!cache.isUseLocalConfigInfo()) {List<CacheData> cacheDatas = removeListenCachesMap.get(String.valueOf(cache.getTaskId()));if (cacheDatas == null) {cacheDatas = new LinkedList<CacheData>();removeListenCachesMap.put(String.valueOf(cache.getTaskId()), cacheDatas);}cacheDatas.add(cache);}}}}// todo  此时,如果需要和服务端数据同步,则listenCachesMap和removeListenCachesMap存放了本地数据,需要和服务端对比boolean hasChangedKeys = false;if (!listenCachesMap.isEmpty()) {for (Map.Entry<String, List<CacheData>> entry : listenCachesMap.entrySet()) {String taskId = entry.getKey();Map<String, Long> timestampMap = new HashMap<>(listenCachesMap.size() * 2);List<CacheData> listenCaches = entry.getValue();for (CacheData cacheData : listenCaches) {timestampMap.put(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant),cacheData.getLastModifiedTs().longValue());}// todo 构建新增数据的请求参数,此请求用于远程和本地对比,发现变动了会进行通知ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(listenCaches);// todo // 配置需要新增或更新监听数据configChangeListenRequest.setListen(true);try {// todo // 获取一个rpc的客户端RpcClient rpcClient = ensureRpcClient(taskId);ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(rpcClient, configChangeListenRequest);if (configChangeBatchListenResponse != null && configChangeBatchListenResponse.isSuccess()) {Set<String> changeKeys = new HashSet<String>();//handle changed keys,notify listenerif (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) {hasChangedKeys = true;for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse.getChangedConfigs()) {String changeKey = GroupKey.getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(),changeConfig.getTenant());changeKeys.add(changeKey);boolean isInitializing = cacheMap.get().get(changeKey).isInitializing();// todo  刷新配置并通知变动refreshContentAndCheck(changeKey, !isInitializing);}}//handler content configsfor (CacheData cacheData : listenCaches) {String groupKey = GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.getTenant());if (!changeKeys.contains(groupKey)) {//sync:cache data md5 = server md5 && cache data md5 = all listeners md5.synchronized (cacheData) {if (!cacheData.getListeners().isEmpty()) {Long previousTimesStamp = timestampMap.get(groupKey);if (previousTimesStamp != null) {if (!cacheData.getLastModifiedTs().compareAndSet(previousTimesStamp,System.currentTimeMillis())) {continue;}}// todo  缓存数据没有变动,设置为和服务器同步cacheData.setSyncWithServer(true);}}}cacheData.setInitializing(false);}}} catch (Exception e) {LOGGER.error("Async listen config change error ", e);try {Thread.sleep(50L);} catch (InterruptedException interruptedException) {//ignore}}}}// todo     // 需要删除的数据不为空if (!removeListenCachesMap.isEmpty()) {for (Map.Entry<String, List<CacheData>> entry : removeListenCachesMap.entrySet()) {String taskId = entry.getKey();List<CacheData> removeListenCaches = entry.getValue();ConfigBatchListenRequest configChangeListenRequest = buildConfigRequest(removeListenCaches);// todo // 配置需要删除configChangeListenRequest.setListen(false);try {// 获取rpc客户端RpcClient rpcClient = ensureRpcClient(taskId);// todo 通知服务端移除数据boolean removeSuccess = unListenConfigChange(rpcClient, configChangeListenRequest);if (removeSuccess) {for (CacheData cacheData : removeListenCaches) {synchronized (cacheData) {// todo  // 移除缓存if (cacheData.getListeners().isEmpty()) {ClientWorker.this.removeCache(cacheData.dataId, cacheData.group, cacheData.tenant);}}}}} catch (Exception e) {LOGGER.error("async remove listen config change error ", e);}try {Thread.sleep(50L);} catch (InterruptedException interruptedException) {//ignore}}}if (needAllSync) {//todo  更新同步时间lastAllSyncTime = now;}//If has changed keys,notify re sync md5.if (hasChangedKeys) {// todo // 服务端告知了有数据变动,则需要再同步一次notifyListenConfig();}}

这段代码极其的长,现在我们来总结一一下具体做了哪些事:

  1. 设置两个监听器cacheMap,一个是带监听器的,一个是不带的

  2. 开始遍历cacheMap集合,忽略什么cache.isSyncWithServer,debug的时候走不到这里,看源码抓住核心流程,将有监听器的放到listenCachesMap(注意,这里有一个分组操作,拿到cache的taskId, 将cache的taskId和 相同taskId的cache组成一个Map:<taskId, {cacheData, cacheData}>),将没有监听器的放到removeListenCacheMap中image.png

  3. 如果listenCachesMap不为空,然后遍历listenCachesMap,

    1. 构造批量配置查询请求
    2. 获取一个RPC的客户端
    3. 发起RPC请求,查询这一个批taskId对应的cacheData发生变化了没,如果有返回值,就会走到refreshContextAndCheck 刷新配置并通知
  4. refreshContentAndCheck�:通过cacheData拿到的dataId、group、tenant 通过getServerConfig调用服务端拿到这个dataId对应的配置

ClientWorker#refreshContentAndCheck

image.png
将请求回来的content、configType、encryptedDataKey都设置到cacheData中,接下来调用cacheData.checkListenerMD5()

另外注意一下cacheData.setContent:会同时设置上md5
image.png

CacheData.checkListenerMD5

image.png
listeners(ManagerListernerWrap)就是我们刚开始创建CacheData设置上的listener上面包装了一层,在创建listener的时候,会把CacheData的content、md5、还有我们创建listener都放到里面,所以这里才会判断当前CacheData里面的md5和listener里面md5是不是一样的,如果不是,就需要通知到listener
image.png

CacheData.safeNotifyListener

 private void safeNotifyListener(final String dataId, final String group, final String content, final String type,final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {final Listener listener = listenerWrap.listener;if (listenerWrap.inNotifying) {LOGGER.warn("[{}] [notify-currentSkip] dataId={}, group={}, md5={}, listener={}, listener is not finish yet,will try next time.",name, dataId, group, md5, listener);return;}// todo 定义一个通知任务Runnable job = new Runnable() {@Overridepublic void run() {long start = System.currentTimeMillis();ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();ClassLoader appClassLoader = listener.getClass().getClassLoader();try {// todo 拓展点,像spring cloud alibaba就用到了,创建了NacosContextRefresherif (listener instanceof AbstractSharedListener) {AbstractSharedListener adapter = (AbstractSharedListener) listener;adapter.fillContext(dataId, group);LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);}// Before executing the callback, set the thread classloader to the classloader of// the specific webapp to avoid exceptions or misuses when calling the spi interface in// the callback method (this problem occurs only in multi-application deployment).Thread.currentThread().setContextClassLoader(appClassLoader);ConfigResponse cr = new ConfigResponse();cr.setDataId(dataId);cr.setGroup(group);cr.setContent(content);cr.setEncryptedDataKey(encryptedDataKey);configFilterChainManager.doFilter(null, cr);String contentTmp = cr.getContent();listenerWrap.inNotifying = true;// todo !!!!最终回调通知,就是这里listener.receiveConfigInfo(contentTmp);// compare lastContent and content// todo   扩展点,告知配置内容的变动if (listener instanceof AbstractConfigChangeListener) {Map data = ConfigChangeHandler.getInstance().parseChangeData(listenerWrap.lastContent, content, type);ConfigChangeEvent event = new ConfigChangeEvent(data);((AbstractConfigChangeListener) listener).receiveConfigChange(event);listenerWrap.lastContent = content;}// 赋予最新的md5listenerWrap.lastCallMd5 = md5;LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ,cost={} millis.", name,dataId, group, md5, listener, (System.currentTimeMillis() - start));} catch (NacosException ex) {LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());} catch (Throwable t) {LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,group, md5, listener, t.getCause());} finally {listenerWrap.inNotifying = false;Thread.currentThread().setContextClassLoader(myClassLoader);}}};final long startNotify = System.currentTimeMillis();try {// todo // 监听器配置了异步执行器,就用配置的执行if (null != listener.getExecutor()) {listener.getExecutor().execute(job);} else {try {//todo  内部线程池执行INTERNAL_NOTIFIER.submit(job);} catch (RejectedExecutionException rejectedExecutionException) {LOGGER.warn("[{}] [notify-blocked] dataId={}, group={}, md5={}, listener={}, no available internal notifier,will sync notifier ",name, dataId, group, md5, listener);job.run();} catch (Throwable throwable) {LOGGER.error("[{}] [notify-blocked] dataId={}, group={}, md5={}, listener={}, submit internal async task fail,throwable= ",name, dataId, group, md5, listener, throwable);job.run();}}} catch (Throwable t) {LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,group, md5, listener, t.getCause());}final long finishNotify = System.currentTimeMillis();LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",name, (finishNotify - startNotify), dataId, group, md5, listener);}

�这块代码也很长,但是比较简单:

  1. 创建一个任务,判断是否是某种类型listener,如果是AbstractSharedListener,就回调到它的方法
  2. 回调到我们正常的listener方法,比如listener.receiverConfigInfo
  3. 判断是否是AbstractConfigChange Listener,如果是,就回调
  4. 看这个listener有没有配置异步执行器Executor,如果有就用它执行,如果没有,就用内部的线程池执行

ClientWorker#ConfigRpcTransportClient�#ensureRpcClient

�到上面为止,其实客户端的主流程已经比较请求,但是在executeConfigListen方法中有一个小方法ensureRpcClient我们就简单的一笔带过,实际上在后续的与服务端请求交互比较有用,我们还是再看一下
image.png
image.png
image.png
简单总结一下:

  1. 通过RpcClientFactory创建了一个GrpcSDKClient,这个之前Nacos服务注册的时候也会创建,所以比较熟悉
  2. 初始化网络请求处理:在这里注册了服务端调用客户端的处理方法, 注意不是客户端请求,而是服务端接受客户端的请求,因为Grpc是可以双向请求的,这个最重要的就是notifyListenConfig,😄是不是非常熟悉,如果我们服务端改动了配置,客户端从这里就可以得到通知,然后往listenExecutebell.offer(bellItem)发送一个信号,客户端就立马开始执行executeConfigListen
  3. rpcClient.start:这个没什么好说的,服务注册那里说过

image.png

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

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

相关文章

拍摄的视频怎么做二维码?视频在线转二维码的技巧

现在学校经常会将学生日常的拍摄的短片做成二维码之后展示给其他人&#xff0c;其他人可以通过扫描二维码来查看个人表现的视频&#xff0c;有些活动视频也会用视频二维码的方式来传播。那么视频二维码制作的方法及步骤是什么样的呢&#xff1f;下面就让小编通过本文来给大家介…

Eclipse 安装使用ABAPGit

Eclipse->Help->Install New software 添加地址 https://eclipse.abapgit.org/updatesite/ 安装完成打开 选择abapGit repositories,先添加仓库 点下图添加自己仓库 如图添加仓库地址 添加完仓库后&#xff0c;点击我的仓库 右键选中行&#xff0c;可以进行push和pu…

自动驾驶IPO第一股及商业化行业标杆 Mobileye

一、Mobileye 简介 Mobileye 是全球领先的自动驾驶技术公司&#xff0c;成立于 1999 年&#xff0c;总部位于以色列耶路撒冷。公司专注于开发视觉感知技术和辅助驾驶系统 (ADAS)&#xff0c;并在自动驾驶领域处于领先地位。Mobileye 是高级驾驶辅助系统&#xff08;ADAS&#…

Vscode编译运行多个C++文件

1.摘要 在使用Vscode编译单个文件网上很多教程&#xff0c;但是对多个文件编译会发现经常出问题&#xff0c;通过不断的借阅网友的教程改进终于完成C运行多个文件教程如下&#xff1a; 2.编译运行过程 2.1 初始配置过程 &#xff08;1&#xff09;Vscode以及MinGW配置教程很…

管理类联考-复试-全流程演练-导航页

文章目录 整体第一步&#xff1a;学校导师两手抓——知己知彼是关键学校校训历史 导师你对导师的研究方向有什么认知。 第二步&#xff1a;面试问题提前背——押题助沟通自我介绍——出现概率&#xff1a;100%为什么选择这个专业&#xff1f;今后如何打算&#xff1f;你认为自己…

高并发内存池项目

目录 项目简介什么是内存池池化技术内存池 内存池主要解决的问题定长内存池的设计高并发内存池的整体框架设计thread cachethread cache的整体设计thread cache哈希桶的对齐规则threadcacheTLS无锁访问 central cachecentralcache整体设计central cache 结构设计central cache核…

Redis——高级主题

介绍Redis的高级主题&#xff0c;包括服务器配置、Redis事务、Redis发布和订阅、Pipeline批量发送请求、数据备份与恢复等。 1、服务器配置 在Windows和Linux的Redis服务器里面&#xff0c;都有一个配置文件。Redis配置文件位于Redis安装目录下&#xff0c;在不同操作系统下&…

Redis核心技术与实战【学习笔记】 - 21.Redis实现分布式锁

概述 在《20.Redis原子操作》我们提到了应对并发问题时&#xff0c;除了原子操作&#xff0c;还可以通过加锁的方式&#xff0c;来控制并发写操作对共享数据的修改&#xff0c;从而保证数据的正确性。 但是&#xff0c;Redis 属于分布式系统&#xff0c;当有多个客户端需要争…

【Java知识手册】一.Java开发工具和前言

文章目录 1 Java前言1.1 简介1.2 Java环境搭建1.3 程序的开发步骤1.4 idea开发工具1.5用idea开发一个helloworld 前言&#xff1a;以初学着的身份&#xff0c;准备在该平台整理点最近学习的知识&#xff0c;方便后续查看相关的技术点&#xff0c;有兴趣的可以一块交流学习。目标…

机器学习本科课程 大作业 多元时间序列预测

1. 问题描述 1.1 阐述问题 对某电力部门的二氧化碳排放量进行回归预测&#xff0c;有如下要求 数据时间跨度从1973年1月到2021年12月&#xff0c;按月份记录。数据集包括“煤电”&#xff0c;“天然气”&#xff0c;“馏分燃料”等共9个指标的数据&#xff08;其中早期的部分…

接口测试要测试什么?

一. 什么是接口测试&#xff1f;为什么要做接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互…

Maven 安装教程

一、安装地址 1.官网安装最新版本 2.其他版本&#xff0c;我这里是maven-3/3.6.2 二、配置环境 1. 点击此电脑鼠标右击->属性->高级系统设置->环境变量 &#xff0c;配置系统变量->新建&#xff1a;MAVEN_HOME 2.配置path 路径 &#xff1a;%MAVEN_HOME%\bin 三、安…

ChatGPT之制作短视频

引言 今天带来了如何使用 ChatGPT和剪映来制作简单的短视频教程&#xff0c;在这其中 ChatGPT的作用主要是帮我们生成文案&#xff0c;剪映的功能就是根据文案自动生成视频&#xff0c;并配上一些图片、动画、字幕和解说。 ChatGPT生成文案 首先&#xff0c;我们需要使用提示…

Anaconda的安装及其配置

一、简介 Anaconda是一个开源的包、环境管理器&#xff0c;主要具有以下功能和特点&#xff1a; 提供conda包管理工具&#xff1a;可以方便地创建、管理和分享Python环境&#xff0c;用户可以根据自己的需要创建不同的环境&#xff0c;每个环境都可以拥有自己的Python版本、库…

常用排序算法(Java版本)

1 引言 常见的排序算法有八种&#xff1a;交换排序【冒泡排序、快速排序】、插入排序【直接插入排序、希尔排序】、选择排序【简单选择排序、堆排序】、归并排序、基数排序。 2 交换排序 所谓交换&#xff0c;就是序列中任意两个元素进行比较&#xff0c;根据比较结果来交换…

nginx slice模块的使用和源码分析

文章目录 1. 为什么需要ngx_http_slice_module2. 配置指令3. 加载模块4. 源码分析4.1 指令分析4.2 模块初始化4.3 slice模块的上下文4.2 $slice_range字段值获取4.3 http header过滤处理4.4 http body过滤处理5 测试和验证 1. 为什么需要ngx_http_slice_module 顾名思义&#…

程序员为什么不喜欢关电脑,这回答很霸道!

在大家的生活中&#xff0c;经常会发现这样一个现象&#xff1a;程序员经常不关电脑。 至于程序员不关电脑的原因&#xff0c;众说纷纭。 其中这样的一个程序员&#xff0c;他的回答很霸道&#xff1a; “因为我是程序员&#xff0c;我有权选择不关电脑。我需要在任何时候都能够…

C++一维数组

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 铁汁们大家好呀&#xff0c;我是PingdiGuo_guo&#xff0c;今天我们来学习一下数组&#xff08;一维&#xff09;。 文章目录 1.数组的概念与思想 2.为什么要使用数组 3.数组的特性 4.数组的操作 1.定义…

0 代码自动化测试:RF 框架实现企业级 UI 自动化测试

前言 现在大家去找工作&#xff0c;反馈回来的基本上自动化测试都是刚需&#xff01;没有自动化测试技能&#xff0c;纯手工测试基本没有什么市场。 但是很多人怕代码&#xff0c;觉得自动化测试就需要代码&#xff01;代码学习起来很难&#xff01; 当然代码学习不难&#xf…

优思学院|精益生产-改变制造业的革命性理念

在今日这个变幻莫测、竞争如潮的市场环境中&#xff0c;企业如同海上的帆船&#xff0c;面临着狂风巨浪的考验。在这样的大背景之下&#xff0c;精益生产&#xff08;Lean Production&#xff09;这一理念&#xff0c;宛如一盏明灯&#xff0c;指引着无数企业穿越迷雾&#xff…