Nacos的服务心跳

news2024/11/17 3:07:12

nacos的实例分为临时实例和永久实例两种,相应的不同的实例会用有不同的心跳机制.
临时实例基于心跳方式做健康检测,永久实例是有Nacos主动探测实例状态.
可以通过在yaml文件配置.

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        ephemeral: false # 设置实例为永久实例。true:临时; false:永久
      server-addr: 192.168.150.1:8845

Nacos提供的心跳的API接口为:/nacos/v1/ns/instance/beat

客户端

NacosNamingService这个接口实现了服务心跳的功能

    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {

        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());

            // 发送心跳到 Nacos 服务
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

BeatInfo
从上面的代码可以看到BeatInfo就是包含心跳需要的各种信息,


/**
 * @author nkorange
 */
public class BeatInfo {
    private int port;
    private String ip;
    private double weight;
    private String serviceName;
    private String cluster;
    private Map<String, String> metadata;
    private volatile boolean scheduled;
    private volatile long period;
    private volatile boolean stopped;
}

BeatReactor
这个类中维护了一个线程池;

    public BeatReactor(NamingProxy serverProxy, int threadCount) {
        this.serverProxy = serverProxy;
        executorService = new ScheduledThreadPoolExecutor(threadCount, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setName("com.alibaba.nacos.naming.beat.sender");
                return thread;
            }
        });
    }

当调用addBeatInfo方法的时候,就会执行心跳:

    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        // 利用线程池,定期执行心跳任务,周期为 beatInfo.getPeriod()
        // 心跳周期的默认值在 com.alibaba.nacos.api.common.Constants 类中
        // public static final long DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5);
        // 可以看到是5秒,默认5秒一次心跳
        // BeatTask:是一个Runnable
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

BeatTask
心跳的任务封装在 BeatTask这个类中,是一个Runnable,其run方法如下:

        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            // 获取心跳周期
            long nextTime = beatInfo.getPeriod();
            try {
                // 发送心跳
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
                long interval = result.getIntValue("clientBeatInterval");
                boolean lightBeatEnabled = false;
                if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
                    lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
                }
                BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                if (interval > 0) {
                    nextTime = interval;
                }
                // 判断心跳结果
                int code = NamingResponseCode.OK;
                if (result.containsKey(CommonParams.CODE)) {
                    code = result.getIntValue(CommonParams.CODE);
                }
                if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                    // 如果失败,则需要 重新注册实例
                    Instance instance = new Instance();
                    instance.setPort(beatInfo.getPort());
                    instance.setIp(beatInfo.getIp());
                    instance.setWeight(beatInfo.getWeight());
                    instance.setMetadata(beatInfo.getMetadata());
                    instance.setClusterName(beatInfo.getCluster());
                    instance.setServiceName(beatInfo.getServiceName());
                    instance.setInstanceId(instance.getInstanceId());
                    instance.setEphemeral(true);
                    try {
                        serverProxy.registerService(beatInfo.getServiceName(),
                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                    } catch (Exception ignore) {
                        // 捕获异常,什么都不干
                    }
                }
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

            }
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }

发送心跳

    public JSONObject sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {

        if (NAMING_LOGGER.isDebugEnabled()) {
            NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
        }
        // 组织请求参数
        Map<String, String> params = new HashMap<String, String>(8);
        String body = StringUtils.EMPTY;
        if (!lightBeatEnabled) {
            body = "beat=" + JSON.toJSONString(beatInfo);
        }
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
        params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
        params.put("ip", beatInfo.getIp());
        params.put("port", String.valueOf(beatInfo.getPort()));
        // 发送请求,这个地址就是:/v1/ns/instance/beat
        String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, HttpMethod.PUT);
        return JSON.parseObject(result);
    }

服务端

对于临时实例,服务端代码分了两部分:
(1) InstanceController提供了一个接口,处理客户端的心跳请求
(2) 定时检测实例心跳是否按期执行
可以根据客户端发起心跳检测的接口找到在InstanceController类中,定义了一个方法来处理心跳请求:

    @CanDistro
    @PutMapping("/beat")
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
    public JSONObject beat(HttpServletRequest request) throws Exception {

        JSONObject result = new JSONObject();

        result.put("clientBeatInterval", switchDomain.getClientBeatInterval());
        // 解析心跳的请求参数
        // 获取 serviceName
        String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        // 获取 namespaceId
        String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
            Constants.DEFAULT_NAMESPACE_ID);
        // 获取clusterName
        String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME,
            UtilsAndCommons.DEFAULT_CLUSTER_NAME);
        // 获取ip
        String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
        // 获取port
        int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
        String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);

        RsInfo clientBeat = null;
        if (StringUtils.isNotBlank(beat)) {
            clientBeat = JSON.parseObject(beat, RsInfo.class);
        }

        if (clientBeat != null) {
            if (StringUtils.isNotBlank(clientBeat.getCluster())) {
                clusterName = clientBeat.getCluster();
            }
            ip = clientBeat.getIp();
            port = clientBeat.getPort();
        }

        if (Loggers.SRV_LOG.isDebugEnabled()) {
            Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
        }

        // 尝试从 Nacos 注册表中 获取实例
        Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);

        // 如果获取失败,说明心跳失败,实例尚未注册
       
        if (instance == null) {
            if (clientBeat == null) {
             	// 对应客户端中,心跳失败,则注册实例的代码
                result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND);
                return result;
            }
            instance = new Instance();
            instance.setPort(clientBeat.getPort());
            instance.setIp(clientBeat.getIp());
            instance.setWeight(clientBeat.getWeight());
            instance.setMetadata(clientBeat.getMetadata());
            instance.setClusterName(clusterName);
            instance.setServiceName(serviceName);
            instance.setInstanceId(instance.getInstanceId());
            instance.setEphemeral(clientBeat.isEphemeral());

            // 重新注册一个实例
            serviceManager.registerInstance(namespaceId, serviceName, instance);
        }

        // 尝试基于 namespaceId 和 serviceName 从注册表中获取 Service 服务
        Service service = serviceManager.getService(namespaceId, serviceName);

        // 如果不存在,说明服务不存在,返回404
        if (service == null) {
        	
            throw new NacosException(NacosException.SERVER_ERROR,
                "service not found: " + serviceName + "@" + namespaceId);
        }
        if (clientBeat == null) {
            clientBeat = new RsInfo();
            clientBeat.setIp(ip);
            clientBeat.setPort(port);
            clientBeat.setCluster(clusterName);
        }
        // 如果心跳没问题,开始处理心跳结果
        service.processClientBeat(clientBeat);

        result.put(CommonParams.CODE, NamingResponseCode.OK);
        result.put("clientBeatInterval", instance.getInstanceHeartBeatInterval());
        result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
        return result;
    }

处理心跳请求

    public void processClientBeat(final RsInfo rsInfo) {
        ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
        clientBeatProcessor.setService(this);
        clientBeatProcessor.setRsInfo(rsInfo);
        HealthCheckReactor.scheduleNow(clientBeatProcessor);
    }

HealthCheckReactor就是对线程池的封装,关键在于ClientBeatProcessor这个类中,他是一个Runnable,其中run方法:

    public void run() {
        Service service = this.service;
        if (Loggers.EVT_LOG.isDebugEnabled()) {
            Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
        }

        String ip = rsInfo.getIp();
        String clusterName = rsInfo.getCluster();
        int port = rsInfo.getPort();
        // 获取集群信息
        Cluster cluster = service.getClusterMap().get(clusterName);
        // 获取集群中的所有实例信息
        List<Instance> instances = cluster.allIPs(true);

        for (Instance instance : instances) {
            // 找到心跳的这个实例
            if (instance.getIp().equals(ip) && instance.getPort() == port) {
                if (Loggers.EVT_LOG.isDebugEnabled()) {
                    Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());
                }
                // 更新实例的最后依一次心跳时间 lastBeat
                // lastBeat 是判断实例心跳是否过期的关键指标!
                instance.setLastBeat(System.currentTimeMillis());
                if (!instance.isMarked()) {
                    if (!instance.isHealthy()) {
                        instance.setHealthy(true);
                        Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",
                            cluster.getService().getName(), ip, port, cluster.getName(), UtilsAndCommons.LOCALHOST_SITE);
                        getPushService().serviceChanged(service);
                    }
                }
            }
        }
    }

心跳异常检测
在服务注册时,一定会创建一个Service对象,而Service中有一个init方法,会在注册的时候被调用

    public void init() {

        // 开启心跳检测的任务
        // 执行心跳检测的定时任务
        HealthCheckReactor.scheduleCheck(clientBeatCheckTask);

        // 遍历注册表中的集群
        for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
            entry.getValue().setService(this);
            // 完成集群初始化
            entry.getValue().init();
        }
    }


    public static void scheduleCheck(ClientBeatCheckTask task) {
        // 5000ms一次,也就是5秒对实例的心跳状态做一次检测
        // task:是一个 Runnable
        futureMap.putIfAbsent(task.taskKey(), EXECUTOR.scheduleWithFixedDelay(task, 5000, 5000, TimeUnit.MILLISECONDS));
    }

ClientBeatCheckTask

    public void run() {
        try {
            if (!getDistroMapper().responsible(service.getName())) {
                return;
            }

            if (!getSwitchDomain().isHealthCheckEnabled()) {
                return;
            }

            // 找到所有 临时 实例的列表
            List<Instance> instances = service.allIPs(true);

            // first set health status of instances:
            for (Instance instance : instances) {
                // 判断时间间隔(当前时间 - 最后一次心跳时间)是否大于 心跳超时时间,默认15秒
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
                    if (!instance.isMarked()) {
                        if (instance.isHealthy()) {
                            // 如果超时,标记实例为不健康 healthy = false
                            instance.setHealthy(false);
                            Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
                                instance.getIp(), instance.getPort(), instance.getClusterName(), service.getName(),
                                UtilsAndCommons.LOCALHOST_SITE, instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
                            // 发布实例状态变更的事件
                            getPushService().serviceChanged(service);
                            SpringContext.getAppContext().publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
                        }
                    }
                }
            }

            if (!getGlobalConfig().isExpireInstance()) {
                return;
            }

            // then remove obsolete instances:
            for (Instance instance : instances) {

                if (instance.isMarked()) {
                    continue;
                }

                // 判断心跳间隔(当前事件 - 最后一次心跳时间)是否大于 实例被删除的最长超时间,默认30秒
                if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
                    // delete instance
                    Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(), JSON.toJSONString(instance));
                    // 如果超过了 30 秒,则删除实例
                    deleteIP(instance);
                }
            }

        } catch (Exception e) {
            Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
        }

    }

主动健康检测
对于非实例,nacos会采用主动的健康检测,定时向实例发送请求,根据响应来判断实例健康状态.
入口是从ServiceManager类中的registerInstance方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面看一下集群初始化的init方法

    public void init() {
        if (inited) {
            return;
        }
        // 创建健康检测的任务
        checkTask = new HealthCheckTask(this);

        // 这里会开启对 非临时实例的 定时健康检测
        HealthCheckReactor.scheduleCheck(checkTask);
        inited = true;
    }

和上面的init方法一样,也是会创建一个任务HealthCheckTask,并且放到线程池里,进行定时检测

    public void run() {

        try {
            if (distroMapper.responsible(cluster.getService().getName()) &&
                switchDomain.isHealthCheckEnabled(cluster.getService().getName())) {
                // 开启健康检测
                healthCheckProcessor.process(this);
                // 记录日志
                if (Loggers.EVT_LOG.isDebugEnabled()) {
                    Loggers.EVT_LOG.debug("[HEALTH-CHECK] schedule health check task: {}", cluster.getService().getName());
                }
            }
        } catch (Throwable e) {
            Loggers.SRV_LOG.error("[HEALTH-CHECK] error while process health check for {}:{}",
                cluster.getService().getName(), cluster.getName(), e);
        } finally {
            if (!cancelled) {
                // 结束后,再次进行任务调度,一定延迟后执行
                HealthCheckReactor.scheduleCheck(this);

                // worst == 0 means never checked
                if (this.getCheckRTWorst() > 0
                    && switchDomain.isHealthCheckEnabled(cluster.getService().getName())
                    && distroMapper.responsible(cluster.getService().getName())) {
                    // TLog doesn't support float so we must convert it into long
                    long diff = ((this.getCheckRTLast() - this.getCheckRTLastLast()) * 10000)
                        / this.getCheckRTLastLast();

                    this.setCheckRTLastLast(this.getCheckRTLast());

                    Cluster cluster = this.getCluster();

                    if (Loggers.CHECK_RT.isDebugEnabled()) {
                        Loggers.CHECK_RT.debug("{}:{}@{}->normalized: {}, worst: {}, best: {}, last: {}, diff: {}",
                            cluster.getService().getName(), cluster.getName(), cluster.getHealthChecker().getType(),
                            this.getCheckRTNormalized(), this.getCheckRTWorst(), this.getCheckRTBest(),
                            this.getCheckRTLast(), diff);
                    }
                }
            }
        }
    }

健康检测逻辑定义在里healthCheckProcessor.process(this);方法中,在HealthCheckProcessor中,这个接口的默认实现是TcpSuperSenseProcessor
在这里插入图片描述

    public void process(HealthCheckTask task) {
        // 获取所有 非临时实例的 集合
        List<Instance> ips = task.getCluster().allIPs(false);

        if (CollectionUtils.isEmpty(ips)) {
            return;
        }

        for (Instance ip : ips) {

            if (ip.isMarked()) {
                if (SRV_LOG.isDebugEnabled()) {
                    SRV_LOG.debug("tcp check, ip is marked as to skip health check, ip:" + ip.getIp());
                }
                continue;
            }

            if (!ip.markChecking()) {
                SRV_LOG.warn("tcp check started before last one finished, service: "
                    + task.getCluster().getService().getName() + ":"
                    + task.getCluster().getName() + ":"
                    + ip.getIp() + ":"
                    + ip.getPort());

                healthCheckCommon.reEvaluateCheckRT(task.getCheckRTNormalized() * 2, task, switchDomain.getTcpHealthParams());
                continue;
            }

            // 封装健康检测信息到 beat
            Beat beat = new Beat(ip, task);
            // 放入到一个阻塞队列中
            taskQueue.add(beat);
            MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet();
        }
    }

可以看到nacos中有很多这种操作,不是立即去执行,而是通过放到阻塞队列里面,进行异步执行.
因为TcpSuperSenseProcessor是一个Runnable,所以我们可以直接看他的run接口:

    public void run() {
        while (true) {
            try {
                // 处理任务
                processTask();

                int readyCount = selector.selectNow();
                if (readyCount <= 0) {
                    continue;
                }

                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove();

                    NIO_EXECUTOR.execute(new PostProcessor(key));
                }
            } catch (Throwable e) {
                SRV_LOG.error("[HEALTH-CHECK] error while processing NIO task", e);
            }
        }
    }
    private void processTask() throws Exception {
        Collection<Callable<Void>> tasks = new LinkedList<>();
        do {
        	// 取出beat
            Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS);
            if (beat == null) {
                return;
            }

            // 将任务封装为一个TaskProcessor,并放入集合
            tasks.add(new TaskProcessor(beat));
        } while (taskQueue.size() > 0 && tasks.size() < NIO_THREAD_COUNT * 64);

        // 批量处理集合中的任务
        for (Future<?> f : NIO_EXECUTOR.invokeAll(tasks)) {
            f.get();
        }
    }

接着看TaskProcessor,因为是一个callable的线程,所以直接看call方法

        public Void call() {
            // 获取检测任务已经等待的时长
            long waited = System.currentTimeMillis() - beat.getStartTime();
            if (waited > MAX_WAIT_TIME_MILLISECONDS) {
                Loggers.SRV_LOG.warn("beat task waited too long: " + waited + "ms");
            }

            SocketChannel channel = null;
            try {
                // 获取实例信息
                Instance instance = beat.getIp();
                Cluster cluster = beat.getTask().getCluster();

                BeatKey beatKey = keyMap.get(beat.toString());
                if (beatKey != null && beatKey.key.isValid()) {
                    if (System.currentTimeMillis() - beatKey.birthTime < TCP_KEEP_ALIVE_MILLIS) {
                        instance.setBeingChecked(false);
                        return null;
                    }

                    beatKey.key.cancel();
                    beatKey.key.channel().close();
                }

                // 通过NIO建立TCP连接
                channel = SocketChannel.open();
                channel.configureBlocking(false);
                // only by setting this can we make the socket close event asynchronous
                channel.socket().setSoLinger(false, -1);
                channel.socket().setReuseAddress(true);
                channel.socket().setKeepAlive(true);
                channel.socket().setTcpNoDelay(true);

                int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport();
                channel.connect(new InetSocketAddress(instance.getIp(), port));

                // 注册连接、读取事件
                SelectionKey key
                    = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
                key.attach(beat);
                keyMap.put(beat.toString(), new BeatKey(key));

                beat.setStartTime(System.currentTimeMillis());

                NIO_EXECUTOR.schedule(new TimeOutTask(key),
                    CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(), "tcp:error:" + e.getMessage());

                if (channel != null) {
                    try {
                        channel.close();
                    } catch (Exception ignore) {
                    }
                }
            }

            return null;
        }
    }

Nacos的健康检测有两种模式:

  1. 临时实例:
    采用客户端心跳检测模式,心跳周期5秒
    心跳间隔超过15秒则标记为不健康
    心跳间隔超过30秒则从服务列表删除
  2. 永久实例:
    采用服务端主动健康检测方式
    周期为2000 + 5000毫秒内的随机数
    检测异常只会标记为不健康,不会删除

以淘宝为例,双十一大促期间,流量会比平常高出很多,此时服务肯定需要增加更多实例来应对高并发,而这些实例在双十一之后就无需继续使用了,采用临时实例比较合适。而对于服务的一些常备实例,则使用永久实例更合适。

与eureka相比,Nacos与Eureka在临时实例上都是基于心跳模式实现,差别不大,主要是心跳周期不同,eureka是30秒,Nacos是5秒。

另外,Nacos支持永久实例,而Eureka不支持,Eureka只提供了心跳模式的健康监测,而没有主动检测功能。

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

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

相关文章

举个栗子~Minitab 技巧(6):使用 T 检验 分析产品质量

在企业生产过程中&#xff0c;我们往往需要对产品质量负责。最常见的场景是&#xff0c;如何判断生产线中的产品&#xff0c;是否满足质量标准&#xff1f;比如&#xff0c;某工厂现有一批产品&#xff0c;目标生产重量是 20g&#xff0c;通过随机抽取不同的产品&#xff0c;如…

springsecurity

目录 一、权限管理简介 1、什么是权限管理 2、认证 2、基于资源的访问控制 三、Spring Security概述 1&#xff0c;Spring Security简介 2、Spring Security快速入门 2.1、引入依赖 2.2、创建一个控制器 2.3、启动项目 四、Spring Security 认证配置 1、WebSecurity…

公司自研组件库打包之后chunk.css文件25W行代码

项目场景&#xff1a; 基于Antd开发的UI组件库&#xff0c;主要分为两部分。 一部分是基础组件&#xff0c;直接在Antd的基础上包了一层&#xff0c;然后根据自身需求拓展了新的功能。如&#xff1a; input的状态除了本身支持的error和warning两种&#xff0c;额外增加了成功的…

手写JS—深拷贝

什么是深拷贝 一个引用对象一般来说由两个部分组成&#xff1a;一个具名的Handle&#xff0c;也就是我们所说的声明&#xff08;如变量&#xff09;和一个内部&#xff08;不具名&#xff09;的对象&#xff0c;也就是具名Handle的内部对象。它在Manged Heap&#xff08;托管堆…

Real diff算法是怎么运作的?

React 的 Reconciliation 算法原理 React 的渲染机制 Reconciliation 过程 React 采用的是虚拟 DOM (即 VDOM )&#xff0c;每次属性 (props) 和状态 (state) 发生变化的时候&#xff0c;render 函数返回不同的元素树&#xff0c;React 会检测当前返回的元素树和上次渲染的元素…

论文阅读笔记 | 三维目标检测——PV-RCNN算法

如有错误&#xff0c;恳请指出。 文章目录1. 背景2. 网络结构2.1 Feature Encoder and Proposal Generation2.2 voxel-to-keypoint scene encoding2.3 Keypoint-to-grid RoI Feature Abstraction3. 实验部分paper&#xff1a;《PV-RCNN: Point-Voxel Feature Set Abstraction f…

JavaScript -- DOM事件总结

文章目录事件1 事件对象简介2 Event对象3 冒泡4 事件的委派5 事件的捕获事件 1 事件对象简介 事件对象是由浏览器在事件触发时所创建的对象&#xff0c;这个对象中封装了事件相关的各种信息通过事件对象可以获取到事件的详细信息比如&#xff1a;鼠标的坐标、键盘的按键…浏览…

单频信号的相位谱计算与误差修正-附Matlab代码

一、问题描述 我们在实际处理时经常遇到只有一个正弦信号的情况&#xff0c;其频率为 f0{{f}_{0}}f0​&#xff0c;在谱分析以后&#xff0c;除了在频率为 f0{{f}_{0}}f0​处有相位数值外&#xff0c;其他频率处都有相位数值&#xff0c;分析其他频谱出现相位值的原因。 例如…

外部tomcat资源整合

Spring Boot应用默认是以jar包方式运行的&#xff0c;Springboot默认有内置的tomcat&#xff0c;在启动的时候会自动的将项目发布&#xff0c;这样各有利弊。 优点&#xff1a;简单&#xff0c;便携 缺点&#xff1a;不支持jsp, 定制优化比较麻烦&#xff0c;需要自己编写ser…

如何开发一个婚恋交友系统?开发功能特点有什么?

婚恋平台为年轻人开启了一个交流恋爱的方式&#xff0c;一方面为的是适龄的年轻单身人士&#xff0c;另一方面为一部分大龄单身人士&#xff0c;解决单很多身人 士的需求&#xff0c;婚恋平台的开发&#xff0c;跟随着互联网的发展&#xff0c;抢占了小程序的热门市场&#xf…

Java线程实现

内容引用自《深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践&#xff08;第3版&#xff09;周志明》 线程的实现 我们知道&#xff0c;线程是比进程更轻量级的调度执行单位&#xff0c;线程的引入&#xff0c;可以把一个进程的资源分配和 执行调度分开&#xff0c;各个…

React框架入门

React是用于构建用户界面的JavaScript库&#xff0c; 起源于Facebook的内部项目&#xff0c;该公司对市场上所有 JavaScript MVC框架都不满意&#xff0c;决定自行开发一套&#xff0c;用于架设Instagram的网站。于2013年5月开源 一、React简介 React以声明式编写 UI&a…

0119 动态规划 Day8

剑指 Offer 10- I. 斐波那契数列 写一个函数&#xff0c;输入 n &#xff0c;求斐波那契&#xff08;Fibonacci&#xff09;数列的第 n 项&#xff08;即 F(N)&#xff09;。斐波那契数列的定义如下&#xff1a; F(0) 0, F(1) 1 F(N) F(N - 1) F(N - 2), 其中 N > 1…

计算机的硬件系统和软件系统的关系

计算机的硬件系统和软件系统的关系是缺一不可。 硬件它是所有软件运行的物质基础。 与硬件直接接触的是操作系统&#xff0c;它处在硬件和其他软件之间&#xff0c;表示它向下控制硬件&#xff0c;向上支持其他软件。 在操作系统之外的各层分别是各种语言处理程序、数据库管理…

CleanMyMacX4.12.1Crack版本频繁弹密码 菜单浮窗无法显示显示空白解决办法

你们有没有出现过在使用 CleanMyMac 清理系统垃圾文件的时候会频繁弹出输入开机密码&#xff1f;那么该如何解决这个问题呢&#xff1f;跟着小编来看看解决方法吧&#xff01; 频繁输入密码 更新CleanMyMacX到4.12.1的Crack版本之后&#xff0c;发现做一些操作要一直输入密码&…

科技云报道:PingCAP黄东旭:Serverless是数据库的未来形态

科技云报道原创。 30年前&#xff0c;程序员要想写代码&#xff0c;必须使用复杂的汇编语言。 但在今天&#xff0c;几乎没有程序员知道如何使用汇编语言&#xff0c;更加简易的高级语言如C&#xff0c;C#&#xff0c;JAVA&#xff0c;Rust&#xff0c;Go已成为开发主流。 随…

分布式文件存储系统FastDFS[3]-通过Docker安装并且从客户端进行上传下载测试

一、FastDFS安装 FastDFS的安装我们还是通过Docker来安装实现吧&#xff0c;直接在Linux上还装还是比较繁琐的&#xff0c;但就学习而言Docker安装还是非常高效的。Docker环境请自行安装哦&#xff0c;不清楚的可以看看我的Docker专题的内容。https://blog.csdn.net/qq_3852657…

【jenkins】1. 安装jenkins (docker-compose)

环境 ubuntu 20docker服务器 ip:xxx.xxx.xxx.xxx 步骤 1. 【编写安装文件】windows下 - 编写 docker-compose.yaml version: "3.1" services:jenkins:image: jenkins/jenkinsrestart: alwayscontainer_name: jenkinsports:- 58080:8080- 50000:50000volumes:- ./…

5.31 综合案例2.0 - 在线MP3音乐播放器

综合案例2.0 - 在线MP3音乐播放器一、案例说明二、准备器件三、案例连线四、代码代码说明复制五、测试一、案例说明 本案例制作一个联网下载声音文件的MP3播放器。 案例功能说明&#xff1a; 案例使用一块IC035串口屏和两个按键通过按键可以切换音乐&#xff0c;下载播放或删除…

瑜岿科技综合能源管理系统助力企业节能降耗工作

能源是国民经济的基础&#xff0c;更是城市赖以发展的动力。优化能源结构、大力发展可再生能源、提高机房能效、实现建筑智慧节能是行业绿色发展重点。在国家碳达峰碳中和的重大战略决策背景下&#xff0c;我国能源体制改革深入推进&#xff0c;能源生产和消费发生重大变革&…