RocketMQ广播模式消费失败是否会重试?

news2025/1/13 16:54:41

文章目录

  • 前言
  • 继续
  • 广播和集群模式的消费流程
    • 集群模式(默认的)
    • 广播模式
    • 小结
  • push和pull介绍
    • 源码展示
  • 偏移量保存失败情况
      • 1. 网络问题
      • 2. Consumer本地问题
      • 3. 消费进度记录器问题
      • 4. 程序设计问题
      • 5. 异常终止
      • 6. 持久化策略问题
      • 7. 同步问题
  • 源码解析
    • `OffsetStore` 接口
    • LocalFileOffsetStore 类
  • 总结

前言

前两天有个同事问了一个问题:“在广播模式下消息消费是否会重试?”,而我的答案是会重试,因为在我的印象中RocketMQ有个最少消费一次机制,自然就会想不管怎么样都有可能出现重复消费的情况。但他立马百度查了一下:

image-20240808153752930

啪啪打脸!!!!!!,当时哥们哑口无言,哈哈哈!

但还是带着疑问的,那RocketMQ是怎么保证最少消费一次的?

继续

其实这个问题应该换种问法,按一般的思考逻辑,我们说的重试是是否会进入RocketMQ的重试队列走它的退避算法,所以应该问:“RocketMQ消息消费失败是否会进入重试队列?”,那这个结果是:不会,如果是是否会重试,是会的,就算不考虑生产者重复推送和Rebalance再均衡机制,消息还是有可能造成重试的。

本文不介绍什么是广播模式和集群模式以及生产者重复推送和Rebalance机制,大家可自行了解。本文只介绍消费端

广播和集群模式的消费流程

  • 广播消费模式下,相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息。即每条消息都会被发送到Consumer Group中的每个Consumer。
  • 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息。即每条消息只会被发送到Consumer Group中的某个Consumer。

集群模式(默认的)

  1. 消息生产者(Producer)将消息发送到RocketMQ的一个主题(Topic),Broker 接收到消息后,将其存储在相应的队列(Message Queue)中。
  2. 消费者实例Consumer会订阅一个或多个主题。当一个消费者订阅某个主题时,RocketMQ会根据负载均衡策略将该主题的消息队列分配给消费者实例。
  3. 消费者实例会主动向Broker发送拉取请求(Pull Request),从被分配的消息队列中获取消息。每个消费者只会拉取自己被分配的队列中的消息。
  4. 消息处理,也就是我们自己的代码逻辑
  5. 消费者每次处理完消息后,会提交当前消费进度(Offset)。集群模式下,这一进度信息会被存储在Broker中。
  6. 如果消息处理失败,可以配置RocketMQ的重试机制,消费者会重新拉取并处理失败的消息,直到处理成功或达到最大重试次数。

第6步就是我同事理解的重试机制,会进入重试队列!多一嘴:如果捕获了异常是不会重试的

                 +------------------+
                 |    Producer      |
                 +------------------+
                          |
                          V
                 +------------------+
                 |     Broker       |
                 +------------------+
                     |    |    |    
                  MQ1    MQ2   MQ3  (多个消息队列)
                     |    |    |
                 +--------+--------+
                 |  Consumer Group  |  (集群模式)
                 |                  |
        +--------+--------+--------+--------+
        |   Consumer A    |   Consumer B    |  (多个消费者实例)
        +-----------------+-----------------+
          (Queue 1, Queue 3)   (Queue 2)

广播模式

  1. 消息生产者Producer将消息发送到RocketMQ的一个主题(Topic)
  2. 消费组(Consumer Group在广播模式下依然存在,但其意义有所不同。每个消费组中的所有消费者实例都会消费该组内订阅的所有消息队列中的消息。
  3. 消费者实例(Consumer订阅一个或多个主题。在广播模式下,消费者不会根据负载均衡策略分配队列,而是每个消费者都会接收并消费该主题的所有队列中的消息。
  4. 每个消费者实例会主动向Broker发送拉取请求(Pull Request),从该主题的所有消息队列中获取消息。
  5. 消费者收到消息后,对消息进行处理。
  6. 广播模式下,消费进度由消费者本地管理,而不是由Broker统一管理。消费者在本地记录其消费的消息偏移量(Offset)。
  7. 在广播模式下,由于每个消费者都会接收并处理相同的消息,因此消息不会丢失。
                 +------------------+
                 |    Producer      |
                 +------------------+
                          |
                          V
                 +------------------+
                 |     Broker       |
                 +------------------+
                     |    |    |    
                  MQ1    MQ2   MQ3  (多个消息队列)
                     |    |    |
                 +--------+--------+
                 |  Consumer Group  |  (广播模式)
                 |                  |
        +--------+--------+--------+--------+
        |   Consumer A    |   Consumer B    |  (多个消费者实例)
        +-----------------+-----------------+
         (MQ1, MQ2, MQ3)    (MQ1, MQ2, MQ3)  (每个消费者接收所有队列中的消息)

小结

首先我们要知道一个点,集群模式下,消费指针是保存在broker中的,而广播模式中的消费指针则保存在各自的消费者本地

所以在集群模式下,消费者消费完消息之后是会告诉Broker当前的偏移量的,从而,如果Broker没有收到消费者偏移量的响应,就会造成下次消费仍然从之前的偏移量开始消费,造成重复消费。

而在广播模式中。偏移量既然是保存在各自的消费者本地,那只要没有保存成功,下次还是会从上一次的偏移量拉取。同样也衍生出另一个问题,当我们选择push模式消费时,偏移量既然是保存在本地的,那broker是怎么知道当前消费者知道消费到哪了,从而不重复push呢

这就要详细了解一下RocketMQ的pushpull这两种消费模式了;

push和pull介绍

RocketMQ的消费模型中,严格来说,没有真正的“push”模式,消费者始终是通过主动拉取(pull)消息的方式工作。无论是集群模式还是广播模式,消费者都会周期性地向Broker发送拉取请求,以获取新消息。这种方式可以被看作是“长轮询”或“循环拉取”的一种变体。

所以本质也就是:消费的主动拉取机制分为两种循环拉取长轮询

  • 循坏拉取:消费者会在一个循环中不断地向Broker发送拉取请求,即使没有新消息,消费者也会周期性地发送请求。这种方式确保了当有新消息到达时,消费者可以尽快获取到消息。(这种方式也就是我们常规理解的Pull模式)
  • 长轮询:RocketMQ的拉取请求支持长轮询机制。消费者向Broker发送拉取请求时,可以指定一个最长等待时间(brokerSuspendMaxTimeMillis)。如果在这段时间内Broker没有新消息可供拉取,Broker会在超时之前持有这个请求,一旦有新消息到达就立即返回给消费者。这样可以减少无效的拉取请求,降低系统资源消耗。(这种方式也就是我们理解的Push)

源码展示

这里我们只展示push 的代码,因为只有push是跟我们常规理解不一样的,大家可以自行去看看pull的

DefaultMQPushConsumerImpl 类是消费者的核心实现类,其中负责拉取消息的方法如下:

public class DefaultMQPushConsumerImpl implements MQConsumerInner {
    ..................................
        .............................
        ..........................................
    public void pullMessage(final PullRequest pullRequest) {
            // ProcessQueue: 表示消息处理队列,消费者从Broker拉取的消息会存入ProcessQueue中。
            final ProcessQueue processQueue = pullRequest.getProcessQueue();
            // isDropped: 检查ProcessQueue是否已经被丢弃。如果ProcessQueue被丢弃(可能是因为Rebalance操作),则停止拉取消息。
            if (processQueue.isDropped()) {
                log.info("the pull request[{}] is dropped.", pullRequest.toString());
                return;
            }

            //  更新最后的拉取时间戳,用于判断消费者的活跃状态。
            pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

            try {
                //  检查消费者的状态是否正常,如果异常,则延迟执行拉取请求
                this.makeSureStateOK();
            } catch (MQClientException e) {
                log.warn("pullMessage exception, consumer state not ok", e);
                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                return;
            }

            // 检查消费者是否处于暂停状态,如果是,则延迟拉取请求。
            if (this.isPause()) {
                log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
                this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
                return;
            }

            // 流量控制: 如果本地缓存的消息数量或消息大小超过了阈值,则延迟拉取消息,以避免消费者处理不过来
            long cachedMessageCount = processQueue.getMsgCount().get();
            long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);

            ..................................
                                   .............................
                                   .......................

            // 顺序消费: 如果是顺序消费模式,消费者必须确保消息队列被锁定,如果没有锁定则延迟拉取消息
            // 偏移量修正: 在首次拉取消息时,可能需要修正消费偏移量,以确保从正确的位置开始消费。
            if (!this.consumeOrderly) {
                if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
                   ..................................
                                   .............................
                                   .......................
                    return;
                }
            } else {
                if (processQueue.isLocked()) {
                    if (!pullRequest.isPreviouslyLocked()) {
                        ..................................
                                   .............................
                                   .......................
                    }
                } else {
                    this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                    log.info("pull message later because not locked in broker, {}", pullRequest);
                    return;
                }
            }

            // 检查是否有订阅数据,如果没有,延迟拉取请求。
            final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
            if (null == subscriptionData) {
                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                log.warn("find the consumer's subscription failed, {}", pullRequest);
                return;
            }

            // 这里是拉取消息最重要的逻辑
            final long beginTimestamp = System.currentTimeMillis();

            // 异步拉取消息的回调接口,在onSuccess中处理不同的拉取结果。
            PullCallback pullCallback = new PullCallback() {
                @Override
                // 包含拉取到的消息以及下一次拉取的起始偏移量。
                public void onSuccess(PullResult pullResult) {
                    if (pullResult != null) {
                        pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,
                                subscriptionData);

                        // 根据拉取结果状态进行相应处理,如FOUND表示成功拉取到消息,OFFSET_ILLEGAL表示偏移量非法,需特殊处理。
                        switch (pullResult.getPullStatus()) {
                            case FOUND:
                                .............................
                                    ...............................
                                break;
                            case NO_NEW_MSG:
                            case NO_MATCHED_MSG:
                               ..................................
                                   .............................
                                   .......................
                                break;
                            case OFFSET_ILLEGAL:
                                ..................................
                                   .............................
                                   .......................
                                break;
                            default:
                                break;
                        }
                    }
                }
            };

            // 构建系统标志(sysFlag): 根据不同的条件构建拉取请求的系统标志,标识是否提交偏移量、订阅信息等。
            boolean commitOffsetEnable = false;
            long commitOffsetValue = 0L;
            if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
                commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
                if (commitOffsetValue > 0) {
                    commitOffsetEnable = true;
                }
            }

            String subExpression = null;
            boolean classFilter = false;
            SubscriptionData sd = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
            if (sd != null) {
                if (this.defaultMQPushConsumer.isPostSubscriptionWhenPull() && !sd.isClassFilterMode()) {
                    subExpression = sd.getSubString();
                }

                classFilter = sd.isClassFilterMode();
            }

            int sysFlag = PullSysFlag.buildSysFlag(
                    commitOffsetEnable, // commitOffset
                    true, // suspend
                    subExpression != null, // subscription
                    classFilter // class filter
            );
            try {
                // 调用拉取API: 最终调用pullKernelImpl提交拉取请求,并指定PullCallback进行异步回调处理。
                this.pullAPIWrapper.pullKernelImpl(
                        pullRequest.getMessageQueue(),
                        subExpression,
                        subscriptionData.getExpressionType(),
                        subscriptionData.getSubVersion(),
                        pullRequest.getNextOffset(),
                        this.defaultMQPushConsumer.getPullBatchSize(),
                        sysFlag,
                        commitOffsetValue,
                        BROKER_SUSPEND_MAX_TIME_MILLIS,
                        CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
                        CommunicationMode.ASYNC,
                        pullCallback
                );
            } catch (Exception e) {
                log.error("pullKernelImpl exception", e);
                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
            }
        }
}

我们可以看到DefaultMQPushConsumerImpl依然使用的pullMessage,同时涉及检查消费者状态、流量控制、顺序消费的处理、订阅信息的获取等关键步骤。

那既然了解了pull和push的区别,那么回到问题,在广播模式下,什么情况下会造成偏移量保存的失败:

偏移量保存失败情况

1. 网络问题

  • 网络中断或不稳定: 广播模式下的消费者保存偏移量时仍需要与Broker通信。如果网络出现问题,偏移量保存请求可能会失败。
  • 网络延迟过高: 高延迟的网络可能导致偏移量保存操作超时,进而失败。

2. Consumer本地问题

  • 消费者本地磁盘故障: 在广播模式下,偏移量通常是存储在消费者本地的(例如磁盘或本地文件系统)。如果消费者的磁盘出现问题,偏移量可能无法成功保存。
  • 消费者崩溃: 如果消费者在保存偏移量之前崩溃,偏移量将不会被更新,导致后续重启时重新消费这些消息。

3. 消费进度记录器问题

  • 进度记录器异常: 在广播模式下,消费者自己负责管理消费进度。如果进度记录器出现异常(例如文件系统读写错误、配置错误等),会导致偏移量无法正确保存。

4. 程序设计问题

  • 代码逻辑错误: 如果在实现消费者的代码中存在逻辑错误,例如在偏移量保存前就返回或未正确捕获异常,可能会导致偏移量未被成功保存。
  • 不合理的异常处理: 如果在偏移量保存失败后未能及时重试或补救,偏移量可能会丢失,影响消费进度的准确性。

5. 异常终止

  • 应用强制退出: 如果消费者进程被强制终止或系统突然关机,当前的偏移量可能未能保存到本地,导致重启后从上一个已保存的偏移量开始消费。

6. 持久化策略问题

  • 异步持久化策略: 如果消费者采用异步持久化策略,在保存偏移量时未能及时持久化,进程退出或出现故障时可能导致偏移量丢失。

7. 同步问题

  • 并发更新冲突: 如果同一消费者实例内存在多线程或异步处理逻辑,并发更新偏移量时未进行妥善的同步处理,可能导致偏移量更新失败或记录错误的偏移量。

源码解析

上面我们一直说广播模式的偏移量会保存在本地,那具体是哪呢?

OffsetStore 接口

OffsetStore 是管理消费进度的接口,其具体实现类包括 LocalFileOffsetStoreRemoteBrokerOffsetStore

广播模式下使用 LocalFileOffsetStore 进行本地存储。

public interface OffsetStore {
    void load() throws MQClientException;

    void updateOffset(final MessageQueue mq, final long offset, final boolean increaseOnly);

    long readOffset(final MessageQueue mq, final ReadOffsetType type);

    void persistAll(final Set<MessageQueue> mqs);

    void persist(final MessageQueue mq);

    void removeOffset(MessageQueue mq);

    void cloneOffset(final MessageQueue srcMQ, final MessageQueue destMQ);
}

LocalFileOffsetStore 类

LocalFileOffsetStore 类在本地文件中存储消费进度。

image-20240822153149537

public class LocalFileOffsetStore implements OffsetStore {
    // 本地文件路径
    private final String storePath;

    // 用于存储消费进度的内存结构
    private ConcurrentMap<MessageQueue, AtomicLong> offsetTable;

    // 构造函数
    public LocalFileOffsetStore(MQClientInstance mQClientFactory, String consumerGroup) {
        this.storePath = mQClientFactory.getClientConfig().getClientLocalOffsetStoreDir()
                + File.separator + consumerGroup;
        this.offsetTable = new ConcurrentHashMap<MessageQueue, AtomicLong>();
    }

    @Override
    public void persistAll(final Set<MessageQueue> mqs) {
        if (null == mqs || mqs.isEmpty())
            return;

        String encodeFileName = this.storePath + File.separator + "offsets.json";
        // 持久化消费进度到本地文件
        // 文件写入逻辑省略
    }

    @Override
    public void updateOffset(final MessageQueue mq, final long offset, final boolean increaseOnly) {
        AtomicLong offsetOld = this.offsetTable.get(mq);
        if (null == offsetOld) {
            this.offsetTable.putIfAbsent(mq, new AtomicLong(offset));
            offsetOld = this.offsetTable.get(mq);
        }
        if (increaseOnly) {
            MixAll.compareAndIncreaseOnly(offsetOld, offset);
        } else {
            offsetOld.set(offset);
        }
    }

    @Override
    public long readOffset(final MessageQueue mq, final ReadOffsetType type) {
        // 从内存或本地文件中读取消费进度
        // 读取逻辑省略
        return 0;
    }

    // 其他方法省略
}

在广播模式下,消费者使用 LocalFileOffsetStore 在本地存储消费进度。消费者不会将消费进度汇报给Broker,而是通过 persistAllupdateOffset 方法将消费进度存储在本地文件中。这确保了消费者在重启时可以从上次的消费进度继续消费,以保证至少消费一次的语义。

总结

所以在广播模式中,消费失败是不会进入重试队列的,这也是我同事想描述的,而我理解的是重复消费;所以在广播模式中只要本地偏移量没有持久化,就会造成重复消费

很多人看不到未来,其实看到了未来 ———————《弱智吧》

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

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

相关文章

亚马逊测评号生存法则:如何抵御亚马逊封号风波?

距离黑五购物狂欢节还剩99天&#xff0c;相信各位商家都在紧锣密鼓的筹备相关事宜&#xff0c;然而&#xff0c;亚马逊的封号风波再次席卷而来。那如何在这场风暴中让亚马逊矩阵测评号安全航行亦或是脱颖而出呢&#xff1f;本文将给你一个答案&#xff0c;并帮助你的亚马逊店铺…

【PyTorch快速入门教程】03 PyTorch基础知识

在PyTorch中&#xff0c;最小的计算单元是张量&#xff08;tensor&#xff09;。因此关于张量的学习还是至关重要的。通过本章节学习&#xff0c;希望你对张量有一个更清晰的了解。 文章目录 1 什么是Tensor2 PyTorch中Tensor使用2.1 创建Tensor2.1.1 直接创建Tensor2.1.2 间接…

anaconda上安装pytorch

1、选择anaconda prompt 2、创建虚拟环境 3、激活进入虚拟环境 4、安装pytorch 怎么得到上面的这串命令&#xff1f; 输入nvidia-smi&#xff0c;查看cuda的版本号为11.7 我这里选择安装cuda的版本号为11.3&#xff0c;满足向下兼容即可。 在安装深度学习环境时&#xff0c;要…

探索《黑神话·悟空》背后的AI技术支持:英伟达全景光线追踪技术、DLSS 3.5 与帧生成

引言 2023 年&#xff0c;游戏《黑神话悟空》以其震撼的视觉效果和深度沉浸的游戏体验&#xff0c;成为全球玩家热议的焦点。这款游戏在发布初期就取得了惊人的销量&#xff1a;预售阶段便突破 120 万套&#xff0c;而发售首日更是达到 450 万份的惊人成绩。这个现象级作品背后…

走进 “星星的孩子” 的世界:理解与关爱儿童自闭症

在这个充满生机与活力的世界里&#xff0c;有一群特殊的孩子&#xff0c;他们仿佛来自遥远的星球&#xff0c;沉浸在自己的独特世界中&#xff0c;难以与外界进行有效的沟通和互动。他们是自闭症儿童&#xff0c;也被称为 “星星的孩子”。 自闭症&#xff0c;又称孤独症谱系障…

Linux 软件编程 数据库

1. 大批量数据存储和管理时使用数据库 2.创建表 create table 表名称(列1 数据类型, 列2 数据类型, ...); 3.插入表 insert into 表名称 values(值1, 值2, ...); 4.查看表 select 列1,列2,... from 表名称 where 匹配条件 order by 列名称 asc/desc; 5.删除表 delete from …

种田RPG游戏(五)

一、重新设置物品栏 1、打开Scripts-Inventory文件新建 ItemSlotData.cs using System.Collections; using System.Collections.Generic; using UnityEngine;[System.Serializable] //单独的类 public class ItemSlotData {public ItemData itemData;//ItemData对象&#xff…

Java 入门指南:Queue 接口

Collection 接口 Collection 接口提供了一系列用于操作和管理集合的方法&#xff0c;包括添加、删除、查询、遍历等。它是所有集合类的根接口&#xff0c;包括 List、Set、Queue 等。 Collection 接口常见方法 add(E element)&#xff1a;向集合中添加元素。 addAll(Collecti…

大模型笔记之-XTuner微调个人小助手认知

前言 使用XTuner 微调个人小助手认知 一、下载模型 #安装魔搭依赖包 pip install modelscope新建download.py内容如下 其中Shanghai_AI_Laboratory/internlm2-chat-1_8b是魔搭对应的模型ID cache_dir/home/aistudio/data/model’为指定下载到本地的目录 from modelscope im…

Stable Diffusion的微调方法原理总结

目录 1、Textural Inversion&#xff08;简易&#xff09; 2、DreamBooth&#xff08;完整&#xff09; 3、LoRA&#xff08;灵巧&#xff09; 4、ControlNet&#xff08;彻底&#xff09; 5、其他 1、Textural Inversion&#xff08;简易&#xff09; 不改变网络结构&…

Ciallo~(∠・ω・ )⌒☆第二十五篇 Redis

Redis 是一个高性能的键值存储数据库&#xff0c;它能够在内存中快速读写数据&#xff0c;并且支持持久化到磁盘。它被广泛应用于缓存、队列、实时分析等场景。 一、启动redis服务器 要打开redis服务器&#xff0c;需要在终端中输入redis-server命令。确保已经安装了redis&…

【Java】/* 链式队列 和 循环队列 - 底层实现 */

一、链式队列 1. 使用双向链表实现队列&#xff0c;可以采用尾入&#xff0c;头出 也可以采用 头入、尾出 (LinkedList采用尾入、头出) 2. 下面代码实现的是尾入、头出&#xff1a; package bageight;/*** Created with IntelliJ IDEA.* Description:* User: tangyuxiu* Date: …

mOTA v2.0

mOTA v2.0 一、简介 本开源工程是一款专为 32 位 MCU 开发的 OTA 组件&#xff0c;组件包含了 bootloader 、固件打包器 (Firmware_Packager) 、固件发送器 三部分&#xff0c;并提供了基于多款 MCU (STM32F1 / STM32F407 / STM32F411 / STM32L4) 和 YModem-1K 协议的案例。基…

【文献及模型、制图分享】2000—2020年中国青饲料播种面积及供需驱动因素的时空格局

文献介绍 高产、优质的青饲料对于国家畜牧业发展和食物供给至关重要。然而&#xff0c;当前对于青饲料播种面积时空变化格局及其阶段性特征、区域差异以及影响因素等尚未清楚。 本文基于省级面板数据分析了2000—2020年青饲料种植的时空格局变化&#xff0c;结合MODIS-NPP产品…

Nginx 405 not allowed

问题原因&#xff1a;nginx不允许静态文件被post请求 解决&#xff1a;添加error_page 405 200 $request_uri;

白酒与家庭:团圆时刻的需备佳品

在中国传统文化中&#xff0c;家庭是社会的基石&#xff0c;是每个人心灵的港湾。而团圆&#xff0c;则是家庭生活中较美好的时刻。在这样一个特殊的日子里&#xff0c;白酒&#xff0c;尤其是豪迈白酒&#xff08;HOMANLISM&#xff09;&#xff0c;成为了团圆时刻的需备佳品。…

了解JS数组元素及属性

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1、定义数组并输出2、查询数组的长度3、访问数组的第一个元素4、访问数组中第一个元素的xxx属性5、从数组元素中提取ID并存储到搜索参数对象 提示&#xff1a;以下是…

C++设计模式1:单例模式(懒汉模式和饿汉模式,以及多线程问题处理)

饿汉单例模式 程序还没有主动获取实例对象&#xff0c;该对象就产生了&#xff0c;也就是程序刚开始运行&#xff0c;这个对象就已经初始化了。 class Singleton { public:~Singleton(){std::cout << "~Singleton()" << std::endl;}static Singleton* …

KUKA KR C2 中文操作指南 详情见目录

KUKA KR C2 中文操作指南 详情见目录

Selenium + Python 自动化测试22(PO+数据驱动)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以独立完成自动化测试的任务。 上一篇我们讨论了PO模式和unittest框架结合起来使用。 本篇文章我们综合一下之前学习的内容&#xff0c;如先将PO模式、数据驱动思想和我们生成HTML报告融合起来&#xff0c;综…