【消息中间件】--- RocketMQ消费者简介(集群、广播消费,推模式,拉模式)

本文对应源码地址:https://github.com/nieandsun/rocketmq-study
rocketmq官网:https://rocketmq.apache.org/docs/quick-start/
rocketmq github托管地址(这里直接给出的是中文docs地址):https://github.com/apache/rocketmq/tree/master/docs/cn


1 集群消费/广播消费概念简介

消息队列 RocketMQ 是基于发布/订阅模型的消息系统。 消息的订阅方订阅关注的 Topic, 以获取并消费消息。 由于订阅方应用一般是分布式系统,以集群方式部署有多台机器。 因此消息队列 RocketMQ 约定以下概念:


1.1 集群

集群: 使用相同 Group ID 的订阅者属于同一个集群。 同一个集群下的订阅者消费逻辑必须完全一致(包括 Tag 的使用) , 这些订阅者在逻辑上可以认为是一个消费节点。


1.2 集群消费(Clustering)+ 使用场景&注意事项

集群消费: 当使用集群消费模式时, 消息队列 RocketMQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可 —> 工作原理如下图所示:
在这里插入图片描述


适用场景&注意事项:

  • (1)消费端集群化部署, 每条消息只需要被处理一次;
  • (2)由于消费进度在服务端维护, 可靠性更高。
  • (3)Topic + Tag下的消息可以保证肯定会被整个集群至少消费一次 ;
  • (4)不保证每一次失败重投的消息路由到同一台机器上, 因此处理消息时不应该做任何确定性假设。
  • (5)集群中的每个消费者消费的消息肯定不会是同一条消息,因为实际上在集群模式下
    • 每一个queue都只能被一个消费者消费
    • 但是每一个消费者都可以消费多个queue

因此,如下图所示,每个消费者消费的肯定不是同一个消息。
在这里插入图片描述


1.3 广播消费(Broadcasting)+ 使用场景&注意事项

广播消费: 当使用广播消费模式时, 消息队列 RocketMQ 会将每条消息推送给集群内所有注册过的客户端, 保证消息至少被每台机器消费一次 —> 作原理如下图所示:
在这里插入图片描述


相比于集群模式,广播模式的特点为: 每个消费者都会消费所订阅的Topic + Tag下的所有queue中的所有消息。
适用场景&注意事项:

  • (1)广播消费模式下不支持顺序消息。
  • (2)广播消费模式下不支持重置消费位点。
  • (3)每条消息都需要被相同逻辑的多台机器处理。
  • (4)广播模式下, 消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次, 但是并不会对消费失败的消息进行失败重投, 因此业务方需要关注消费失败的情况。
  • (5)广播模式下, 客户端每一次重启都会从最新消息消费。 客户端在被停止期间发送至服务端的消息将会被自 动跳过, 请谨慎选择
  • (6)广播模式下, 每条消息都会被大量的客户端重复处理, 因此推荐尽可能使用集群模式。
  • (7)目前仅 Java 客户端支持广播模式。
  • (8)广播模式下服务端不维护消费进度, 所以消息队列 RocketMQ 控制台不支持消息堆积查询、 消息堆积报警和订阅关系查询功能。
  • (9)消费进度在客户端维护, 出现消息重复消费的概率稍大于集群模式。

1.4 简单聊一聊RocketMQ的设计理念 — 至少一次


1.4.1 RocketMQ保证至少消费一次的原理简介

上面介绍到:

  • 在集群模式下,RocketMQ 可以保证Topic + Tag下的消息可以肯定会被整个集群至少消费一次
  • 在广播模式下,RocketMQ 可以保证至少被每台机器消费一次

其实现原理是什么呢?官网(https://github.com/apache/rocketmq/blob/master/docs/cn/features.md)介绍如下。

这里其实和mysql实现事务的原理非常类似,我就不过多言语了。
在这里插入图片描述


1.4.2 至少一次不是有且仅有一次 — 若幂等性要求高,需在业务层面进行去重处理

这里并不是我咬文嚼字,因为官网上(https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md)有对其进行明确地说明:
在这里插入图片描述
可以想象,RocketMQ之所以这样做肯定是为了效率而不去做更多的判断 。
给我们的提示: 如果项目中的幂等性确实要求比较高 —> 肯定要在业务层面进行去重处理。


2 集群消费测试

测试代码如下:

/**
 * 消费者-推模式-集群
 */
public class PushClusterConsumerA {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        //实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");

        //设置NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");//指定NameServer地址

        //订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
        consumer.subscribe("Topic-NRSC", "*");

        //设置消费模式为广播模式
        //consumer.setMessageModel(MessageModel.BROADCASTING);

        //每次从最后一次消费的地址
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

        //注册回调实现类来处理从broker拉取回来的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("ConsumerPartOrder Started.%n");
    }
}

测试结果:
先启动三个同groupName的消费者组成一个集群,他们都订阅主题 — Topic-NRSC, 然后启动生产者向broker发送10条消息,三个消费者消费的消息分别如下:

  • ConsumerA

在这里插入图片描述

  • ConsumerB

在这里插入图片描述

  • ConsumerC

在这里插入图片描述

再回过头去看1中所讲的集群消费的概念,相信你肯定就都懂了。


3 广播消费测试

测试代码如下:
只需要将2中设置消费模式为广播模式:下注释掉的那一行代码打开就可以了。

测试结果:
先启动三个以广播模式接收消息的消费者,他们都订阅主题 — Topic-NRSC, 然后启动生产者向broker发送10条消息,三个消费者消费的消息分别如下:

  • ConsumerA、ConsumerB和ConsumerC 结果一样,如下:

在这里插入图片描述

再回过头去看1中所讲的广播消费的概念,相信你肯定也都懂了。


4 工作经验分享

(1)说实话我们的项目中其实没用过广播模式
(2)假使你的业务里真的需要使用广播模式,其实我觉得你也可以在开发中先用集群模式,为什么这样说呢?

有心的读者可能看到了,在1中介绍集群消费和广播消费时,分别有两句话:

  • 集群模式: 由于消费进度在服务端维护, 可靠性更高。
  • 广播模式: 服务端不维护消费进度, 所以消息队列 RocketMQ 控制台不支持消息堆积查询、 消息堆积报警和订阅关系查询功能。

上面这两句话的具体意思是啥呢?
其实很简单,假设消费者还没开启,此时生产者向broker发送了10条Topic 为 XXX 的消息,那么:

  • 在集群模式下

    • 如果此时开启一个订阅了 XXX 消息的消费者肯定会接收到这10条中的某一条或某几条
    • 更爽的是假如你把这个进程停了,只要换个消费者groupName再重新启动进程就又可以再消费这10条中的某一条或某几条消息了
    • 这有啥好处呢??? —> 当然是非常方便在debug模式下调试+ 优化你的代码了。
  • 在广播模式下

    • 对不起,这时候你将收不到任何消息,因为广播模式下,服务端不维护消费进度----> 客户端每一次重启都会从最新消息消费。 客户端在被停止期间发送至服务端的消息将会被自动跳过, 因此请谨慎选择

5 拉取式消费(Pull Consumer)简介

其实2 中的代码就是推动式消费(Push Consumer) ,在该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高 —》 据说在底层其实它还是使用了拉取模式,只是消费者和broker之间维持了一个长连接。


接下来看一下拉取模式如何实现消息消费,示例代码如下。

/***
 * 拉模式
 */
public class PullConsumer {
    private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pullconsumer");
        consumer.setNamesrvAddr("localhost:9876");
        //consumer.setBrokerSuspendMaxTimeMillis(1000);

        System.out.println("ms:" + consumer.getBrokerSuspendMaxTimeMillis());
        consumer.start();

        //1.获取MessageQueues并遍历(一个Topic包括多个MessageQueue)
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("Topic-NRSC");
        for (MessageQueue mq : mqs) {
            System.out.println("queueID:" + mq.getQueueId());
            //获取偏移量
            long Offset = consumer.fetchConsumeOffset(mq, true);

            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);
                    //2.维护Offsetstore(这里存入一个Map)
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());

                    //3.根据不同的消息状态做不同的处理
                    switch (pullResult.getPullStatus()) {
                        case FOUND: //获取到消息
                            for (int i = 0; i < pullResult.getMsgFoundList().size(); i++) {
                                System.out.printf("%s%n", new String(pullResult.getMsgFoundList().get(i).getBody()));
                            }
                            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 = OFFSE_TABLE.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

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

说实话代码量相比于推模式还是挺多的,而且感觉并没有推模式好理解。我在项目里其实也没用过这种模式。
有兴趣的自己clone下来本文对应的源码研究研究吧:https://github.com/nieandsun/rocketmq-study, 这里我就不过多展开了。


  • 简单看一下上诉代码的执行结果:

在这里插入图片描述


end !!! — 2020/05/18 02:07

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/106180416