RocketMQ:消费者

46 阅读7分钟

消费者

  • ConsumerGroup :标识同一类消费者,同一组中的消费者必须保持相同的消费逻辑和配置。
  • 订阅关系:topic和tag构成,同组内的订阅关系保持一致。

订阅关系不一致,会导致消费为什么问题? 消费消息紊乱 (消息丢失,重复消费)

订阅关系不一致:同组内消费者存在topic不同和tag不同的情况

  • topic不同:同一个topic下执行rebalence时,每个消费者都会分配到自己要消费的队列,由于topic不同会导致有的消费者实际并没有订阅这个topic,从而不会消费这个对应topic的消息,这个时候这个组内会有固定的队列的消息没有被消费到,出现消息丢失
  • tag不同:同理,会出现消息丢失,如果和其他组的tag重复,会出现重复消费的情况。

Rebalence机制:给消费者组分配其要消费的队列

同组内一个队列只会有一个消费者去消费

消费模式

  • 集群消费:按照消费者组,负载均衡的消费Topic内的消息 不同消费者组的消费位点独立,也就是说订阅关系相同(topic和tag相同)的两个消费者组,同一个消息会被这两个消费者组分别消费一次。
  • 广播消费:消息会被消费者组内所有消费者都消费一次,如用于集群内服务统一刷新本地缓存。

消费的可靠性保证

重试-死信机制

  • 重试Topic:某个消息被消费失败了,会被保存对应的重试Topic中,重试Topic被自动订阅。命名%RETRY%消费者组 进入重试Topic的消息有16次重试的机会,会按照一定的时间间隔进行,4个小时内会重试完
  • 死信Topic:如果消费了17次都没有成功,消息会被保存到对应的死信队列,需要人工介入处理。命名%DLQ%消费者组名

处理死信Topic:在消息没有被删除之前处理,在排查出具体原因之前,及时在控制台导出死信消息,排查出原因后重新发送死信消息。

Rebbalance机制

  • 消费者组内成员变更,Broker掉线以及Topic队列数量发生变化,组内成员都会自动感知重新触发Rebalance

RocketMQ提供了默认的5种分配策略:也可以自定义实现。

  • AllocateMessageQueueAveragely:平均分配,默认策略,也是推荐使用策略
  • AllocateMessageQueueAveragelyByCircle:环形分配策略。
  • AllocateMessageQueueByConfig:手动配置。
  • AllocateMessageQueueConsistentHash:一致性Hash分配。
  • AllocateMessageQueueByMachineRoom:机房分配策略。

消费方式

  • DefaultMQPullConsumer : 需要主动调用拉取API获取对应Topic的消息然后消费
  • DefaultMQPushConsumer :不需要主动调用拉取API,看起来想Broker主动往客户端推送消息。

pull和push本质都是客户端主动拉取消息:push是由客户端自动实现了循环的拉取消息,所以看起来像Broker主动推送消息给客户端。

消费服务类型

Push模式客户端会拉取多个消息缓存到本地,消费服务类型是指本地是对于这些拉取到的消息顺序消费还是并行消费

  • 顺序消费:保证消息的消费按照队列的先进先出的顺序进行消费 会用synchronize保证顺序消费
  • 并行消费:不保证有序消费,充分利用CPU

具体是顺序还是并行具体看设置的监听器类型:org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService和org.apache.rocketmq.client.impl.cons umer.ConsumeMessageOrderlyService

相关配置

consumer.setConsumeThreadMin(10);  // 最小10个线程
consumer.setConsumeThreadMax(32);  // 最大32个线程

消费者核心属性

  • namesrvAddr:RocketMQ 集群的 Namesrv 地址,多个,则用分号分开。
  • persistConsumerOffsetInterval:持久化消费位点时间间隔,单位为 ms,默认为5000ms。
  • consumerGroup:消费者组名字。
  • messageModel:消费模式,现在支持集群模式消费和广播模式消费。
  • messageQueueListener:消息路由信息变化时回调处理监听器,一般在重新平衡时会被调用。
  • offsetStore:位点存储模块。集群模式位点会持久化到Broker中,广播模式持久化到本地文件中。
  • maxReconsumeTimes:最大重试次数,可以配置。
  • pull() :从Broker中Pull消息
  • fetchSubscribeMessageQueues(final String topic):获取一个Topic的全部Queue信息。
  • consumeTimestamp:表示从哪一时刻开始消费,格式为 yyyyMMDDHHmmss,默认为半小时前。
  • consumeFromWhere:一个枚举,表示从什么位点开始消费,用枚举值。PushConsumer
  • subscription:订阅关系,表示当前消费者订阅了哪些Topic的哪些Tag。PushConsumer
  • messageListener:消息Push回调监听器。PushConsumer
  • consumeConcurrentlyMaxSpan:并发消息的最大位点差 (当前拉取的最大 offset - 当前提交的最小 offset)。如果 Pull消息的位点差超过该值,拉取变慢。PushConsumer
  • pullThresholdForQueue:一个 Queue 能缓存的最大消息数。超过该值则采取拉取流控措施。PushConsumer
  • pullThresholdSizeForQueue:一个Queue最大能缓存的消息字节数,单位是MB。PushConsumer

消费进度保存机制 :消息进度持久化

  • 集群消费:进度保存在broker
  • 广播消费:进度保存在客户端

主要分为定时持久化和不定时持久化两种。

定时持久化:提供有API接口进行设置

  • setPersistConsumerOffsetInterval

不定时持久化时机:

  • 拉取消息完成后,如果拉取位点非法,客户端会主动提交一次最新的位点。
  • 执行拉取动作:本地位点大于0,主动上传最新位点。
  • 关闭前上报位点

RocketMQ通过定时和不定时两种方式在一定程度上保证了位点上报的及时性,但是仍然会有消息消费了但是没有及时上报位点客户端就宕机了,这个时候会出现消息重复消费的情况。(RocketMQ不完全支持Exactly-Once的语义:每条消息无论在何种网络异常、系统故障或重试情况下,都只会被成功处理一次,既不会丢失,也不会重复。

  • 完全实现幂等的代价很大
  • 在业务层面实现消息幂等的代价较小

消费失败

  • 对于顺序消费来说:如果消费失败,消息会延迟一定时间后重新放入线程池内重新消费

消息过滤

  • tag过滤: 最常用,broker端过滤掉不符合要求的消息,然后返回给客户端 Broker端完成第一次过滤:通过tag的hashcode进行对比的,字符串对比速度较慢,hashcode则速度较快(位运算) 客户端完成第二次过滤:客户端是通过字符串比较完成过滤动作
  • SQL92过滤 Broker端通过布隆过滤器完成第一次过滤:同样也会有过滤失败的情况
  • Filter server过滤 需要一个专门的Filter server来四线过滤功能,消费者委托filter server去实现具体过滤操作。

布隆过滤器:用于检查是否在集合中,不在集合则一定不在集合中则一定不在,如果在集合中则有概率检查错误 底层是通过bitmap实现,通过hash算法将key的值hash到具体的位,如果hash后对应bit位为1,说明有可能有数据,如果为0则一定没有数据

Pull模式Demo

Maven依赖

        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.8.0</version>
        </dependency>
    public void test01() throws Exception{ 
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_01");
        consumer.setNamesrvAddr("192.168.169.223:9876");
        consumer.start();
​
        try {
            MessageQueue mq = new MessageQueue();
            mq.setQueueId(0);
            mq.setTopic("topic_test01");
            mq.setBrokerName("broker-a");
            long offset = consumer.fetchConsumeOffset(mq, false);
            if (offset < 0) {
                offset = 0; // 如果没有消费记录,从0开始
            }
            PullResult pullResult = consumer.pull(mq, "*", offset, 32);
            
            List<MessageExt> msgList = pullResult.getMsgFoundList();
​
            for (MessageExt msg : msgList) {
                // 将byte[]转换为字符串,使用UTF-8编码
                String body = new String(msg.getBody(), StandardCharsets.UTF_8);
                System.out.println(body);
            }
​
            // 更新消费进度
            consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
            // 在关闭消费者之前,可以强制同步到Broker
            consumer.getDefaultMQPullConsumerImpl().getOffsetStore().persist(mq);
        } catch (Exception e) {
            e.printStackTrace();
        }
        consumer.shutdown();
    }

Push模式Demo

    public void test02() throws Exception{
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_5");
        consumer.setNamesrvAddr("192.168.169.223:9876");
​
        consumer.subscribe("topic_test01", "*");
        consumer.subscribe("topic_test02", "*");
        consumer.subscribe("topic_test03", "*");
​
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    try {
                        // 处理消息
                        String topic = msg.getTopic();
                        String tags = msg.getTags();
                        String body = new String(msg.getBody());
                        System.out.printf("Received message - Topic: %s, Tags: %s, Body: %s%n",
                                topic, tags, body);
​
                    } catch (Exception e) {
                        // 处理异常,返回重试
                        System.err.println("Process message failed: " + e.getMessage());
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }
                }
                // 消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
​
        consumer.start();
        System.out.println("Consumer started successfully");
​
        while (true){
            Thread.sleep(10000);
        }
    }