[Nacos] Nacos Client获取所有服务和定时更新Client端的注册表 (三)

文章目录

      • 1.Nacos Client获取所有服务
        • 1.1 Client如何获取所有服务
        • 1.2 Client获取服务方法getServices()详解
      • 2.Nacos定时更新Client端的注册表
        • 2.1 Nacos和Eureka定时更新Client端的注册表的区别
        • 2.2 Client定时更新本地服务过程
        • 2.3 updateServiceNow方法解析
        • 2.4 定时更新本地注册表中的当前服务

Nacos的服务发现功能: 获取所有服务, 定时更新Client端的注册表

1.Nacos Client获取所有服务

1.1 Client如何获取所有服务

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

在这里插入图片描述

在这里插入图片描述

NacosDiscoveryClientAutoConfiguration.java

@Configuration
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({ SimpleDiscoveryClientAutoConfiguration.class,
		CommonsClientAutoConfiguration.class })
public class NacosDiscoveryClientAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public NacosDiscoveryProperties nacosProperties() {
		return new NacosDiscoveryProperties();
	}

	@Bean
	public DiscoveryClient nacosDiscoveryClient(
			NacosDiscoveryProperties discoveryProperties) {
		return new NacosDiscoveryClient(discoveryProperties);
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.watch.enabled", matchIfMissing = true)
	public NacosWatch nacosWatch(NacosDiscoveryProperties nacosDiscoveryProperties) {
		return new NacosWatch(nacosDiscoveryProperties);
	}
}

NacosDiscoveryClient#getServices()

    @Override
    public List<String> getServices() {

        try {
            ListView<String> services = discoveryProperties.namingServiceInstance()
                    .getServicesOfServer(1, Integer.MAX_VALUE);
            return services.getData();
        } catch (Exception e) {
            log.error("get service name from nacos server fail,", e);
            return Collections.emptyList();
        }
    }

在这里插入图片描述

这里的discoveryProperties为上面的NacosDiscoveryClientAutoConfiguration自动注入了。

DiscoveryClientHealthIndicator为SpringBoot的actuator自带的监控功能。
DiscoveryClientHealthIndicator#health()调用了这个getServices()方法。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.2 Client获取服务方法getServices()详解

NacosMaingService#getServicesOfServer()

在这里插入图片描述

NamingProxy#getServiceList()

在这里插入图片描述

最后还是通过向Server端发送get请求。

在这里插入图片描述

result为两个服务名称。

2.Nacos定时更新Client端的注册表

还是自动注册类NacosDiscoreryClientAutoConfiguration.java, 会自动注入NaocsWatch类, 这个类在Nacos源码不存在, 只是自动注入SpringBoot的类。

在这里插入图片描述

2.1 Nacos和Eureka定时更新Client端的注册表的区别

Eureka:

Nacos:

2.2 Client定时更新本地服务过程

NacosNamingService#subscribe()

在这里插入图片描述

通过hostReactor.getServiceInfo()更新本地服务过程

HostReactor#getServiceInfo()

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

        NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
        // 构建key,其格式为  groupId@@微服务名称@@clusters名称
        String key = ServiceInfo.getKey(serviceName, clusters);
        if (failoverReactor.isFailoverSwitch()) {
            return failoverReactor.getService(key);
        }

        // 从当前client的本地注册表中获取当前服务
        ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

        if (null == serviceObj) {  // 若本地注册表中没有该服务,则创建一个
            // 创建一个空的服务(没有任何提供者实例instance的ServiceInfo)
            serviceObj = new ServiceInfo(serviceName, clusters);

            // serviceInfoMap即为Client端的本地注册表
            serviceInfoMap.put(serviceObj.getKey(), serviceObj);

            // updatingMap是一个临时缓存,其主要是使用这个缓存map的key,
            // 使用map的key不能重复这个特性
            // 只要有服务名称出现在这个缓存map中,就表示当前这个服务正在被更新
            // 准备要更新serviceName的服务了,就先将其名称写入到这个临时缓存map
            updatingMap.put(serviceName, new Object());
            // 更新本地注册表中的serviceName的服务
            updateServiceNow(serviceName, clusters);
            // 更新完毕,将该serviceName服务从临时缓存map中干掉
            updatingMap.remove(serviceName);

            // 若当前注册表中已经有了这个服务,那么查看一下临时缓存map中
            // 是否存在该服务。若存在,则说明这个服务正在被更新,所以本次
            // 操作先等待wait一会儿
        } else if (updatingMap.containsKey(serviceName)) {

            if (UPDATE_HOLD_INTERVAL > 0) {
                // hold a moment waiting for update finish
                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 ServiceInfo getServiceInfo0(String serviceName, String clusters) {

        String key = ServiceInfo.getKey(serviceName, clusters);
        // serviceInfoMap即为Client端的本地注册表,
        // 其key为 groupId@@微服务名称@@clusters名称
        // value为ServiceInfo
        return serviceInfoMap.get(key);
    }
  • 通过Key, Value的模式去serviceInfoMap即为Client端的本地注册表获取服务, Key: groupId@@微服务名称@@clusters名称, Value:ServiceInfo
  • 先从当前client的本地注册表中获取当前服务, 若本地注册表中没有该服务,则创建一个, 首先创建一个空的服务, 然后填充属性。
  • updatingMap: updatingMap是一个临时缓存,其主要是使用这个缓存map的key, 使用map的key不能重复这个特性, 只要有服务名称出现在这个缓存map中,就表示当前这个服务正在被更新, 准备要更新serviceName的服务了,就先将其名称写入到这个临时缓存map。
  • updateServiceNow(serviceName, clusters): 更新本地注册表中的serviceName的服务, 更新完毕后从updatingMap删除serviceName服务
  • 若当前注册表中已经有了这个服务,那么查看一下临时缓存map中, 是否存在该服务。若存在,则说明这个服务正在被更新, 操作先等待wait一会儿
  • 最后, 启动一个定时任务,定时更新本地注册表中的当前服务

2.3 updateServiceNow方法解析

更新本地注册表中的serviceName的服务

    private void updateServiceNow(String serviceName, String clusters) {
        try {
            // 更新本地注册表中的serviceName的服务
            updateService(serviceName, clusters);
        } catch (NacosException e) {
            NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
        }
    }

在这里插入图片描述

在这里插入图片描述

processServiceJson方法去将 将来自于Server的ServiceInfo更新到本地注册表, 这里的ServerInfo为向server提交一个GTE请求, 是JSON字符串的形式。

HostReactor#processServiceJson(): 将来自Server的ServerInfo更新至本地注册表, 有一些情况具体分析。

    public ServiceInfo processServiceJson(String json) {
        // 将来自于Server的JSON转换为ServiceInfo
        ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
        // 获取注册表中当前服务的ServiceInfo
        ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
        if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
            //empty or error push, just ignore
            return oldService;
        }

        boolean changed = false;

        // 若当前注册表中存在当前服务,则想办法将来自于server的数据更新到本地注册表
        if (oldService != null) {

            // 为了安全起见,这种情况几乎是不会出现的
            if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
                NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
                        + serviceInfo.getLastRefTime());
            }

            // 将来自于Server的serviceInfo替换掉注册表中的当前服务
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);

            // 遍历本地注册表中当前服务的所有instance实例
            Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
            for (Instance host : oldService.getHosts()) {
                // 将当前遍历的instance主机的ip:port作为key,instance作为value,
                // 写入到一个新的map
                oldHostMap.put(host.toInetAddr(), host);
            }

            // 遍历来自于server的当前服务的所有instance实例
            Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
            for (Instance host : serviceInfo.getHosts()) {
                // 将当前遍历的instance主机的ip:port作为key,instance作为value,
                // 写入到一个新的map
                newHostMap.put(host.toInetAddr(), host);
            }

            // 该set集合中存放的是,两个map(oldHostMap与newHostMap)中都有的ip:port,
            // 但它们的instance不相同,此时会将来自于server的instance写入到这个set
            Set<Instance> modHosts = new HashSet<Instance>();
            // 只有newHostMap中存在的instance,即在server端新增的instance
            Set<Instance> newHosts = new HashSet<Instance>();
            // 只有oldHostMap中存在的instance,即在server端被删除的instance
            Set<Instance> remvHosts = new HashSet<Instance>();

            List<Map.Entry<String, Instance>> newServiceHosts = new ArrayList<Map.Entry<String, Instance>>(
                    newHostMap.entrySet());
            // 遍历来自于server的主机
            for (Map.Entry<String, Instance> entry : newServiceHosts) {
                Instance host = entry.getValue();
                // ip:port
                String key = entry.getKey();
                // 在注册表中存在该ip:port,但这两个instance又不同,则将这个instance写入到modHosts
                if (oldHostMap.containsKey(key) && !StringUtils
                        .equals(host.toString(), oldHostMap.get(key).toString())) {
                    modHosts.add(host);
                    continue;
                }

                // 若注册表中不存在该ip:port,说明这个主机是新增的,则将其写入到newHosts
                if (!oldHostMap.containsKey(key)) {
                    newHosts.add(host);
                }
            }

            // 遍历来自于本地注册表的主机
            for (Map.Entry<String, Instance> entry : oldHostMap.entrySet()) {
                Instance host = entry.getValue();
                String key = entry.getKey();
                if (newHostMap.containsKey(key)) {
                    continue;
                }

                // 注册表中存在,但来自于server的serviceInfo中不存在,
                // 说明这个instance被干掉了,将其写入到remvHosts
                if (!newHostMap.containsKey(key)) {
                    remvHosts.add(host);
                }

            }

            if (newHosts.size() > 0) {
                changed = true;
                NAMING_LOGGER.info("new ips(" + newHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(newHosts));
            }

            if (remvHosts.size() > 0) {
                changed = true;
                NAMING_LOGGER.info("removed ips(" + remvHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(remvHosts));
            }

            if (modHosts.size() > 0) {
                changed = true;
                // 变更心跳信息BeatInfo
                updateBeatInfo(modHosts);
                NAMING_LOGGER.info("modified ips(" + modHosts.size() + ") service: " + serviceInfo.getKey() + " -> "
                        + JacksonUtils.toJson(modHosts));
            }

            serviceInfo.setJsonFromServer(json);

            // 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列
            if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
                eventDispatcher.serviceChanged(serviceInfo);
                DiskCache.write(serviceInfo, cacheDir);
            }

            // 若本地注册表中就没有当前服务,则直接将来自于server的serviceInfo写入到注册表
        } else {
            changed = true;
            NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                    + JacksonUtils.toJson(serviceInfo.getHosts()));
            // 将来自于server的serviceInfo写入到注册表
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
            // 将这个发生变更的serviceInfo记录到一个缓存队列
            eventDispatcher.serviceChanged(serviceInfo);
            serviceInfo.setJsonFromServer(json);
            DiskCache.write(serviceInfo, cacheDir);
        }

        MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());

        if (changed) {
            NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                    + JacksonUtils.toJson(serviceInfo.getHosts()));
        }

        return serviceInfo;
    }
  • 来自于Server的数据是最新的数据, oldService为当前注册表中的服务SericeInfo, 为旧的服务。
  • 若当前注册表中存在当前服务, 将来自于server的数据更新到本地注册表
    • 将来自于Server的serviceInfo替换掉注册表中的当前服务, 保持至serviceInfoMap
    • 遍历本地注册表中当前服务的所有instance实例, 将当前遍历的instance主机的ip:port作为key,instance作为value,写入到一个新的map, oldHostMap
    • 遍历来自于server的当前服务的所有instance实例, 将当前遍历的instance主机的ip:port作为key,instance作为value,入到一个新的map, newHostMap
    • 创建3个Set集合, modHosts存放的是两个map(oldHostMap与newHostMap)中都有的ip:port, 但它们的instance不相同,此时会将来自于server的instance写入到这个set, newHosts存放的是只有newHostMap中存在的instance,即在server端新增的instance, remvHosts存放的是只有oldHostMap中存在的instance,即在server端被删除的instance
    • 遍历来自于server的主机, 如果在注册表中存在该ip:port,但这两个instance又不同,则将这个instance写入到modHosts, 如果若注册表中不存在该ip:port,说明这个主机是新增的,则将其写入到newHosts
    • 遍历来自于本地注册表的主机, 如果注册表中存在,但来自于server的serviceInfo中不存在, 说明这个instance被干掉了,将其写入到remvHosts
    • 如果modHosts中存在对象, 那么变更心跳信息BeatInfo, updateBeatInfo()
    • 只要发生了变更,就将这个发生变更的serviceInfo记录到一个缓存队列, DiskCache
  • 若本地注册表中就没有当前服务,则直接将来自于server的serviceInfo写入到注册表, 将这个发生变更的serviceInfo记录到一个缓存队列, DiskCache

2.4 定时更新本地注册表中的当前服务

HostReactor#scheduleUpdateIfAbsent()
发起一个定时任务, 定时更新本地注册表中的当前服务

在HostReator#getServiceInfo()最后有一个定时任务的代码

在这里插入图片描述

    public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
        // futureMap是一个缓存map,其key为 groupId@@微服务名称@@clusters
        // value是一个定时异步操作对象
        // 这种结构称之为:双重检测锁,DCL,Double Check Lock
        // 该结构是为了避免在并发情况下,多线程重复写入数据
        // 该结构的特征:
        // 1)有两个不为null的判断
        // 2)有共享集合
        // 3)有synchronized代码块
        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));
            // 将这个定时异步操作对象写入到缓存map
            futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
        }
    }

这个是一个DCL结构, 结构是为了避免在并发情况下,多线程重复写入数据。

UpdateTask#run() 启动定时任务

        @Override
        public void run() {
            long delayTime = DEFAULT_DELAY;

            try {
                // 从本地注册表中获取当前服务
                ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));

                // 若本地注册表中不存在该服务,则从server获取到后,更新到本地注册表
                if (serviceObj == null) {
                    // 从server获取当前服务,并更新到本地注册表
                    updateService(serviceName, clusters);
                    return;
                }

                // 处理本地注册表中存在当前服务的情况
                // 1)serviceObj.getLastRefTime() 获取到的是当前服务最后被访问的时间,这个时间
                // 是来自于本地注册表的,其记录的是所有提供这个服务的instance中最后一个instance
                // 被访问的时间
                // 2)缓存lastRefTime 记录的是当前instance最后被访问的时间
                // 若1)时间 小于 2)时间,说明当前注册表应该更新的
                if (serviceObj.getLastRefTime() <= lastRefTime) {
                    updateService(serviceName, clusters);
                    serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
                } else {
                    // if serviceName already updated by push, we should not override it
                    // since the push data may be different from pull through force push
                    refreshOnly(serviceName, clusters);
                }

                // 将来自于注册表的这个最后访问时间更新到当前client的缓存
                lastRefTime = serviceObj.getLastRefTime();

                if (!eventDispatcher.isSubscribed(serviceName, clusters) && !futureMap
                        .containsKey(ServiceInfo.getKey(serviceName, clusters))) {
                    // abort the update task
                    NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
                    return;
                }
                if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                    incFailCount();
                    return;
                }
                delayTime = serviceObj.getCacheMillis();
                resetFailCount();
            } catch (Throwable e) {
                incFailCount();
                NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
            } finally {
                // 开启下一次的定时任务
                executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
            }
        }
  1. 从本地注册表中获取当前服务, 若本地注册表中不存在该服务, Server获取后, 更新到本地注册表ServiceInfo
    在这里插入图片描述
    这里的updateService()在之前分析过。

  2. 处理本地注册表中存在当前服务的情况, 判断当前服务最后被访问的时间和当前instance最后被访问的时间的大小
    如果当前服务最后被访问的时间小于等于当前instance最后被访问的时间的话, 那么说明当前注册表应该更新的
    在这里插入图片描述
    这里的updateService()在之前分析过。

  3. 最后开启下一次的定时任务
    在这里插入图片描述

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

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

相关文章

40亿个QQ号,限制1G内存,如何去重?

40亿个QQ号&#xff0c;限制1G内存&#xff0c;如何去重&#xff1f; 40亿个unsigned int&#xff0c;如果直接用内存存储的话&#xff0c;需要&#xff1a; 4*4000000000 /1024/1024/1024 14.9G &#xff0c;考虑到其中有一些重复的话&#xff0c;那1G的空间也基本上是不够…

OPPO哲库事件 “ 始末 ” ! 集体打哑谜?

1►OPPO哲库解散 2019 年&#xff0c;美国商务部以“科技网络安全”为由&#xff0c;将华为公司及其70家附属公司列入出口管制“实体名单”。与此同时&#xff0c;OPPO 创始人兼 CEO陈明永对外宣布&#xff0c;公司将为未来三年内投入 500 亿元用于前沿技术和深水区技术的探索…

安装编译PostgreSql15.3.0

一、下载源码 方式一 官网手动下载 https://www.postgresql.org/download/. 解压 tar -zxvf postgresql-14.2.tar.gz方式二 git clone git clone https://github.com/postgres/postgres.git解压或下载后计入postgres目录 cd postgres-15.3二、创建目录 用root账户创建 创建…

Apache Pulsar入门指南

1.概述 Apache Pulsar 是灵活的发布-订阅消息系统&#xff08;Flexible Pub/Sub messaging&#xff09;&#xff0c;采用计算与存储分离的架构。雅虎在 2013 年开始开发 Pulsar &#xff0c;于 2016 年首次开源&#xff0c;目前是 Apache 软件基金会的顶级项目。Pulsar 具有支…

C++系列六:运算符

C运算符 1. 算术运算符2. 关系运算符3. 逻辑运算符4. 按位运算符5. 取地址运算符6. 取内容运算符7. 成员选择符8. 作用域运算符9. 总结 1. 算术运算符 算术运算符用于执行基本数学运算&#xff0c;例如加减乘除和取模等操作。下表列出了C中支持的算术运算符&#xff1a; 运算…

为什么要做问卷调查?企业获得用户心声的捷径

问卷调查作为一种重要的数据收集方法&#xff0c;在市场营销、社会学研究、用户研究等领域得到广泛应用。通过问卷调查&#xff0c;我们可以了解受访者的态度、行为、需求等信息&#xff0c;进而为企业和组织的决策提供支持。那么&#xff0c;为什么要做问卷调查呢&#xff1f;…

day5 - 利用阈值勾勒

阈值处理在计算机视觉技术中占有十分重要的位置&#xff0c;他是很多高级算法的底层逻辑之一。本实验将练习使用图像阈值处理技术来处理不同的情况的图像&#xff0c;并获得图像轮廓。 完成本期内容&#xff0c;你可以&#xff1a; 了解图像阈值处理技术的定义和作用 掌握各阈…

苏州狮山广场能耗管理系统

摘要&#xff1a;随着社会生活水平的提高&#xff0c;经济的繁荣发展&#xff0c;人们对能源的需求逐渐增长&#xff0c;由此带来的能源危机日益严重。商场如何实时的了解、分析和控制商场的能源消耗已成为需要解决的迫在眉睫的难题。传统的能源消耗智能以月/季度/年为周期进行…

Autosar RTE S/R接口implicit与Explicit的实现与区别

文章目录 前言接口的代码implicitIReadIWrite ExplicitReadWrite 区别与使用场景总结 前言 Autosar官方文档阅读起来比较费劲&#xff0c;一般从实际应用中来了解更多规范中的内容。本文介绍最常用的RTE S/R接口的implicit隐式与Explicit显式两种方式的实现与差别 接口的代码…

Node.js

目录 Node.js是什么 入门案例 fs文件系统模块 案例 http模块 创建最简单的web服务器 网页跳转案例 模块化 模块化概念 模块化规范 Node.js 中模块的分类 加载模块 模块作用域 module对象 Node.js中的模块化规范 第三方模块 (包) 安装包的命令 卸载包的命令 …

oracle客户端的安装教程

文章目录 一、安装前的准备工作 1.1、百度网盘安装包的连接 1.2、百度网盘oracle11g软件包 二、oracle数据库客户端的安装与数据的准备 安装步骤 前言 本文主要讲解oracle客户端的安装与简单使用过程 一、安装前的准备工作 1.1、百度网盘安装包的连接 客户端的软件包 …

【Java EE 初阶】网络编程套接字UDP

目录 1.为什么需要网络编程&#xff1f; 2.什么是网络编程&#xff1f; 3.发送端和接收端 4.请求和响应 5.客户端和服务端 6.如何进行网络编程&#xff08;Socket套接字&#xff09; 1.如何进行网络编程 2.TCP与UDP的区别 1.流套接字&#xff1a;使用传输层TCP协议 2.…

面了一个测试工程师要求月薪23K,总感觉他藏了很多面试题...

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

【前端知识】Cookie, Session,Token和JWT的发展及区别(四)

【前端知识】Cookie, Session,Token和JWT的发展及区别&#xff08;四&#xff09; 9. JWT9.1 JWT的背景及定义&#xff08;1&#xff09;JWT的字面理解&#xff08;2&#xff09;JWT与传统Token的区别 9.2 JWT的组成&#xff08;1&#xff09; Header&#xff08;头部&#xff…

【负载均衡式在线OJ】 数据库

文章目录 41.使用Postman进行综合调试42.了解-前端预备52. 添加oj用户到MySQL53. 使用MySQL_Workbench创建表结构54. 测试录题功能55.重新设计oj_model56.编写oj_model具体代码57.MySQL综合测试58.结项与项目扩展思路 41.使用Postman进行综合调试 完善判题功能 先编译再测试 …

项目管理PMP好考吗,没有经验?

现在越来越多的产品经理和开发人员也投入到考PMP的大军中&#xff0c;在真实的项目中也会有很多产品经理兼任项目经理的职责&#xff0c;这点还是比较常见的&#xff0c;如果说产品或者开发人员考了PMP证书&#xff0c;本身也会让你在找工作的大军中更具有优势&#xff0c;俗话…

一文读懂selenium自动化测试(基于Python)

前言 我们今天来聊聊selenium自动化测试&#xff0c;我们都知道selenium是一款web自动化测试的工具&#xff0c;它应该如何去运用呢?我们接着看下去。 ​1、Selenium简介&#xff1a; 1.1 Selenium&#xff1a; Selenium是一款主要用于Web应用程序自动化测试的工具集合。Sele…

Vue事件

1&#xff0c;回顾js中的事件&#xff1f; 答&#xff1a;标签的状态变化或者被外物改变则称为事件。一般js中的事件都是由浏览器捕捉得到&#xff0c;然后传递给js引擎&#xff0c;浏览器检测到HTML页面中某个标签元素发生了指定的事件&#xff0c;而对应的DOM节点必须去调用回…

Python系列模块之标准库re详解

感谢点赞和关注 &#xff0c;每天进步一点点&#xff01;加油&#xff01; 目录 一、Python 正则表达式 1.1 re模块常用操作 1.2 re.match 1.3 re.search 1.4 re.findall 1.5 re.compile 函数 1.6 re.sub 检索和替换 1.7 re.split拆分 1.8 实战案例&#xff1a;根据文…

全网最全性能测试总结,分析性能测试问题+性能调优方案...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能分析和优化一…