全网第一篇把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() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("receive:" + configInfo);
            }

            @Override
            public 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的cacheData
            Map<String, List<CacheData>> listenCachesMap = new HashMap<String, List<CacheData>>(16);
            // todo 存放不含邮listen的cacheData
            Map<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  config
                        if (!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 即删除, 放入removeListenCachesMap
                        if (!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 listener
                            if (!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 configs
                            for (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() {
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
                    // todo 拓展点,像spring cloud alibaba就用到了,创建了NacosContextRefresher
                    if (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;
                    }
                    // 赋予最新的md5
                    listenerWrap.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

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

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

相关文章

搭建frp

1.frp 是什么&#xff1f; frp 是一款高性能的反向代理应用&#xff0c;专注于内网穿透。它支持多种协议&#xff0c;包括 TCP、UDP、HTTP、HTTPS 等&#xff0c;并且具备 P2P 通信功能。使用 frp&#xff0c;您可以安全、便捷地将内网服务暴露到公网&#xff0c;通过拥有公网…

解决nvrtc: error: invalid value for --gpu-architecture (-arch)

问题描述 在使用pytorch3d的时候&#xff0c;可以正常的import&#xff0c;但是在执行错误的使用就会报&#xff0c;nvrtc: error: invalid value for --gpu-architecture (-arch)&#xff0c;的错误&#xff0c;图片如下&#xff1a; 我的环境是&#xff1a; 显卡&#xff1…

精细管理药厂设备,制药机械设备管理平台系统助力生产提效

制药行业的复杂性要求对药品的品质和安全性进行严格控制&#xff0c;而这离不开高效管理各类机械设备。然而&#xff0c;随着制药企业规模的不断扩大和技术的迅猛进步&#xff0c;如何有效管理这些设备成为一个亟待解决的问题。在这一挑战面前&#xff0c;PreMaint制药机械设备…

Antd+React+react-resizable实现表格拖拽功能

1、先看效果 2、环境准备 "dependencies": {"antd": "^5.4.0","react-resizable": "^3.0.4",},"devDependencies": {"types/react": "^18.0.33","types/react-resizable": "^…

前端面试题——Vue的双向绑定

前言 双向绑定机制是Vue中最重要的机制之一&#xff0c;甚至可以说是Vue框架的根基&#xff0c;它将数据与视图模板相分离&#xff0c;使得数据处理和页面渲染更为高效&#xff0c;同时它也是前端面试题中的常客&#xff0c;接下来让我们来了解什么是双向绑定以及其实现原理。…

Python的包安装工具——pip命令大全

对于大多数使用Python的人来说&#xff0c;一定知道pip这个包安装工具&#xff0c;但是对pip可能还不是很了解&#xff0c;今天作者给大家介绍一下pip的命令&#xff0c;以方便灵活使用pip。 一、pip工具使用方法 pip的语法如下&#xff1a; pip [options] 式中&#xff1a…

InverseMatrix3D

InverseMatrixVT3D: An Efficient Projection Matrix-Based Approach for 3D Occupancy Prediction https://github.com/DanielMing123/InverseMatrixVT3D InverseMatrix3D过程总结如下&#xff1a; 1. 用2D backbone提取N个视角的多尺度图像特征&#xff0c;表示如下&#xf…

机器学习聚类算法

聚类算法是一种无监督学习方法&#xff0c;用于将数据集中的样本划分为多个簇&#xff0c;使得同一簇内的样本相似度较高&#xff0c;而不同簇之间的样本相似度较低。在数据分析中&#xff0c;聚类算法可以帮助我们发现数据的内在结构和规律&#xff0c;从而为进一步的数据分析…

Centos 内存和硬盘占用情况以及top作用

目录 只查看内存使用情况&#xff1a; 内存使用排序取前5个&#xff1a; 硬盘占用情况 定位占用空间最大目录 top查看cpu及内存使用信息 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&…

Python 潮流周刊#38:Django + Next.js 构建全栈项目

△△请给“Python猫”加星标 &#xff0c;以免错过文章推送 你好&#xff0c;我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容&#xff0c;大部分为英文。本周刊开源&#xff0c;欢迎投稿[1]。另有电报频道[2]作为副刊&#xff0c;补充发布更加丰富的资讯&#xff0c;…

protoc结合go完成protocol buffers协议的序列化与反序列化

下载protoc编译器 下载 https://github.com/protocolbuffers/protobuf/releases ps: 根据平台选择需要的编译器&#xff0c;这里选择windows 解压 加入环境变量 安装go专用protoc生成器 https://blog.csdn.net/qq_36940806/article/details/135017748?spm1001.2014.3001.…

canvas图片上设置镂空文字效果

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

VR全景技术可以应用在哪些行业,VR全景技术有哪些优势

引言&#xff1a; VR全景技术&#xff08;Virtual Reality Panorama Technology&#xff09;是一种以虚拟现实技术为基础&#xff0c;通过360度全景影像、立体声音、交互元素等手段&#xff0c;创造出沉浸式的虚拟现实环境。该技术不仅在娱乐领域有着广泛应用&#xff0c;还可…

方案分享:F5怎么样应对混合云网络安全?

伴随着云计算走入落地阶段&#xff0c;企业的云上业务规模增长迅猛。具有部署灵活、成本低、最大化整合现有资产、促进业务创新等优点的混合云逐渐成为企业选择的部署方式。与此同时&#xff0c;安全运营的复杂度进一步提高。比如安全堆栈越来越复杂、多云基础设施和应用添加网…

小白Linux学习笔记-Linux开机启动流程

Linux 开机启动流程 文章目录 Linux 开机启动流程启动流程概览详细讲解开机软件 —— BIOS、Grub名词解释流程解释BIOS 开机文档 —— menu.lst、grub.confGrub 配置文档流程解释 init 程序流程解释init 执行的相关文件 run-level(启动等级) 相关的命令实验rhel6 单用户模式修改…

机器学习数据预处理方法(数据重编码)

文章目录 [TOC]基于Kaggle电信用户流失案例数据&#xff08;可在官网进行下载&#xff09;一、离散字段的数据重编码1.OrdinalEncoder自然数排序2.OneHotEncoder独热编码3.ColumnTransformer转化流水线 二、连续字段的特征变换1.标准化&#xff08;Standardization&#xff09;…

数字人客服技术预研

技术洞察 引言 在当今数字化时代&#xff0c;不断进步和创新的人工智能&#xff08;AI&#xff09;技术已经渗透到各行各业中。随着AI技术、大模型技术逐步发展&#xff0c;使得数字人的广泛应用成为可能&#xff0c;本文将跟大家一起探讨AI数字人客服的概念、优势、应用场景…

苹果电脑录制视频在哪里?教你快速找到它!

录制电脑屏幕已成为了许多用户日常所需的操作&#xff0c;无论是录制在线课程、游戏过程&#xff0c;还是网络会议&#xff0c;一款好的录屏软件能帮助用户高效、便捷地完成任务。苹果电脑是当今主流的计算机设备之一&#xff0c;可是很多用户不知道苹果电脑录制视频在哪里。在…

麒麟信安登录央视, 深度展现为中国信息安全铸“魂”之路

近日&#xff0c;麒麟信安登录央视频道&#xff0c;《麒麟信安——为中国信息安全铸“魂”》在CCTV-4中文国际频道、CCTV-7国防军事频道、CCTV-10 科教频道、CCTV-12社会与法频道、CCTV-17农业农村频道&#xff0c;向亿万观众深度展现麒麟信安为中国信息安全铸“魂”之路。 麒…

Kafka相关内容复习

为什么要用消息队列 解耦 允许你独立的扩展或修改两边的处理过程&#xff0c;只要确保它们遵守同样的接口约束。 可恢复性 系统的一部分组件失效时&#xff0c;不会影响到整个系统。消息队列降低了进程间的耦合度&#xff0c;所以即使一个处理消息的进程挂掉&#xff0c;加入队…