RocketMQ4.3.X笔记(4):消费者 Consumer

消费者概述

消费者类型

在RocketMQ中根据使用者对读取操作的控制情况,消费者可分为两种类型。

  • 一个是 DefaultMQPushConsumer ,由系统控制读取操作,收到消息后自动调用传入的处理方法来处理;
  • 另一个是 DefaultMQPullConsumer ,读取操作中的大部分功能由使用者自主控制。

消费者组与订阅模式

  1. ConsumerGroup: Consumer 的 GroupName 用于把多个Consumer 组织到一起,提高并发处理能力,GroupName 需要和消息模式( MessageModel )配合使用。
  2. MessageModel:RocketMQ 支持两种消息模式: Clustering(默认) 和 Broadcasting
  3. ConsumerGroup 与 MessageModel 配合:
  • 在Clustering 模式下,同一个 ConsumerGroup ( GroupName 相同) 里的每个Consumer 只消费所订阅消息的一部分内容, 同一个 ConsumerGroup 里所有的Consumer 消费的内容合起来才是所订阅Topic 内容的整体,从而达到负载均衡的目的
  • 在Broadcasting 模式下,同一个ConsumerGroup 里的每个Consumer 都能消费到所订阅Topic 的全部消息,也就是一个消息会被多次分发,被多个Consumer 消费

MessageListener

  1. MessageListenerConcurrently
  • 使用者将并发地使用消息。为了获得良好的性能,建议使用这种方法。
  • 不建议抛出异常,可以返回ConsumeConcurrentlyStatus.RECONSUME_LATER状态
  1. MessageListenerOrderly
  • 使用者将锁定每个MessageQueue,以确保按顺序逐个使用它。这将导致性能损失,但是当关心消息的顺序时,这是非常有用的。
  • 不建议抛出异常,可以返回ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT代替

消费状态 Consume Status

  1. MessageListenerConcurrently
  • ConsumeConcurrentlyStatus.CONSUME_SUCCESS:成功消费
  • ConsumeConcurrentlyStatus.RECONSUME_LATER:告诉使用者现在不使用它,以后需要重新使用它。然后可以继续使用其他消息。
  1. MessageListenerOrderly 顺序消息
  • ConsumeOrderlyStatus.SUCCESS 消费成功
  • ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT:因为关心顺序,所以不能跳过消息,但是可以返回 SUSPEND_CURRENT_QUEUE_A_MOMENT 来告诉使用者等待一会儿

线程池大小 Thread Number

消费端使用 ThreadPoolExecutor 在内部处理消费,因此可以通过设置 setConsumeThreadMinsetConsumeThreadMax 来更改它

ConsumeFromWhere

当一个新的Consumer Group建立时,它将需要决定是否需要使用Broker中已经存在的历史消息。

  • CONSUME_FROM_LAST_OFFSET:将忽略历史消息,并使用此后生成的任何消息。
  • CONSUME_FROM_FIRST_OFFSET:将消费 Broker 中存在的所有消息。
  • CONSUME_FROM_TIMESTAMP:来消费指定时间戳之后生成的消息

消息重复 Duplication

  1. 消息重复的原因
  • Producer 重新发送,如 FLUSH_SLAVE_TIMEOUT 情况
  • Consumer 消费了一些消息,这些消息消费的 offsets 没有来得及更新到Broker,这时 Consumer 挂了
  1. 因此,如果应用程序不能容忍重复,那么可能需要进行一些外部工作来处理这个问题。例如,可以检查数据库的主键、使用幂等操作

PushConsumer

DefaultMQPushConsumer 的简单使用

  1. 使用DefaultMQPushConsumer 主要是设置好各种参数和传入处理消息的函数。系统收到消息后自动调用处理函数来处理消息,自动保存Offset ,而且加入新的DefaultMQPushConsumer 后会自动做负载均衡。
  2. DefaultMQPushConsumer 需要设置三个参数:
  • 一是这个Consumer 的 GroupName
  • 二是NameServer 的地址和端口号
  • 三是Topic 的名称
  1. 设置 MessageModel。 RocketMQ 支持两种消息模式: Clustering 和 Broadcasting

  2. NameServer 的地址和端口号,可以填写多个,用分号隔开,达到消除单点故障的目的

  3. Topic 名称用来标识消息类型,需要提前创建。

  • 如果不需要消费某个 Topic 下的所有消息,可以通过指定消息的 Tag 进行消息过滤,比如:Consumer.subscribe ("Topic Test", "tagl||tag2||tag3"),( Tag 是在发送消息时设置的标签) 。
  • 在填写Tag 参数的位置,用null或者* 表示要消费这个Topic的所有消息
// Consumer 的 GroupName 用于把多个 Consumer 组织到一起,提高并发处理能力。
// GroupName 需要和消息模式(MessageModel)配合使用
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("base_consumer_group");

// NameServer 的地址与端口号。多个用分号隔开,达到消除单点故障的目的
pushConsumer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");

// 设置消费位
pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

// CLUSTERING 模式,同一个 ConsumerGroup(GroupName相同)里的每个 Consumer 只消费所订阅消息的一部分。同一个 ConsumerGroup 里所有的 Consumer 消费的内容合起来才是所订阅Topic内容的整体,从而达到负载
// BROADCASTING 模式,同一个 ConsumerGroup 里面的每个 Consumer 都能消费到所订阅 Topic 的全部消息。也就是一个消息会被多次分发,被多个 Consumer 消费
pushConsumer.setMessageModel(MessageModel.CLUSTERING);

// 设置 Topic 的名称用来标识消息类型。
// 如果不需要消费某个topic 下的所有消息,可以通过指定消息的 Tag 进行消息过滤。null 或者 * 表示要消费全部消息
pushConsumer.subscribe("base_topic", "*");


pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {

        System.out.printf(Thread.currentThread().getName() + "收到新消息:" + msgs + "%n");
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

pushConsumer.start();

DefaultMQPushConsumer 的处理流程

  1. DefaultMQPushConsumer 主要功能是 DefaultMQPushConsumerImpl 类实现的,消息的处理在于方法 pullMessage 中的 PullCallback 中。在 PullCallback 的 onSuccess 函数中有个 switch 语句,根据从 Broker 返回的消息类型做相应的处理。
public void pullMessage(final PullRequest pullRequest) {
    ... ...
    PullCallback pullCallback = new PullCallback() {
                @Override
                public void onSuccess(PullResult pullResult) {
    
                        switch (pullResult.getPullStatus()) {
                            case FOUND:
                        
                            case NO_NEW_MSG:
               
                            case NO_MATCHED_MSG:
         
                            case OFFSET_ILLEGAL:
               
                        }
                
                }
    };
}
  1. DefaultMQPushConsumer 的源码中有很多 PullRequest 的语句,原因在于 Push是通过长轮询的方式达到的,长轮询方法既有 pull 的优点,又兼具Push方式的实时性。
  • Push 方式是 Server 端接收到消息后,主动把消息推送给Client端,实时性高。但是有很多弊端:
    • 加大了 Server 的工作量影响 Server 的性能;
    • 其次 Client的 处理能力各不相同,Client的状态不受Server控制,如果Client不能及时处理 Server 推送过来的消息,会造成各种潜在的问题。
  • Pull 方式是 Client 端循环地从 Server 端拉取消息,主动权在 Client 手中,自己拉取到一定数量后,处理妥当了在接着拉取。Pull 方式的问题时循环拉取的间隔不好设定,间隔太短就出在一个"盲等"的状态,浪费资源;每个pull的时间间隔太长,Server端有消息到来时,有可能没有被及时处理。
  1. 长轮询方式通过 Client 端和 Server 端的配合,达到既拥有 Pull 的优点,又能达到保证实时性的目的。
  • Pull 源码 requestHeader.setSuspendTimeoutMillis (brokerSuspendMaxTimeMillis) 设置 Broker 最长阻塞时间,默认是15秒,是Broker在没有新消息的时候才阻塞,有消息会立即返回
  • 长轮询服务端接收到消息请求后,如果队列里面没有新消息,并不急于返回,通过一个循环不断查看状态,每次 waitForRunning 一段时间(默认是5秒),然后再Check。
  • 默认情况下当 broker 一直没有新消息,第三次check的时候,等待时间超过 request 里面的 SuspendMaxTimeMills ,就返回空结果。在等待的过程中,Broker 收到新信息,会直接返回。
  • 长轮询的核心是 Broker 端 Hold 住客户单过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给客户端。
  • 长轮询的主动权还是掌握在客户端手中,broker 即使有大量消息堆积,也不会主动推送给客户端
  1. 长轮询方式的局限性,是在 hold 住 Consumer 请求时候需要占用资源,它适合在消息队列客户端连接数可控的场景
  2. 关于长轮询的具体实现,见源码分析的博文

DefaultMQPushConsumer 流量控制

  1. 原理
  • 线程池。PushConsumer 的核心还是Pull方式,有个线程池,消息处理逻辑在各个线程里同时执行。配置这个线程池可以控制消费的流量。
  • ProcessQueue。
    • RocketMQ 定义了一个快照类ProcessQueue。,在PushConsumer 运行的时候, 每个Message Queue 都会有个对应的ProcessQueue对象,保存了这个Message Queue 消息处理状态的快照
    • ProcessQueue 对象里主要的内容是一个TreeMap 和一个读写锁。TreeMap里以Message Queue 的Offset 作为Key ,以消息内容的引用为Value ,保存了所有从MessageQueue 获取到,但是还未被处理的消息; 读写锁控制着多个线程对TreeMap 对象的并发访:
    • PushConsumer 会判断获取但还未处理的消息个数、消息总大小、Offset 的跨度,任何一个值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的。此外Process Queue 还可以辅助实现顺序消费的逻辑
  1. Consumer 可以设置四个参数进行流量控制:
  • setConsumeThreadMin 设置Consumer 的线程数
  • setConsumeThreadMax 设置Consumer 的线程数
  • setPullBatchSize 指的是一次从Broker 的一个Message Queue 获取消息的最大数量,默认值是32
  • setConsumeMessageBatchMaxSize 指的是这个Consumer的Executor (也就是调用MessageListener 处理的地方)一次传人的消息数( List msgs 这个链表的最大长度),默认值是 1

PullConsumer 简单使用

使用 DefaultMQPullConsumer 像使用DefaultMQPushConsumer 一样需要设置各种参数,写处理消息的函数,同时还需要做额外的事情。

  • 获取 Message Queue 并遍历。
  • 维护 Offsetstore。
  • 根据不同的消息状态做不同的处理。
public class PullConsumer {
    private static final Map<MessageQueue, Long> OFFSET_TABLE = new HashMap<>();

    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");

        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
        # 一个Topic 包括多个Message Queue ,如果这个Consumer 需要获取Topic下所有的消息,就要遍历多有的Message Queue 。
        # 如果有特殊情况,也可以选择某些特定的Message Queue 来读取消息
        for (MessageQueue mq : mqs) {
            System.out.printf("Consume from the queue: %s%n", mq);
            SINGLE_MQ:
            while (true) {
                try {
                    PullResult pullResult =
                            consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    System.out.printf("%s%n", pullResult);

                    # 维护 offset。从一个Message Queue 里拉取消息的时候,要传人Offset 参数( long 类型的值),随着不断读取消息, Offset 会不断增长。这个时候由用户负责把Offset存储下来,根据具体情况可以存到内存里、写到磁盘或者数据库里等
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

                    # 根据不同的消息状态做不同的处理
                    # 拉取消息的请求发出后,会返回: FOUND 、NO_MATCHED_MSG 、NO_NEW_MSG 、OFFSET_ILLEGAL 四种状态,需要根据每个状态做不同的处理。比较重要的两个状态是FOUNT 和NO_NEW_MSG ,分别表示获取到消息和没有新的消息
                    switch (pullResult.getPullStatus()) {
                        case FOUND:
                            break;
                        case NO_MATCHED_MSG:
                            break;
                        case NO_NEW_MSG:
                            break SINGLE_MQ;
                        case OFFSET_ILLEGAL:
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        consumer.shutdown();
    }

    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = OFFSET_TABLE.get(mq);
        if (offset != null) {
            return offset;
        }

        return 0;
    }

    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        OFFSET_TABLE.put(mq, offset);
    }

}

Consumer 的启动、关闭流程

  1. 对于PullConsumer 来说,使用者主动权很高,可以根据实际需要暂停、停止、启动消费过程。需要注意的是 Offset 的保存,要在程序的异常处理部分增加把 Offset 写人磁盘方面的处理,记准了每个Message Queue 的Offset ,才能保证消息消费的准确性。

  2. DefaultMQPushConsumer 的退出, 要调用shutdown()函数, 以便释放资源、保存Offset 等。这个调用要加到Consumer 所在应用的退出逻辑中。

  3. PushConsumer 在启动的时候,会做各种配置检查,然后连接NameServer获取Topic 信息,启动时如果遇到异常,比如无法连接NameServer,程序仍然可以正常启动不报错(日志里有WARN 信息) 。在单机环境下可以测试这种情况,启动DefaultMQPushConsumer 时故意把NameServer 地址填错,程序仍然可以正常启动,但是不会收到消息。

  • 这和分布式系统的设计有关, RocketMQ 集群可以有多个NameServer 、Broker,某个机器出异常后整体服务依然可用。所以 DefaultMQPushConsumer 被设计成当发现某个连接异常时不立刻退出,而是不断尝试重新连接。可以进行这样一个测试,在 DefaultMQPushConsumer 正常运行的时候,手动 kill 掉 Broker 或NameServer ,过一会儿再启动。会发现 DefaultMQPushConsumer 不会出错退出,在服务恢复后正常运行,在服务不可用的这段时间,仅仅会在日志里报异常信息。
  1. 如果需要在 DefaultMQPushConsumer 启动的时候,及时暴露配置问题,该如何操作呢? 可以在Consumer.start()语句后调用: Consumer.fetchSubscribeMessageQueues ("TopicName") ,这时如果配置信息写得不准确,或者当前服务不可用,这个语句会报 MQC!ientException 异常

存储队列位置信息 offset

Offset 概述

  1. RocketMQ 中, 一种类型的消息会放到一个Topic 里,为了能够并行, 一般一个Topic 会有多个Message Queue (也可以设置成一个), Offset 是指某个Topic 下的一条消息在某个Message Queue 里的位置,通过 Offset的值可以定位到这条消息,或者指示Consumer 从这条消息开始向后继续处理。
  2. Offset 的类结构,主要分为本地文件类型和Broker 代存的类型两种。
  • 对于 DefaultMQPushConsurner 来说,
    • 默认是 CLUSTERING 模式,由Broker 端存储和控制Offset 的值,使用 RemoteBrokerOffsetStore 结构。
    • BROADCASTING 模式下,每个Consumer都收到这个Topic 的全部消息,各个Consumer 间相互没有干扰, RocketMQ 使用 LocalfileOffsetStore ,把Offset 存到本地。

OffsetStore

  1. OffsetStore 使用Json 格式存储
  2. 在使用 ·DefaultMQPushConsumer 的时候,我们不用关心 OffsetStore 的事,但是如果 PullConsumer,我们就要自己处理 OffsetStore。上面的例子 PullConsumer.java ,使用内存存储。

Offset 的使用

  1. 设置Consumer 读取消息的初始位置,DefaultMQPushConsumer 类里函数 setConsumeFromWhere(ConsumeFromWhere.CONSUME FROM_FIRST_OFFSET)实现这个功能。
  2. 具体设置的类型,见上面 ConsumeFromWhere 的类型说明。如设置从某个时间开始消费消息, Consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP), Consumer.setConsumeTimestamp("20131223171201"),时间戳格式是精确到秒的。
  3. 注意设置读取位置不是每次都有效,它的优先级默认在Offset Store 后面。
  • 比如在DefaultMQPushConsumer 的 BROADCASTING 方式下,默认是从Broker 里读取某个Topic 对应ConsumerGroup 的Offset , 当读取不到Offset 的时候, ConsumeFromWhere 的设置才生效。
  • 大部分情况下这个设置在ConsumerGroup 初次启动时有效。如果Consumer 正常运行后被停止, 然后再启动, 会接着上次的Offset 开始消费, ConsumeFrom Where 的设置元效

参考

  1. Apache RocketMQ 官网
  2. 官方 consumer
  3. Learning_RocketMQ 源码
  4. 官方例子
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值