[RocketMQ] Consumer 负载均衡服务 RebalanceService入口源码 (十五)

  • RocketMQ一个消费者组中可以有多个消费者, 在集群模式下他们共同消费topic下的所有消息, RocketMQ规定一个消息队列仅能被一个消费者消费, 但是一个消费者可以同时消费多个消息队列。
  • 需要负载均衡服务RebalanceService来进行消息队列分配的重平衡。
  • 使用负载均衡服务RebalanceService来专门处理多个消息队列和消费者的对应关系, 如何分配消息队列给这些消费者。

RocketMQ消费者负载均衡服务RebalanceService的入口代码

文章目录

      • 1.负载均衡或者重平衡
        • 1.1 RebalanceService自动重平衡
        • 1.2 Consumer启动重平衡
        • 1.3 Broker请求重平衡
        • 1.4 Broker处理心跳请求
          • 1.4.1 registerConsumer注册消费者
          • 1.4.2 客户端处理重平衡请求

1.负载均衡或者重平衡

有三种情况会触发Consumer进行负载均衡或者说重平衡。

  1. RebalanceService服务是一个线程任务, 由MQClientInstance启动, 每隔20s自动进行一次自动负载均衡。
  2. Broker触发的重平衡:
    1. Broker收到心跳请求之后如果发现消息中有新的consumer连接或者consumer订阅了新的topic或者移除了topic的订阅, Broker发送Code为NOTIFY_CONSUMER_IDS_CHANGED的请求给该group下面的所有Consumer, 要求进行一次负载均衡。
    2. 如果某个客户端连接出现连接异常事件EXCEPTION、连接断开事件CLOSE、或者连接闲置事件IDLE, 则Broker同样会发送重平衡请求给消费者组下面的所有消费者。
  3. 新的Consumer服务启动的时候, 主动调用rebalanceImmediately唤醒负载均衡服务rebalanceService。

1.1 RebalanceService自动重平衡

RebalanceService#run方法, 最多每隔20s执行一次重平衡。

/**
 * RebalanceServicede 方法
 */
@Override
public void run() {
    log.info(this.getServiceName() + " service started");
    /*
     * 运行时逻辑
     * 如果服务没有停止,则在死循环中执行负载均衡
     */
    while (!this.isStopped()) {
        //等待运行,默认最多等待20s,可以被唤醒
        this.waitForRunning(waitInterval);
        //执行重平衡操作
        this.mqClientFactory.doRebalance();
    }

    log.info(this.getServiceName() + " service end");
}

1.2 Consumer启动重平衡

新的Consumer服务启动的时候, 主动调用rebalanceImmediately唤醒负载均衡服务rebalanceService。

/**
 * MQClientInstance的方法
 * 立即重平衡
 */
public void rebalanceImmediately() {
    //唤醒重平衡服务,立即重平衡
    this.rebalanceService.wakeup();
}

1.3 Broker请求重平衡

  1. Broker收到心跳请求之后如果发现消息中有新的consumer连接或者consumer订阅了新的topic或者移除了topic的订阅, Broker发送Code为NOTIFY_CONSUMER_IDS_CHANGED的请求给该group下面的所有Consumer, 要求进行一次负载均衡。
  2. 如果某个客户端连接出现连接异常事件EXCEPTION、连接断开事件CLOSE、或者连接闲置事件IDLE, 则Broker同样会发送重平衡请求给消费者组下面的所有消费者。处理入口方法为ClientHousekeepingService# doChannelCloseEvent方法。

新的Consumer和Producer启动的时候, 发送心跳给Broker, MQClientInstance的内部的服务也会定时30s发送心跳信息给Broker。

心跳请求的Code为HEART_BEAT, 该请求最终被Broker的ClientManageProcessor处理器处理。

/**
 * ClientManageProcessor的方法, Broker处理心跳方法
 */
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request)
        throws RemotingCommandException {
    switch (request.getCode()) {
        //客户端心跳请求
        case RequestCode.HEART_BEAT:
            //客户端心跳请求
            return this.heartBeat(ctx, request);
        case RequestCode.UNREGISTER_CLIENT:
            return this.unregisterClient(ctx, request);
        case RequestCode.CHECK_CLIENT_CONFIG:
            return this.checkClientConfig(ctx, request);
        default:
            break;
    }
    return null;
}

1.4 Broker处理心跳请求

Broker的ClientManageProcessor#heartBeat该方法用于Broker处理来自客户端, consumer和producer的请求。

  1. 解码消息中的信息成为HeartbeatData对象。
  2. 循环遍历处理consumerDataSet集合, 对ConsumerData信息进行注册或者更改, 如果consumer信息修改的话, Broker会发送NOTIFY_CONSUMER_IDS_CHANGED请求给同组的所有consumer客户端, 进行重平衡操作。
  3. 循环遍历处理consumerDataSet集合, 对ProducerData信息注册或者更改。
/**
 * ClientManageProcessor的方法
 * <p>
 * 处理客户端心跳请求
 */
public RemotingCommand heartBeat(ChannelHandlerContext ctx, RemotingCommand request) {
    //构建响应命令对象
    RemotingCommand response = RemotingCommand.createResponseCommand(null);
    //解码
    HeartbeatData heartbeatData = HeartbeatData.decode(request.getBody(), HeartbeatData.class);
    //构建客户端连接信息对象
    ClientChannelInfo clientChannelInfo = new ClientChannelInfo(
            ctx.channel(),
            heartbeatData.getClientID(),
            request.getLanguage(),
            request.getVersion()
    );
    /*
     * 1 循环遍历处理consumerDataSet,即处理consumer的心跳信息
     */
    for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
        //查找broker缓存的当前消费者组的订阅组配置
        SubscriptionGroupConfig subscriptionGroupConfig =
                this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(
                        data.getGroupName());
        boolean isNotifyConsumerIdsChangedEnable = true;
        //如果已存在订阅组
        if (null != subscriptionGroupConfig) {
            //当consumer发生改变的时候是否支持通知同组的所有consumer,默认true,即支持
            isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable();
            int topicSysFlag = 0;
            if (data.isUnitMode()) {
                topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
            }
            //尝试创建重试topic
            String newTopic = MixAll.getRetryTopic(data.getGroupName());
            this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
                    newTopic,
                    subscriptionGroupConfig.getRetryQueueNums(),
                    PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
        }
        /*
         * 注册consumer,返回consumer信息是否已发生改变
         * 如果发生了改变,Broker会发送NOTIFY_CONSUMER_IDS_CHANGED请求给同组的所有consumer客户端,要求进行重平衡操作
         */
        boolean changed = this.brokerController.getConsumerManager().registerConsumer(
                data.getGroupName(),
                clientChannelInfo,
                data.getConsumeType(),
                data.getMessageModel(),
                data.getConsumeFromWhere(),
                data.getSubscriptionDataSet(),
                isNotifyConsumerIdsChangedEnable
        );

        if (changed) {
            //如果consumer信息发生了改变,打印日志
            log.info("registerConsumer info changed {} {}",
                    data.toString(),
                    RemotingHelper.parseChannelRemoteAddr(ctx.channel())
            );
        }
    }
    /*
     * 2 循环遍历处理producerDataSet,即处理producer的心跳信息
     */
    for (ProducerData data : heartbeatData.getProducerDataSet()) {
        /*
         * 注册producer
         */
        this.brokerController.getProducerManager().registerProducer(data.getGroupName(),
                clientChannelInfo);
    }
    //返回响应
    response.setCode(ResponseCode.SUCCESS);
    response.setRemark(null);
    return response;
}
1.4.1 registerConsumer注册消费者

循环遍历处理consumerDataSet集合, 对ConsumerData信息进行注册或者更改, 如果consumer信息修改的话, Broker会发送NOTIFY_CONSUMER_IDS_CHANGED请求给同组的所有consumer客户端, 进行重平衡操作。

/**
 * ConsumerManager的方法
 * <p>
 * 注册consumer,返回consumer信息是否已发生改变
 * 如果发生了改变,Broker会发送NOTIFY_CONSUMER_IDS_CHANGED请求给同组的所有consumer客户端,要求进行重平衡操作
 *
 * @param group                            消费者组
 * @param clientChannelInfo                客户端连接信息
 * @param consumeType                      消费类型,PULL or PUSH
 * @param messageModel                     消息模式,集群 or 广播
 * @param consumeFromWhere                 启动消费位置
 * @param subList                          订阅信息数据
 * @param isNotifyConsumerIdsChangedEnable 一个consumer改变时是否通知该consumergroup中的所有consumer进行重平衡
 * @return 是否重平衡
 */
public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
                                ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
                                final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) {
    //获取当前group对应的ConsumerGroupInfo
    ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
    //如果为null,那么新建一个ConsumerGroupInfo并存入consumerTable
    if (null == consumerGroupInfo) {
        ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
        ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
        consumerGroupInfo = prev != null ? prev : tmp;
    }
    /*
     * 1 更新连接
     */
    boolean r1 =
            consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
                    consumeFromWhere);
    /*
     * 2 更新订阅信息
     */
    boolean r2 = consumerGroupInfo.updateSubscription(subList);
    /*
     * 2 如果连接或者订阅信息有更新,并且允许通知,那么通知该consumergroup中的所有consumer进行重平衡
     */
    if (r1 || r2) {
        if (isNotifyConsumerIdsChangedEnable) {
            //CHANGE事件
            this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel());
        }
    }
    // 3.注册订阅信息到ConsumerFilterManager
    this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList);

    return r1 || r2;
}

1.updateChannel更新连接

更新ConsumerGroup组对应的ConsumerGroupInfo的一些属性, 还会判断当前连接是否是新连接, 如果Broker此前没有该连接的信息, 那么表示有新的consumer连接到此broker, 需要通知当前ConsumerGroup的所有consumer进行重平衡。

/**
 * ConsumerGroupInfo的方法
 * <p>
 * 更新连接
 *
 * @param infoNew          新连接信息
 * @param consumeType      消费类型,PULL or PUSH
 * @param messageModel     消息模式,集群 or 广播
 * @param consumeFromWhere 启动消费位置
 * @return 是否通知
 */
public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consumeType,
                             MessageModel messageModel, ConsumeFromWhere consumeFromWhere) {
    boolean updated = false;
    //更新信息
    this.consumeType = consumeType;
    this.messageModel = messageModel;
    this.consumeFromWhere = consumeFromWhere;
    //根据当前连接获取channelInfoTable缓存中的连接信息
    ClientChannelInfo infoOld = this.channelInfoTable.get(infoNew.getChannel());
    //如果缓存中的连接信息为null,说明当前连接是一个新连接
    if (null == infoOld) {
        //存入缓存
        ClientChannelInfo prev = this.channelInfoTable.put(infoNew.getChannel(), infoNew);
        //长期按没有该连接信息,那么表示有新的consumer连接到此broekr,那么需要通知
        if (null == prev) {
            log.info("new consumer connected, group: {} {} {} channel: {}", this.groupName, consumeType,
                    messageModel, infoNew.toString());
            updated = true;
        }

        infoOld = infoNew;
    } else {
        //异常情况
        if (!infoOld.getClientId().equals(infoNew.getClientId())) {
            log.error("[BUG] consumer channel exist in broker, but clientId not equal. GROUP: {} OLD: {} NEW: {} ",
                    this.groupName,
                    infoOld.toString(),
                    infoNew.toString());
            this.channelInfoTable.put(infoNew.getChannel(), infoNew);
        }
    }
    //更新更新时间
    this.lastUpdateTimestamp = System.currentTimeMillis();
    infoOld.setLastUpdateTimestamp(this.lastUpdateTimestamp);

    return updated;
}

2.updateSubscription更新订阅信息

更新此ConsumerGroup组对应的订阅信息集合, 如果存在新增订阅的topic, 或者移除某个topic的订阅, 那么需要通知当前ConsumerGroup的所有consumer进行重平衡。

RocketMQ需要保证组内的所有消费者订阅的topic都必须一致。

  1. 方法首先遍历当前请求传递的订阅信息集合, 然后对于每个订阅的topic从subscriptionTable缓存中尝试获取, 获取不到则表示新增了topic订阅信息, 存入subscriptionTable。
  2. 遍历subscriptionTable集合, 判断每一个topic是否存在于当前请求传递的订阅信息集合中, 如果不存在, 表示consumer移除了topic的订阅, 那么当前topic的订阅信息会从subscriptionTable集合中被移除。
/**
 * ConsumerGroupInfo的方法
 * 更新订阅信息
 *
 * @param subList 订阅信息集合
 */
public boolean updateSubscription(final Set<SubscriptionData> subList) {
    boolean updated = false;
    //遍历订阅信息集合
    for (SubscriptionData sub : subList) {
        //根据订阅的topic在ConsumerGroup的subscriptionTable缓存中此前的订阅信息
        SubscriptionData old = this.subscriptionTable.get(sub.getTopic());
        //如果此前没有关于该topic的订阅信息,那么表示此topic为新增订阅
        if (old == null) {
            //存入subscriptionTable
            SubscriptionData prev = this.subscriptionTable.putIfAbsent(sub.getTopic(), sub);
            //此前没有关于该topic的订阅信息,那么表示此topic为新增订阅,那么需要通知
            if (null == prev) {
                updated = true;
                log.info("subscription changed, add new topic, group: {} {}",
                        this.groupName,
                        sub.toString());
            }
        } else if (sub.getSubVersion() > old.getSubVersion()) {
            //更新数据
            if (this.consumeType == ConsumeType.CONSUME_PASSIVELY) {
                log.info("subscription changed, group: {} OLD: {} NEW: {}",
                        this.groupName,
                        old.toString(),
                        sub.toString()
                );
            }

            this.subscriptionTable.put(sub.getTopic(), sub);
        }
    }
    /*
     * 遍历ConsumerGroup的subscriptionTable缓存
     */
    Iterator<Entry<String, SubscriptionData>> it = this.subscriptionTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, SubscriptionData> next = it.next();
        //获取此前订阅的topic
        String oldTopic = next.getKey();

        boolean exist = false;
        //判断当前的subList是否存在该topic的订阅信息
        for (SubscriptionData sub : subList) {
            //如果存在,则退出循环
            if (sub.getTopic().equals(oldTopic)) {
                exist = true;
                break;
            }
        }
        //当前的subList不存在该topic的订阅信息,说明consumer移除了对于该topic的订阅
        if (!exist) {
            log.warn("subscription changed, group: {} remove topic {} {}",
                    this.groupName,
                    oldTopic,
                    next.getValue().toString()
            );
            //移除数据
            it.remove();
            //那么需要通知
            updated = true;
        }
    }

    this.lastUpdateTimestamp = System.currentTimeMillis();

    return updated;
}

3.consumerIdsChangeListener.handle监听器通知

通知监听器处理对应的事件, 事件为ConsumerGroupEvent.CHANGE。

  1. 如果运行通知, 则遍历该ConsumerGroup的连接集合, 然后对每个连接调用notifyConsumerIdsChanged方法通知对应的客户端消费者执行负载均衡。

  2. notifyConsumerIdsChanged: broker发送客户端一个重平衡请求, Code为NOTIFY_CONSUMER_IDS_CHANGED。

/**
 * DefaultConsumerIdsChangeListener的方法
 * 
 * 处理监听到的事件
 * @param event 事件
 * @param group 消费者组
 * @param args 参数
 */
@Override
public void handle(ConsumerGroupEvent event, String group, Object... args) {
    if (event == null) {
        return;
    }
    switch (event) {
        //改变事件,需要通知该消费者组的每一个消费者
        case CHANGE:
            if (args == null || args.length < 1) {
                return;
            }
            //获取参数
            List<Channel> channels = (List<Channel>) args[0];
            //如果允许通知
            if (channels != null && brokerController.getBrokerConfig().isNotifyConsumerIdsChangedEnable()) {
                //遍历连接集合
                for (Channel chl : channels) {
                    //通知该消费者客户端执行负载均衡
                    this.brokerController.getBroker2Client().notifyConsumerIdsChanged(chl, group);
                }
            }
            break;
        case UNREGISTER:
            this.brokerController.getConsumerFilterManager().unRegister(group);
            break;
        case REGISTER:
            if (args == null || args.length < 1) {
                return;
            }
            Collection<SubscriptionData> subscriptionDataList = (Collection<SubscriptionData>) args[0];
            this.brokerController.getConsumerFilterManager().register(group, subscriptionDataList);
            break;
        default:
            throw new RuntimeException("Unknown event " + event);
    }
}

/**
 * Broker2Client的方法
 * <p>
 * 通知消费者变更
 *
 * @param channel       连接
 * @param consumerGroup 消费者组
 */
public void notifyConsumerIdsChanged(
        final Channel channel,
        final String consumerGroup) {
    if (null == consumerGroup) {
        log.error("notifyConsumerIdsChanged consumerGroup is null");
        return;
    }
    //构建请求头
    NotifyConsumerIdsChangedRequestHeader requestHeader = new NotifyConsumerIdsChangedRequestHeader();
    requestHeader.setConsumerGroup(consumerGroup);
    //构建远程命令对象,请求code为NOTIFY_CONSUMER_IDS_CHANGED
    RemotingCommand request =
            RemotingCommand.createRequestCommand(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, requestHeader);

    try {
        //发送单向请求,无需等待客户端回应
        this.brokerController.getRemotingServer().invokeOneway(channel, request, 10);
    } catch (Exception e) {
        log.error("notifyConsumerIdsChanged exception. group={}, error={}", consumerGroup, e.toString());
    }
}

1.4.2 客户端处理重平衡请求

broker的请求在客户端是通过ClientRemotingProcessor#processRequest处理的。

NOTIFY_CONSUMER_IDS_CHANGED请求通过客户端的ClientRemotingProcessor#notifyConsumerIdsChanged方法处理。

/**
 * ClientRemotingProcessor的方法
 * <p>
 * 处理来自远程服务端的请求
 */
@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
                                      RemotingCommand request) throws RemotingCommandException {
    switch (request.getCode()) {
        case RequestCode.CHECK_TRANSACTION_STATE:
            return this.checkTransactionState(ctx, request);
        case RequestCode.NOTIFY_CONSUMER_IDS_CHANGED:
            //处理NOTIFY_CONSUMER_IDS_CHANGED请求
            return this.notifyConsumerIdsChanged(ctx, request);
        case RequestCode.RESET_CONSUMER_CLIENT_OFFSET:
            return this.resetOffset(ctx, request);
        case RequestCode.GET_CONSUMER_STATUS_FROM_CLIENT:
            return this.getConsumeStatus(ctx, request);

        case RequestCode.GET_CONSUMER_RUNNING_INFO:
            return this.getConsumerRunningInfo(ctx, request);

        case RequestCode.CONSUME_MESSAGE_DIRECTLY:
            return this.consumeMessageDirectly(ctx, request);

        case RequestCode.PUSH_REPLY_MESSAGE_TO_CLIENT:
            return this.receiveReplyMessage(ctx, request);
        default:
            break;
    }
    return null;
}

notifyConsumerIdsChanged客户端重平衡

客户端接口到broker的重平衡请求之后, 调用这个方法。内部仅仅是调用我们之前讲的rebalanceImmediately 方法唤醒负载均衡服务rebalanceService, 进行重平衡。

public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx,
                                                RemotingCommand request) throws RemotingCommandException {
    try {
        //解析请求头
        final NotifyConsumerIdsChangedRequestHeader requestHeader =
                (NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class);
        //打印日志
        log.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately",
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                requestHeader.getConsumerGroup());
        //熟悉的方法,立即进行重平衡
        this.mqClientFactory.rebalanceImmediately();
    } catch (Exception e) {
        log.error("notifyConsumerIdsChanged exception", RemotingHelper.exceptionSimpleDesc(e));
    }
    return null;
}

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

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

相关文章

day40-3d Background Boxes(3D背景盒子转换)

50 天学习 50 个项目 - HTMLCSS and JavaScript day40-3d Background Boxes&#xff08;3D背景盒子转换&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewp…

39. Linux系统下在Qt5.9.9中搭建Android开发环境

1. 说明 QT版本:5.9.9 电脑系统:Linux JDK版本:openjdk-8-jdk SDK版本:r24.4.1 NDK版本:android-ndk-r14b 效果展示: 2. 具体步骤 大致安装的步骤如下:①安装Qt5.9.9,②安装jdk,③安装ndk,④安装sdk,⑤在qt中配置前面安装的环境路径 2.1 安装Qt5.9.9 首先下载…

ubuntu20.04 安装 Qt5.15

目录 安装前工作 选择安装QT的哪个版本 安装时候选择哪些组件 安装Qt5.15 在线安装 我选择的组件 源码包安装 测试 安装前工作 ubuntu20.04.3安装Qt6.22操作步骤_ubuntu安装qt6_sonicss的博客-CSDN博客 # 安装g、gcc编译器 sudo apt-get install build-essential 安装l…

计算机网络(Computer Networks)基础

本篇介绍计算机网络的基础知识。 文章目录 1. 计算机网络历史2. 以太网" (Ethernet)2.1 以太网" (Ethernet)的简单形式及概念2.2 指数退避解决冲突问题2.3 利用交换机减少同一载体中设备2.4 互联网&#xff08;The Internet&#xff09;2.5 路由(routing)2.6 数据包…

spring拦截器 与统一格式

目录 前言模拟拦截器拦截器的实现原理什么是动态代理? 什么是静态代理静态代理与动态代理的区别两种常用的动态代理方式基于接口的动态代理基于类的动态代理 JDK Proxy 与 CGlib的区别 其他 统⼀访问前缀添加统⼀异常处理统⼀数据返回格式 前言 之前博客讲述了 , 关于SpringA…

【Huawei】WLAN实验(三层发现)

拓扑图如上&#xff0c;AP与S1在同一VLAN,S1与AC在同一VLAN&#xff0c;AP采用三层发现AC&#xff0c;AP与客户的DHCP由S1提供。 S1配置 vlan batch 10 20 30 dhcp enable ip pool apgateway-list 192.168.20.1network 192.168.20.0 mask 255.255.255.0option 43 sub-option …

代码随想录算法训练营第二十二天 | 读PDF复习环节2

读PDF复习环节2 本博客的内容只是做一个大概的记录&#xff0c;整个PDF看下来&#xff0c;内容上是不如代码随想录网站上的文章全面的&#xff0c;并且PDF中有些地方的描述&#xff0c;是很让我疑惑的&#xff0c;在困扰我很久后&#xff0c;无意间发现&#xff0c;其网站上的讲…

LeetCode 刷题 数据结构 数组 283题 移动零

难度&#xff1a;简单 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0]示例 2: 输入:…

mysql的主从复制

1.主从复制的原理 主从复制的原理是通过基于日志的复制方式实现数据的同步。当主服务器上发生数据变更时&#xff0c;会将这些变更写入二进制日志&#xff08;Binary Log&#xff09;中。从服务器通过连接到主服务器&#xff0c;请求从主服务器获取二进制日志&#xff0c;并将…

opencv python 训练自己的分类器

源码下载 一、分类器制作 1.样本准备 收集好你所需的正样本&#xff0c;和负样本&#xff0c;分别保存在不同文件夹 在pycharm新建项目&#xff0c;项目结构如下&#xff1a;has_mask文件夹放置正样本&#xff0c;no_mask文件夹放置负样本 安装opencv&#xff0c;把opencv包…

C语言基础入门详解二

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 一、C语言多级指针入门 #include<stdio.h> #include<stdlib.h>/**多级指针…

基于 moleculer 微服务架构的智能低代码PaaS 平台源码 可视化开发

低代码开发平台源码 低代码管理系统PaaS 平台 无需代码或通过少量代码就可以快速生成应用程序的开发平台。 本套低代码管理后台可以支持多种企业应用场景&#xff0c;包括但不限于CRM、ERP、OA、BI、IoT、大数据等。无论是传统企业还是新兴企业&#xff0c;都可以使用管理后台…

ChatGPT的应用与发展趋势:解析人工智能的新风口

目录 优势 应用领域 发展趋势 总结 在人工智能技术迅猛发展的时代&#xff0c;自然语言处理系统的提升一直是研究者们追求的目标。作为人工智能领域的重要突破之一&#xff0c;ChatGPT以其出色的语言模型和交互能力&#xff0c;在智能对话领域取得了重要的进展。 ChatGPT是…

kubernetes介绍

介绍 Kubernetes 是一个开源的容器编排引擎&#xff0c;用来对容器化应用进行自动化部署、 扩缩和管理。 Kubernetes 这个名字源于希腊语&#xff0c;意为“舵手”或“飞行员”。k8s 这个缩写是因为 k 和 s 之间有八个字符的关系。 Google 在 2014 年开源了 Kubernetes 项目。…

K8S中网络如何通信

Kubernetes 提出了一个自己的网络模型“IP-per-pod”&#xff0c;能够很好地适应集群系统的网络需求&#xff0c;它有下面的这 4 点基本假设&#xff1a; 集群里的每个 Pod 都会有唯一的一个 IP 地址。Pod 里的所有容器共享这个 IP 地址。集群里的所有 Pod 都属于同一个网段。…

SQL-每日一题【626.换座位】

题目 表: Seat 编写SQL查询来交换每两个连续的学生的座位号。如果学生的数量是奇数&#xff0c;则最后一个学生的id不交换。 按 id 升序 返回结果表。 查询结果格式如下所示。 示例 1: 解题思路 前置知识 MySQL 的 MOD() 函数是取模运算的函数&#xff0c;它返回两个数相除…

qt简易闹钟

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);ui->stopBtn->setDisabled(true);this->setFixedSize(this->size()); //设置固定大小this->s…

【C语言进阶】程序环境和预处理

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;C语言 &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、程序的翻译环境和执行环境 二、详解编译和链接 2.1翻译环境 2.2编译的过…

Windows 10 中无法最大化任务栏中的程序

方法1&#xff1a;仅选择选项 PC 屏幕 如果您使用双显示器&#xff0c;有时这可能会发生在您的 1 台计算机已插入但您正在访问的应用程序正在另一台计算机上运行的情况下&#xff0c;因此您看不到任何选项。因此&#xff0c;请设置仅在主计算机上显示显示的 PC 屏幕选项。 第…

nacos2.2.3最新版启动所遇到的问题总结

前言 有问题就看官方文档&#xff0c;看不懂或者还是报错再看博客&#xff01;因为有时候忙的焦头烂额&#xff0c;却发现官方写的非常清楚&#xff0c;而且人家还自带一个example示例&#xff0c;自己都没有看&#xff0c;自己瞎折腾&#xff01;本人吃过亏&#xff0c;特此提…
最新文章