RcoketMQ解决不是完全顺序消费问题

RocketMQ顺序消费的工作原理:

RocketMQ中顺序性主要指的是消息顺序消费。RocketMQ中每一个消费组一个单独的线程池并发消费拉取到的消息,即消费端是多线程消费。而顺序消费的并发度等于该消费者分配到的队列数。
RocketMQ的完成顺序性主要是由3把锁来实现的:如下图:
在这里插入图片描述

1、消费端在启动时首先会进行队列负载机制,遵循一个消费者可以分配多个队列,但一个队列只会被一个消费者消费的原则。
2、消费者根据分配的队列,向 Broker 申请琐,如果申请到琐,则拉取消息,否则放弃消息拉取,等到下一个队列负载周期(20s)再试。
3、拉取到消息后会在消费端的线程池中进行消费,但消费的时候,会对消费队列进行加锁,即同一个消费队列中的多条消息会串行执行
4、在消费的过程中,会对处理队列(ProccessQueue)进行加锁,保证处理中的消息消费完成,发生队列负载后,其他消费者才能继续消费。
前面2把琐比较好理解,最后一把琐有什么用呢?
例如队列 q3 目前是分配给消费者C2进行消费,已将拉取了32条消息在线程池中处理,然后对消费者进行了扩容,分配给C2的q3队列,被分配给C3了,由于C2已将处理了一部分,位点信息还没有提交,如果C3立马去消费q3队列中的消息,那存在一部分数据会被重复消费,故在C2消费者在消费q3队列的时候,消息没有消费完成,那负载队列就不能丢弃该队列,就不会在broker端释放琐,其他消费者就无法从该队列消费,尽最大可能保证了消息的重复消费,保证顺序性语义。
再补充一点:要保证顺序消费,其重试次数为Integer.Max_Value。

实际上,RocketMQ是支持顺序消费的。
但这个顺序,不是全局顺序,只是分区顺序。要全局顺序只能一个分区。
之所以出现场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不同的queue(分区)
如图:
在这里插入图片描述

而消费端消费的时候,是会分配到多个queue的,多个queue是同时拉去提交消费。
如图:
在这里插入图片描述

但是同一条queue里面,RocketMQ的确是能保证FIFO的。那么要做到顺序消息,应该怎么实现呢——把消息确保投递到同一条queue。
rocketmq消息生产端示例代码如下:
在这里插入图片描述
按照这个示例,把订单号取了做了一个取模运算再丢到selector中,selector保证同一个模的都会投递到同一个queue.
即:相同订单号的–>有相同的模–>有相同的qqueue。
最后就会类似这样。
在这里插入图片描述
这样同一批你需要做到顺序消费的肯定会投递到同一个queue,同一个queue肯定会投递到同一个消费实例,同一个消费实例肯定是顺序拉去并顺序提交线程池的,只要保证消费端顺序消费,则大功告成!
如何保证顺序消费? 如果是使用MessageListenerOrderly则自带此实现,如果是使用MessageListenerConcurrently,则需要把线程池改为单线程模式。
(这里假设触发了重排导致queue分配给了别人也没关系,由于queue的消息永远是FIFO,最多只是已经消费的消息重复而已,queue内顺序还是能保证)
但的确会有一些异常场景会导致乱序。如master宕机,导致写入队列的数量上出现变化。
如果还是沿用取模的seletor,就会一批订单号的消息前面散列到q0,后面的可能散到q1,这样就不能保证顺序了。除非选择牺牲failover特性,如master挂了无法发通下来那批消息。
从消费端,如果想保证这批消息是M1消费完成再消费M2的话,可以使用MessageListenerOrdery接口,但是这样的话会有以下问题:
1.遇到消息失败的消息,无法跳过,当前队列消费暂停
2. 目前版本的RocketMQ的MessageListenerOrderly是不能从slave消费消息的。

个人项目示例

MessageListenerOrdery和MessageListenerConcurrently代码

package com.soochowlife.config;

import com.alibaba.fastjson.JSON;
import com.soochowlife.common.TagsEnum;
import com.soochowlife.modules.entity.MQConsumeResult;
import com.soochowlife.modules.service.MQConsumeService;
import com.soochowlife.modules.service.MQMsgProcessor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

/**
 * 消费者消费消息监听并路由至对应消息处理
 */
@Component
@Slf4j
public class MQConsumeMsgListenerProcessor implements MessageListenerOrderly {
    @Autowired
    private Map<String, MQMsgProcessor> mqMsgProcessorServiceMap;

    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        if (CollectionUtils.isEmpty(msgs)) {
            log.info("接受到的消息为空,不处理,直接返回成功");
            return ConsumeOrderlyStatus.SUCCESS;
        }
        ConsumeOrderlyStatus consumeOrderlyStatus = ConsumeOrderlyStatus.SUCCESS;
        try {
            //根据Topic分组
            Map<String, List<MessageExt>> topicGroups = msgs.stream().collect(Collectors.groupingBy(MessageExt::getTopic));
            for (Entry<String, List<MessageExt>> topicEntry : topicGroups.entrySet()) {
                String topic = topicEntry.getKey();
                //根据tags分组
                Map<String, List<MessageExt>> tagGroups = topicEntry.getValue().stream().collect(Collectors.groupingBy(MessageExt::getTags));
                for (Entry<String, List<MessageExt>> tagEntry : tagGroups.entrySet()) {
                    String tag = tagEntry.getKey();
                    //消费某个主题下,tag的消息
                    this.consumeMsgForTag(topic, tag, tagEntry.getValue());
                }
            }
        } catch (Exception e) {
            log.error("处理消息失败", e);
            consumeOrderlyStatus = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }
        // 如果没有return success ,consumer会重新消费该消息,直到return success
        return consumeOrderlyStatus;
    }
    /**
     * 默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息<br/>
     * 不要抛异常,如果没有return CONSUME_SUCCESS ,consumer会重新消费该消息,直到return CONSUME_SUCCESS
     */
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        if (CollectionUtils.isEmpty(msgs)) {
            log.info("接受到的消息为空,不处理,直接返回成功");
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        ConsumeConcurrentlyStatus concurrentlyStatus = ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        try {
            //根据Topic分组
            Map<String, List<MessageExt>> topicGroups = msgs.stream().collect(Collectors.groupingBy(MessageExt::getTopic));
            for (Entry<String, List<MessageExt>> topicEntry : topicGroups.entrySet()) {
                String topic = topicEntry.getKey();
                //根据tags分组
                Map<String, List<MessageExt>> tagGroups = topicEntry.getValue().stream().collect(Collectors.groupingBy(MessageExt::getTags));
                for (Entry<String, List<MessageExt>> tagEntry : tagGroups.entrySet()) {
                    String tag = tagEntry.getKey();
                    //消费某个主题下,tag的消息
                    this.consumeMsgForTag(topic, tag, tagEntry.getValue());
                }
            }
        } catch (Exception e) {
            log.error("处理消息失败", e);
            concurrentlyStatus = ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        // 如果没有return success ,consumer会重新消费该消息,直到return success
        return concurrentlyStatus;
    }

    /**
     * 根据topic 和 tags路由,查找消费消息服务
     *
     * @param topic
     * @param tag
     * @param value
     */
    private void consumeMsgForTag(String topic, String tag, List<MessageExt> value) {
        //根据topic 和  tag查询具体的消费服务
        MQMsgProcessor imqMsgProcessor = selectConsumeService(topic, tag);
        try {
            if (imqMsgProcessor == null) {
                log.error(String.format("根据Topic:%s和Tag:%s 没有找到对应的处理消息的服务", topic, tag));
            } else {
                log.info(String.format("根据Topic:%s和Tag:%s 路由到的服务为:%s,开始调用处理消息", topic, tag, imqMsgProcessor.getClass().getName()));
                //调用该类的方法,处理消息
                MQConsumeResult mqConsumeResult = imqMsgProcessor.handle(topic, tag, value);
                if (mqConsumeResult != null && mqConsumeResult.isSuccess()) {
                    log.info("消息处理成功:" + JSON.toJSONString(mqConsumeResult));
                } else {
                    log.error("消费消息失败");
                }
            }
        } catch (Exception e) {
            log.error("消费消息失败");
        }
    }

    /**
     * 根据topic和tag查询对应的具体的消费服务
     *
     * @param topic
     * @param tag
     * @return
     */
    private MQMsgProcessor selectConsumeService(String topic, String tag) {
        MQMsgProcessor imqMsgProcessor = null;
        for (Entry<String, MQMsgProcessor> entry : mqMsgProcessorServiceMap.entrySet()) {
            //获取service实现类上注解的topic和tags
            MQConsumeService consumeService = entry.getValue().getClass().getAnnotation(MQConsumeService.class);
            if (consumeService == null) {
                log.error("消费者服务:" + entry.getValue().getClass().getName() + "上没有添加MQConsumeService注解");
                continue;
            }
            String annotationTopic = consumeService.topic().getCode();
            if (!annotationTopic.equals(topic)) {
                continue;
            }
            TagsEnum[] tagsArr = consumeService.tags();
            //"*"号表示订阅该主题下所有的tag
            if (tagsArr[0].equals("*")) {
                //获取该实例
                imqMsgProcessor = entry.getValue();
                break;
            }
            boolean isContains = false;
            for (TagsEnum tagsEnum : tagsArr){
                if (tagsEnum.getTags().equals(tag)){
                    isContains = true;
                }
            }
            if (isContains) {
                //获取该实例
                imqMsgProcessor = entry.getValue();
                break;
            }
        }
        return imqMsgProcessor;
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值