0202心跳和服务续约源码解析-nacos2.x-微服务架构

news2025/1/21 2:51:28

文章目录

    • 1 客户端心跳任务
    • 2 服务端处理
      • 2.1 服务注册时开启客户端心跳检查
      • 2.2 客户端发送心跳任务续约
      • 2.3 服务实例移除
      • 2.4 心跳任务闭环
    • 结语

1 客户端心跳任务

在上一篇文章==0201服务注册源码解析-nacos2.x-微服务架构==分析客户端服务注册的时候,流程在NacosNamingService#registerInstance()的方法中,调用registerService()方法之前先执行了客户端发送心跳任务。源代码如下1-1所示:

@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);
        long instanceInterval = instance.getInstanceHeartBeatInterval();
        beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);

        beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
    }

    serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
  • 判断实例为临时实例,执行发送定时心跳任务
  • 封装心跳对象-BeatInfo类型,设置服务名称、Ip、端口、集群名称等等信息
  • BeatReactor对象添加心跳任务

我们来看下BeatReactor是干嘛的?源代码如下1-2所示:

package com.alibaba.nacos.client.naming.beat;

import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.client.monitor.MetricsMonitor;
import com.alibaba.nacos.client.naming.net.NamingProxy;
import com.alibaba.nacos.client.naming.utils.UtilAndComs;

import java.util.Map;
import java.util.concurrent.*;

import static com.alibaba.nacos.client.utils.LogUtils.NAMING_LOGGER;

/**
 * @author harold
 */
public class BeatReactor {

    private ScheduledExecutorService executorService;

    private NamingProxy serverProxy;

    public final Map<String, BeatInfo> dom2Beat = new ConcurrentHashMap<String, BeatInfo>();

    public BeatReactor(NamingProxy serverProxy) {
        this(serverProxy, UtilAndComs.DEFAULT_CLIENT_BEAT_THREAD_COUNT);
    }

    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;
            }
        });
    }

    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        dom2Beat.put(buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()), beatInfo);
        executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

    public void removeBeatInfo(String serviceName, String ip, int port) {
        NAMING_LOGGER.info("[BEAT] removing beat: {}:{}:{} from beat map.", serviceName, ip, port);
        BeatInfo beatInfo = dom2Beat.remove(buildKey(serviceName, ip, port));
        if (beatInfo == null) {
            return;
        }
        beatInfo.setStopped(true);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

    private String buildKey(String serviceName, String ip, int port) {
        return serviceName + Constants.NAMING_INSTANCE_ID_SPLITTER
            + ip + Constants.NAMING_INSTANCE_ID_SPLITTER + port;
    }

    class BeatTask implements Runnable {

        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

        @Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long result = serverProxy.sendBeat(beatInfo);
            long nextTime = result > 0 ? result : beatInfo.getPeriod();
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }
}
  • ScheduledExecutorService executorService:定时任务线程池

  • NamingProxy serverProxy:简单理解提供远程调用

  • Map<String, BeatInfo> dom2Beat ConcurrentHashMap类型:心跳任务缓存

  • BeatTask:心跳线程,线程run方法通过serverProxy发起远程调用,把心跳信息发送给nacos服务端。根据返回时间重新,通过定时任务线程池执行新的心跳任务。该线程被设置为守护线程。

客户端心跳任务执行核心逻辑:

  1. 心跳管理BeatReactor缓存心跳对象

  2. ScheduledExecutorService 定时任务线程池执行心跳任务(线程)

  3. NamingProxy发起远程调用,根据结果,重新执行步骤2来维持心跳

通过开启守护线程,定时发送心跳更新时间这种机制,有没有很熟悉的感觉?回想下redis 分布式锁或者红锁算法。

2 服务端处理

我们来到服务端这边看下,做了那些关于客户端心跳相关的处理呢?

2.1 服务注册时开启客户端心跳检查

示意图2.1-1如下所示:

在这里插入图片描述

首先,在之前我们讲解服务端服务注册的时候,提到创建Client的时候,代码2.1-1如下

    private void createIpPortClientIfAbsent(String clientId) {
        if (!clientManager.contains(clientId)) {
            // 忽略客户端创建
            clientManager.clientConnected(clientId, clientAttributes);
        }
    }

默认时临时的,我们继续查看EphemeralIpPortClientManager#()方法,代码2.1-2如下:

    @Override
    public boolean clientConnected(final Client client) {
        clients.computeIfAbsent(client.getClientId(), s -> {
            Loggers.SRV_LOG.info("Client connection {} connect", client.getClientId());
            IpPortBasedClient ipPortBasedClient = (IpPortBasedClient) client;
            ipPortBasedClient.init();
            return ipPortBasedClient;
        });
        return true;
    }
    

ipPortBasedClient.init()继续查看初始化方法,代码2.1-3:

    public void init() {
        if (ephemeral) {
            beatCheckTask = new ClientBeatCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(beatCheckTask);
        } else {
            healthCheckTaskV2 = new HealthCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(healthCheckTaskV2);
        }
    }

ephemeral默认为true:

  • 创建客户端心跳检查任务
  • 定时任务线程池执行该任务

定时任务代码2.1-4:

    public static void scheduleCheck(BeatCheckTask task) {
        Runnable wrapperTask =
                task instanceof NacosHealthCheckTask ? new HealthCheckTaskInterceptWrapper((NacosHealthCheckTask) task)
                        : task;
        futureMap.computeIfAbsent(task.taskKey(),
                k -> GlobalExecutor.scheduleNamingHealth(wrapperTask, 5000, 5000, TimeUnit.MILLISECONDS));
    }

即定时任务延时5s后开始执行定时任务,间隔5s。我们来看下执行的ClientBeatCheckTaskV2具体执行了什么任务?

@Override
public void doHealthCheck() {
    try {
        Collection<Service> services = client.getAllPublishedService();
        for (Service each : services) {
            HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client
                    .getInstancePublishInfo(each);
            interceptorChain.doInterceptor(new InstanceBeatCheckTask(client, each, instance));
        }
    } catch (Exception e) {
        Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
    }
}

@Override
public void run() {
    doHealthCheck();
}

继续追踪下InstanceBeatCheckTask任务做了什么呢?

public class InstanceBeatCheckTask implements Interceptable {
	    static {
        CHECKERS.add(new UnhealthyInstanceChecker());
        CHECKERS.add(new ExpiredInstanceChecker());
        CHECKERS.addAll(NacosServiceLoader.load(InstanceBeatChecker.class));
    }
        @Override
    public void passIntercept() {
        for (InstanceBeatChecker each : CHECKERS) {
            each.doCheck(client, service, instancePublishInfo);
        }
    }
    
    @Override
    public void afterIntercept() {
    }
    }
  • 该实例健康检查任务添加2项检查:不健康实例检查和过期实例健康检查

看下不健康实例检查做了什么?

public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) {
    if (instance.isHealthy() && isUnhealthy(service, instance)) {
        changeHealthyStatus(client, service, instance);
    }
}

private boolean isUnhealthy(Service service, HealthCheckInstancePublishInfo instance) {
    long beatTimeout = getTimeout(service, instance);
    return System.currentTimeMillis() - instance.getLastHeartBeatTime() > beatTimeout;
}
private void changeHealthyStatus(Client client, Service service, HealthCheckInstancePublishInfo instance) {
    instance.setHealthy(false);
    Loggers.EVT_LOG
            .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client last beat: {}", instance.getIp(),
                    instance.getPort(), instance.getCluster(), service.getName(), UtilsAndCommons.LOCALHOST_SITE,
                    instance.getLastHeartBeatTime());
    NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service));
    NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client));
    NotifyCenter.publishEvent(new HealthStateChangeTraceEvent(System.currentTimeMillis(),
            service.getNamespace(), service.getGroup(), service.getName(), instance.getIp(), instance.getPort(),
            false, "client_beat"));
}
  • 如果服务实例健康状态之前为true,检测当前是否健康
  • 通过判断(当前时间-实例最后一次心跳时间)是否大于心跳超时时间(默认15s)
  • 如果超心跳超时时间,设置服务实例为不健康

看下过期时间检测任务具体做类什么?

public class ExpiredInstanceChecker implements InstanceBeatChecker {
    
    @Override
    public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) {
        boolean expireInstance = ApplicationUtils.getBean(GlobalConfig.class).isExpireInstance();
        if (expireInstance && isExpireInstance(service, instance)) {
            deleteIp(client, service, instance);
        }
    }
    
    private boolean isExpireInstance(Service service, HealthCheckInstancePublishInfo instance) {
        long deleteTimeout = getTimeout(service, instance);
      // deleteTimeout默认30s
        return System.currentTimeMillis() - instance.getLastHeartBeatTime() > deleteTimeout;
    }

    
    private void deleteIp(Client client, Service service, InstancePublishInfo instance) {
        // 省略日志记录
      // 移除该服务实例
        client.removeServiceInstance(service);
      // 省略事件发布
    }
}
  • 判断实例算法过去算法:(当前时间-最后一次实例心跳时间)> 删除超时时间(默认30s);
  • 如果判断实例过期,会移除该服务实例。

2.2 客户端发送心跳任务续约

示意图2.2-2如下所示:

在这里插入图片描述

Url: /v1/ns/instance ,匹配服务端InstanceController#beat()代码2.2-1如下所示:

@CanDistro
@PutMapping("/beat")
@Secured(action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {
    
    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
    
    String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
    RsInfo clientBeat = null;
    if (StringUtils.isNotBlank(beat)) {
        clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
    }
   // 省略。。。获取信息
    BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();
    builder.setRequest(request);
    int resultCode = getInstanceOperator()
            .handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat, builder);
    result.put(CommonParams.CODE, resultCode);
    result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,
            getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName));
    result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
    return result;
}

我查看handleBeat() 方法,实际执行InstanceOperatorClientImpl#handleBeat()方法,代码如下:

@Override
public int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster,
        RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws NacosException {
    Service service = getService(namespaceId, serviceName, true);
    String clientId = IpPortBasedClient.getClientId(ip + InternetAddressUtil.IP_PORT_SPLITER + port, true);
    IpPortBasedClient client = (IpPortBasedClient) clientManager.getClient(clientId);
    if (null == client || !client.getAllPublishedService().contains(service)) {
        if (null == clientBeat) {
            return NamingResponseCode.RESOURCE_NOT_FOUND;
        }
        Instance instance = builder.setBeatInfo(clientBeat).setServiceName(serviceName).build();
        registerInstance(namespaceId, serviceName, instance);
        client = (IpPortBasedClient) clientManager.getClient(clientId);
    }
    if (!ServiceManager.getInstance().containSingleton(service)) {
        throw new NacosException(NacosException.SERVER_ERROR,
                "service not found: " + serviceName + "@" + namespaceId);
    }
    if (null == clientBeat) {
        clientBeat = new RsInfo();
        clientBeat.setIp(ip);
        clientBeat.setPort(port);
        clientBeat.setCluster(cluster);
        clientBeat.setServiceName(serviceName);
    }
    ClientBeatProcessorV2 beatProcessor = new ClientBeatProcessorV2(namespaceId, clientBeat, client);
    HealthCheckReactor.scheduleNow(beatProcessor);
    client.setLastUpdatedTime();
    return NamingResponseCode.OK;
}
  • 初始第一次client==null,会创建客户端实例并注册
  • HealthCheckReactor.scheduleNow(beatProcessor);会通过定时任务线程池执行ClientBeatProcessorV2类型的任务

下面我们来看下ClientBeatProcessorV2线程类型里面具体做了什么?

public void run() {
        if (Loggers.EVT_LOG.isDebugEnabled()) {
            Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
        }
        String ip = rsInfo.getIp();
        int port = rsInfo.getPort();
        String serviceName = NamingUtils.getServiceName(rsInfo.getServiceName());
        String groupName = NamingUtils.getGroupName(rsInfo.getServiceName());
        Service service = Service.newService(namespace, groupName, serviceName, rsInfo.isEphemeral());
        HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client.getInstancePublishInfo(service);
        // 获取服务实例的IP端口与心跳传递的IP端口比较
        if (instance.getIp().equals(ip) && instance.getPort() == port) {
            if (Loggers.EVT_LOG.isDebugEnabled()) {
                Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo);
            }
          // 这里完成服务实例续约,即通过设置最后心跳时间
            instance.setLastHeartBeatTime(System.currentTimeMillis());
            if (!instance.isHealthy()) {
                instance.setHealthy(true);
// 省略事件发布
            }
        }
    }
  • 通过心跳传递的IP和端口与当前nacos以发布的对应服务实例IP和端口比对,确定是哪个服务实例发送的心跳。
  • 上面学习中,我们知道心跳检查通过(当前时间-服务实例最后心跳时间与设置的时间比对)完成的,这里把最好心跳时间更新为当前时间,完成了服务实例的续约;
  • 如果之前因为网络延时等原因造成实例被设置为不健康,这里重新设置实例为健康状态。

2.3 服务实例移除

在#2.1中我们知道当检测任务检测到服务实例过期后,会移除该实例 ,看看具体做了什么,继续追踪下AbstractClient#removeServiceInstance()方法:

@Override
public InstancePublishInfo removeServiceInstance(Service service) {
    InstancePublishInfo result = publishers.remove(service);
    if (null != result) {
        if (result instanceof BatchInstancePublishInfo) {
            MetricsMonitor.decrementIpCountWithBatchRegister(result);
        } else {
            MetricsMonitor.decrementInstanceCount();
        }
        NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
    }
    Loggers.SRV_LOG.info("Client remove for service {}, {}", service, getClientId());
    return result;
}

protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = new ConcurrentHashMap<>(16, 0.75f, 1);
  • publishers:nacos维护的缓存key为服务名,value为服务发布实例的缓存,类型为ConcurrentHashMap;
  • 服务实例移除就是冲当前服务实例缓存中移除该服务对应的服务实例。

2.4 心跳任务闭环

客户端根据服务的返回的心跳时间,执行新的定时任务。

public void run() {
    if (beatInfo.isStopped()) {
        return;
    }
    long result = serverProxy.sendBeat(beatInfo);
    long nextTime = result > 0 ? result : beatInfo.getPeriod();
    executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}

结语

如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/spring-cloud-study.git

参考地址:

[1]Nacos官网

[2]Nacos-服务端心跳机制

[3]Nacos客户端心跳续约

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

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

相关文章

重装系统下载网址

[置顶]无论会不会安装系统&#xff0c;都一定会需要&#xff0c;觉得内容不错欢迎一键三连哦 稳定 | 方便 | 好用 1、MSDN 用过最简单好用&#xff0c;下载不限速&#xff0c;支持迅雷、IDM多种下载方式 https://www.xitongku.com 2、Windows系统下载仓储站 为小白重装系统提供…

KIOPTRIX: LEVEL 4通关详解

环境配置 vulnhub上下载的文件没有vmx 去3的文件里偷一个 记事本打开把所有Kioptrix3_vmware改成Kioptrix4_vmware 然后网卡地址随便改一下 打开后会提示找不到虚拟机,手动选一下就行了 信息收集 漏洞发现 web一上去就是一个登录框 扫路径发现database.sql 但是密码是错的…

高通开发系列 - linux arm64 toolchain交叉编译器编译错误

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 概述下载aarch64交叉编译器编译使能编译环境使能defconfig编译问题1:address-of-packed-member问题2:attribute-alias=问题3:array…

Kubernetes 笔记(14)— 滚动更新、定义应用版本、实现应用更新、管理应用更新、添加更新描述

滚动更新&#xff0c;使用 kubectl rollout 实现用户无感知的应用升级和降级。 1. 定义应用版本 在 Kubernetes 里&#xff0c;版本更新使用的不是 API 对象&#xff0c;而是两个命令&#xff1a;kubectl apply 和 kubectl rollout&#xff0c;当然它们也要搭配部署应用所需要…

人工智能作业之遗传算法

遗传算法1.遗传算法定义2.相关术语3.遗传算法的主要步骤4.遗传算法的参数设计原则5.代码实现1.遗传算法定义 遗传算法&#xff08;Genetic Algorithm, GA&#xff09;起源于对生物系统所进行的计算机模拟研究。它是模仿自然界生物进化机制发展起来的随机全局搜索和优化方法&am…

java 婚恋交友网站Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 java 婚恋交友网站是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

IMX6ULL学习笔记(22)——eLCDIF接口使用(TFT-LCD屏显示)

一、TFT-LCD简介 TFT-LCD&#xff08;Thin Film Transistor-Liquid Crystal Display&#xff09; 即薄膜晶体管液晶显示器。TFT-LCD 与无源 TN-LCD、 STN-LCD 的简单矩阵不同&#xff0c;它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管&#xff08;TFT&#xff09;&#…

智慧网点解决方案 | 助推银行“营销-销售-服务”一体化建设

传统网点的智慧化变革已成为新形势下银行创新业务服务模式与产品、优化客户体验、提质增效的一大阵地。如何在网点转型过程中充分发挥边缘计算等新技术的价值&#xff0c;引领行业数字化转型新趋势&#xff0c;成为银行业面临的共同课题。 在传统银行网点向智慧网点的转型过程…

基于Java+SpringBoot+vue的家具销售电商平台设计与实现【源码(完整源码请私聊)+论文+演示视频+包运行成功】

博主介绍&#xff1a;专注于Java技术领域和毕业项目实战 &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案例&#xff08;300套&#xff09; 目录 一、效果演示 二、…

真00后整顿职场?公司新来了个00后卷王,3个月薪资干到20K.....

最近聊到软件测试的行业内卷&#xff0c;越来越多的转行和大学生进入测试行业。想要获得更好的待遇和机会&#xff0c;不断提升自己的技能栈成了测试老人迫在眉睫的问题。 不论是面试哪个级别的测试工程师&#xff0c;面试官都会问一句“会编程吗&#xff1f;有没有自动化测试…

Redis基础总结-redis简介

Redis基础Redis基础目标&#xff1a;1. Redis 简介1.1 NoSQL概念1.1.1 问题现象1.1.2 NoSQL的概念1.2 Redis概念1.2.1 redis概念1.2.2 redis的应用场景1.3 Redis 的下载与安装1.3.1 Redis 的下载与安装1.4 Redis服务器启动1.4.1 Redis服务器启动1.4.2 Redis客户端启动1.4.3 Red…

数字化转型迫在眉睫!药企如何应用AI技术加速创新?

导语 | 近年来&#xff0c;随着 AI 等技术的发展应用&#xff0c;数字化、智能化日渐成为各行各业转型升级的新兴力量&#xff0c;其与医药产业的融合创新也逐渐成为当前的新趋势&#xff0c;众多医药制造企业蓄势待发&#xff0c;搭乘数字化的快车&#xff0c;驶入高速发展的快…

论文笔记:Fully Convolutional Networks for Semantic Segmentation

摘要 卷积网络是产生特征层次结构的强大视觉模型。我们展示了卷积网络本身&#xff0c;经过端到端、像素到像素的训练&#xff0c;超过了语义分割的最新技术水平。我们的主要见解是构建“全卷积”网络&#xff0c;该网络接受任意大小的输入并通过有效的推理和学习产生相应大小…

css的font-size属性、line-height属性、height属性

目录 一&#xff0c;字体框 二、font-size属性 三、line-height属性 四、line-height和font-size的联系 简介&#xff1a;font-size是css中关于字体的样式属性&#xff0c;注意与文本属性text-xxx进行区别。因为文本由一个个字符组成&#xff0c;所以字体属性也会对文本属性…

海伦司的酒何时“醒”

被年轻人喝出来的“酒馆第一股”海伦司&#xff0c;目前正经历疯狂开店之后的阵痛。 3月24日&#xff0c;海伦司国际控股有限公司(下称“海伦司”,09869.HK)发布了2022年的业绩报告。 海伦司是一家连锁酒馆品牌&#xff0c;其年报公布后的首个交易日&#xff0c;其股价跌幅达…

Qt5.12實戰之Qt調用Linux靜態庫(.a)與動態庫(.so)

1.準備編譯好的靜態庫&#xff0c;複製到lib目錄 &#xff0c;動態庫複製到bin目錄 2.創建Qt控制臺應用&#xff0c;並添加靜態庫引用 右擊工程名call_liba,選擇添加擴展庫 選擇要添加的libtest.a 然後 點擊 OPEN 點擊Next後會自動添加靜態庫相關引用 到工程 的.pro文件 中 生…

OpenCV实例(五)指纹识别

OpenCV实例&#xff08;五&#xff09;指纹识别1.指纹识别概述1.1概述1.2原理2.指纹识别算法2.1特征提取2.2MCC匹配方法2.3尺度不变特征变换&#xff08;SIFT&#xff09;3.显示指纹的关键点4.基于SIFT的指纹识别作者&#xff1a;Xiou 1.指纹识别概述 1.1概述 指纹识别&…

程序设计方法学

体育竞技分析 问题分析 体育竞技分析 需求&#xff1a;毫厘是多少&#xff1f; 如何科学分析体育竞技比赛&#xff1f; 输入&#xff1a;球员的水平 输出&#xff1a;可预测的比赛成绩 体育竞技分析&#xff1a;模拟N场比赛 计算思维&#xff1a;抽象 自动化 模拟&am…

QML控件--Menu

文章目录一、控件基本信息二、控件使用三、属性成员四、成员函数一、控件基本信息 二、控件使用 import QtQuick 2.10 import QtQuick.Window 2.10 import QtQuick.Controls 2.3ApplicationWindow{visible: true;width: 1280;height: 720;Button {id: fileButtontext: "Fi…

2023最全的自动化测试入门基础知识(建议收藏)

1)首先&#xff0c;什么是自动化测试&#xff1f; 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。通常&#xff0c;在设计了测试用例并通过评审之后&#xff0c;由测试人员根据测试用例中描述的过程一步步执行测试&#xff0c;得到实际结果与期望结果的比较。…