RocketMQ知识点汇总

消息队列功能

解耦

异步

流量削峰

消息分发

消息分发如下,各个子系统(消费者组)有各自的Offset(消费偏移量、消费进度),互不影响,这种方式比RPC要简单很多,因为消息只用发送一次即可,而RPC可能要发起多次调用,每个子系统一次。
在这里插入图片描述

不同类型的消费者

推模型DefaultMQPushConsumer

由系统控制读操作,收到消息后自动调用传入的MessageListener中的处理方法来处理,自动保存Offset,加入新的DefaultMQPush Consumer 后会自动做负载均衡
在这里插入图片描述

推模型实现原理

推模型是拉模型的包装,实际是通过“长轮询”方式达到Push 的效果。“长轮询”的核心是,Broker 端HOLD 住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给Consumer。“长轮询”的主动权还是掌握在Consumer 手中,Broker 即使有大量消息积压,也不会主动推送给Consumer 。所以当消息积压时,可以采用pull的方式自主控制拉取频率和时长。

推模型流量控制

PushConsumer 有个线程池,消息处理逻辑在各个线程里同时执行
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
Pull 获得的消息,如果直接提交到线程池里执行,很难监控和控制,比如,如何得知当前消息堆积的数量?如何重复处理某些消息?如何延迟处理某些消息?RocketMQ 定义了一个快照类ProcessQueue 来解决这些问题,在PushConsumer 运行的时候,每个MessageQueue 都会有个对应的ProcessQueue(队列消费的快照)对象,保存了这个MessageQueue 消息处理状态的快照。ProcessQueue 对象里主要的内容是一个TreeMap 和一个读写锁。TreeMap里以MessageQueue 的Offset 作为Key ,以消息内容的引用为Value ,保存了所有从MessageQueue 获取到,但是还未被处理的消息;读写锁控制着多个线程对TreeMap 对象的并发访问。有了P rocess Queue 对象,流量控制就方便和灵活多了,客户端在每次Pull请求前会做下面三个判断来控制流量,代码如下
在这里插入图片描述
PushConsumer 会判断获取但还未处理的消息个数、消息总大小、Offset 的跨度,任何一个值超过设定的大小就隔一段时间再拉取消
息,从而达到流量控制的目的。

拉模型DefaultMQPullConsumer

拉模型的操作方式

读取操作中的大部分功能由使用者自主控制,例如:

  • 根据topic获取Message Queue 并遍历
  • 存储每个队列上的消费偏移量,下面代码用map保存
  • 根据不同的消息状态做不同的处理
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("please_rename_unique_group_name_5");

        consumer.start();

        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
        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);
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    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 = OFFSE_TABLE.get(mq);
        if (offset != null)
            return offset;

        return 0;
    }

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

}

不同类型的生产者

DefaultMQProducer

生产者发送消息默认使用的是DefaultMQProducer 类,内部持有一个DefaultMQProducerImp,发送消息实际均通过这个类发送。
在这里插入图片描述

同步发送

超时时间默认3秒
在这里插入图片描述

异步发送

传入回调函数
在这里插入图片描述

单向发送

类似于UDP,不等待broker的确认,方法便直接返回。显然,方法可以提高吞吐量,但可能会消息丢失。
在这里插入图片描述

延迟消息

RocketMQ 支持发送延迟消息,Broker 收到这类消息后,延迟一段时间再处理。使用方法是在创建Message 对象时,调用setDelayTimeLevel(intlevel)方法设置延迟时间,然后再把这个消息发送出去。目前延迟的时间不支持任意设置,仅支持预设值的时间长度(1 s/5s/10s/30s/I m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1 h /2h )。比如setDelayTimeLevel(3)表示延迟10s .

一个延时消息被发出到消费成功经历以下几个过程:

  1. 设置消息的延时级别delayLevel
  2. producer发送消息
  3. broker收到消息在准备将消息写入存储的时候,判断是延时消息则更改Message的topic为延时消息队列的topic(SCHEDULE_TOPIC_XXXX),也就是将消息投递到延时消息队列
  4. 每一个延时等级一个queue, 每个延时队列启动一个定时任务来处理该队列的延时消息,定时任务从延时队列中读取消息,拿到消息后判断是否达到延时时间,如果到了则修改topic为原始topic。并将消息投递到原始topic的队列
  5. consumer像消费其他消息一样从broker拉取消息进行消费

参考:https://blog.csdn.net/gesanghuakaisunshine/article/details/80261628

分布式消息队列的协调者

NameServer 的功能

NameServer 是整个消息队列中的状态服务器,集群的各个组件通过它来了解全局的信息。同时,各个角色的机器都要定期向NameServer 上报自己的状态,超时不上报的话,NameServer 会认为某个机器出故障不可用了,其他的组件会把这个机器从可用列表里移除。

集群状态的存储结构

在org.apache.rocketmq.namesrv.routeinfo 的RoutelnfoManager 类中,有五个变量,集群的状态就保存在这五个变量中。
在这里插入图片描述
QueueData里保存了broker的名称、读写队列数量等。List的长度等于这个Topic的Master Broker的数量

private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;

相同名称的broker有多台机器,一个master,多个slave。
brokerData里存储着master和slave的地址信息、集群名称等

private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;

一个集群包含多少个master broker

private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;

BrokerLiveTable存储的内容是这台Broker 机器的实时状态,包括上次更新状态的时间戳,Broker向NameServr发送的心跳检测会更新时间戳。NameServer 会定期检查这个时间戳,超时没有更新就认为这个Broker 无效了,将其从Broker 列表里清除。

private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;

为何不用ZooKeeper

  • ZooKeeper 的功能很强大,包括自动Master 选举等,而RocketMQ不需要进行Master选举,用不到这些复杂的功能,所以只需要一个轻量级的元数据服务器就足够了。
  • 中间件对稳定性要求很高,NameServr代码量少,容易维护,所以不需要再依赖另一个中间件。

消息存储结构

RocketMQ 消息的存储是由ConsumeQueue 和CommitLog 配合完成的,消息真正的物理存储文件是CommitLog, ConsumeQueue 是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。
在这里插入图片描述

顺序写

commtLog的存储其实是分多层的,commitLog -> mappedFileQueue -> mappedFile,其中真正存储数据的是mappedFile。

commitLog在mappedFile当中存储消息的格式是【msg + msg + msg + …+msg+blank】。也就是当最后的位置放不下消息的时候就填充空白。
在这里插入图片描述
commitLog数据存储过程
commitLog内部的数据结构,核心的在于MappedFileQueue这个对象,以及每个MappedFile的大小(1G=1024 * 1024 * 1024)。

commitLog保存消息的过程如下

1、在mappedFileQueue里面选择最近的mappedFile文件,如果没有mappedFile文件或者mappedFile数据已经满的情况下就新建一个mappedFile文件。

2、选择mappedFile文件之后,开始计算消息体大小并保存至mappedFile文件当中,在整个保存过程中先用临时的byteBuffer(msgStoreItemMemory)保存,如果mappedFile文件能够保存下最新的消息体就保存消息至mappedFile文件,否则就保存一个结束符。

mappedFile的文件生成其实有一定的规则,首先mappedFile文件的命名是以1024 * 1024*1024=1073741824进行递增,也就是说第一个文件名字为000000001073741824,第二个名字是以00000000002147483648进行命令,以次递增。体现在代码中就是以上一个文件的便宜量加上1073741824即可。

详见:https://www.jianshu.com/p/2c904cc42d95

CommitLog: CommitLog是存储消息内容的存储主体,Producer发送的消息都会顺序写入CommitLog文件。由于需要存储的消息随着时间推移会变得很大,因此CommitLog将日志做了拆分,每个CommitLog文件大小为1G,文件名(长度20位,左边补0)为该文件中的消息起始偏移量,比如第一个CommitLog起始偏移量为0,其文件名为(00000000000000000000),1G=1073741824,故第二个文件的起始偏移量为1073741824,文件名为00000000001073741824。commitLog文件存储路径为$HOME/store/commitLog

ConsumeQueue:ConsumeQueue(逻辑消费队列)是消息消费队列,由于CommitLog中为了消息的存储性能考虑,所有消息都是顺序写入的(即不同Topic的消息混淆存储),但Consumer消费端又是根据Topic来订阅消费消息,如果要根据Topic来订阅消息,势必遍历CommitLog中存储的消息来过滤Topic,这种方式的性能是非常差的。因此MQ中设计了ConsumeQueue来提高消息消费性能,consumequeue文件可以看成是基于topic的commitlog索引文件。即每个Topic下的每个queueId对应一个Consumequeue,其中存储了消息对应在CommitLog文件中的物理偏移量offset,消息大小size,消息Tag的hash值
ConsumeQueue文件的存储路径为$HOME/store/consumequeue,其下文件夹组织方式为topic/queueId/consumequeue文件

IndexFile:顾名思义,IndexFile是索引文件,其提供了根据消息的key值,时间区间来快速检索消息的方法。底层是HashMap的文件索引实现。IndexFile的名称是创建时间的时间戳,存储路径为$HOME/store/index

单个Broker实例中,所有的队列共享一个CommitLog文件,即所有消息顺序写入CommitLog文件

对于这样的一个大型文件,要随机读,如何提高读写效率呢?
答案就是“内存映射文件”(内存映射过的文件)。

MappedFile:先明确一点,什么是MappedFile,前面介绍CommitLog文件存储时介绍到,实际文件系统中CommitLog是分成了1个个定长的子文件的。MappedFile就可以理解为这些定长子文件在内存中的映射。MappedFile内部持有NIO中的MappedByteBuffer对文件进行读写操作,将对文件的操作转化为对内存地址的操作,提高了文件读写效率(因为需要使用内存映射机制,因此CommitLog文件采用定长结构存储,方便将文件映射到内存)

详见:https://blog.csdn.net/hosaos/article/details/102523345

mmap(内存映射文件):
在一般的文件读写中,会有两次数据拷贝,一次是从硬盘拷贝到操作系统内核,另一次是从操作系统内核拷贝到用户态的应用程序。而在内存映射文件中,一般情况下,只有一次拷贝,且内存分配在操作系统内核,应用程序访问的就是操作系统的内核内存空间,这显然要比普通的读写效率更高。

内存映射文件的另一个重要特点是,它可以被多个不同的应用程序共享,多个程序可以映射同一个文件,映射到同一块内存区域,一个程序对内存的修改,可以让其他程序也看到,这使得它特别适合用于不同应用程序之间的通信。
在这里插入图片描述

在这里插入图片描述

随机读

利用操作系统的pagecache 机制,可以批量地从磁盘读取,作为cache存到内存中,加速后续的读取速度。

零拷贝

说起来比较麻烦,简而言之,内存分为虚拟内存和物理内存,每个进程都有自己的虚拟地址空间(32位,0-4G),程序里操作的都是虚拟内存,虚拟内存映射着真实的物理内存。

再者,操作系统将内存划分为用户态和内核态两块,用户态和内核态的虚拟地址空间分别映射着不同的物理地址空间,一次系统调用,比如读,需要将磁盘页数据拷贝到内核态,再由内核态拷贝到用户态。

零拷贝就是说,用户态和内核态映射同一块物理地址空间,磁盘页数据加载到物理地址空间后,用户态就能直接使用了,不需要经由内核态到用户态的拷贝。

高可用

RocketMQ 分布式集群是通过Master 和Slave 的配合达到高可用性的。也就是消费端的主从机制+发送端的多master集群,消除单点故障。

Master 角色的Broker 支持读和写,Slave 角色的Broker 仅支持读,也就是Producer 只能和Master 角色的Broker 连接写人消息;Consumer 可以连接Master 角色的Broker ,也可以连接Slave 角色的Broker 来读取消息。

消费端的高可用

主从机制:
当Master 不可用或者繁忙的时候,Consumer 会被自动切换到从Slave 读。

发送端的高可用

多master集群:
在创建Topic 的时候,把Topic 的多个Message Queue 创建在多个Broker 组上(相同Broker 名称,不同brokerId 的机器组成一个Broker 组),这样当一个Broker 组的Master 不可用后,其他组的Master 仍然可用,Producer 仍然可以发送消息。

Topic分片。在分布式数据库和分布式缓存领域,分片概念已经有了清晰的定义
在这里插入图片描述
将Topic分片再切分为若干等分,其中的一份就是一个Queue。每个Topic分片等分的Queue的数量可以不同,由用户在创建Topic时指定。

我们知道,数据分片的主要目的是突破单点的资源(网络带宽,CPU,内存或文件存储)限制从而实现水平扩展。RocketMQ 在进行Topic分片以后,已经达到水平扩展的目的了,为什么还需要进一步切分为Queue呢?

Queue是负载均衡过程中资源分配的基本单元,在一个Consumer Group内,Queue和Consumer之间的对应关系是一对多的关系:一个Queue最多只能分配给一个Consumer,一个Cosumer可以分配得到多个Queue。
在这里插入图片描述
如图所示,TOPIC_A在一个Broker上的Topic分片有5个Queue,一个Consumer Group内有2个Consumer按照集群消费的方式消费消息,按照平均分配策略进行负载均衡得到的结果是:第一个 Consumer 消费3个Queue,第二个Consumer 消费2个Queue。如果增加Consumer,每个Consumer分配到的Queue会相应减少。Rocket MQ的负载均衡策略规定:Consumer数量应该小于等于Queue数量,如果Consumer超过Queue数量,那么多余的Consumer 将不能消费消息。

详见:https://blog.csdn.net/binzhaomobile/article/details/73332463

刷盘方式

异步刷盘

消息写入内核态页缓存(PAGECACHE) ,当页缓存消息量积累到一定程度,由操作系统统一刷盘。优点:写操作立即返回,吞吐量大;

同步刷盘

消息写入页缓存后,立即通知刷盘线程刷盘,刷盘完成再返回。

配置方式

刷盘方式通过Broker 配置文件里的flushDiskType 参数
设置,这个参数被配置成SYNC_FLUSH 、ASYNC_FLUSH 中的一个。

主从复制方式

如果一个Broker 组有Master 和Slave, 消息需要从Master 复制到Slave
上,有同步和异步两种复制方式。

同步复制

等Master 和Slave 均写成功后才反馈给客户端写成功状态

异步复制

只要Master 写成功即可反馈给客户端写成功状态。

配置方式

通过Broker 配置文件里的brokerRole 参数进行设置的,这个参数可以被设置成ASYNC_MASTER 、SYNC_MASTER 、SLAVE 三个值中的一个。

刷盘、主从复制方式小结

通常情况下,主从都要配置成异步刷盘,主从之间配置成同步复制。能提高吞吐量,且保证数据不丢。

顺序消息

顺序消息是指消息的消费顺序和产生顺序相同,在有些业务逻辑下,必须保证顺序。比如订单的生成、付款、发货,这3 个消息必须按顺序处理才行。顺序消息分为全局顺序消息和部分顺序消息,全局顺序消息指某个Topic 下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可,比如上面订单消息的例子,只要保证同一个订单ID 的三个消息能按顺序消费即可。

全局顺序消息

消除所有的并发处理,一个topic只能有一个broker组,且读写队列设为1,Producer 和Consumer 的并发设置也要是1,Producer、MessageQueue、Consumer为一对一对一的关系

部分顺序消息

要保证部分消息有序,需要发送端和消费端配合处理。

发送端

发送端在发送消息时使用MessageQueueSelector,把同一业务ID 的消息发送到同一个Message Queue

SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {`在这里插入代码片`
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, orderId);

消费端

消费端注册消息监听器时,使用MessageListenerOrderly 类

consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(false);
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 3) == 0) {
                    return ConsumeOrderlyStatus.ROLLBACK;
                } else if ((this.consumeTimes.get() % 4) == 0) {
                    return ConsumeOrderlyStatus.COMMIT;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

消息重复

解决消息重复有两种方法

  1. 保证消费逻辑的幕等性(多次调用和一次调用效果相同),可以用redis实现的分布式锁来实现
  2. 维护一个已消费消息的记录,消费前查询这个消息是否被消费过。

这两种方法都需要使用者自己实现。比如,消息来时,先用业务上的唯一标识,查询数据库表中是否存在这条消息,若存在则不消费消息直接返回,若不存在则将业务上的唯一标识插入redis中,作为分布式锁的key,同时设置过期时间,业务逻辑处理完后,从redis中删掉这个key,释放锁。(这里redis分布式锁的很多细节没有说到)

消息优先级

创建多个Topic

情况一:
如果当前Topic 里有多种相似类型的消息,比如类型AA 、AB 、AC ,当AB 、AC 的消息量很大,但是处理速度比较慢的时候,队列里会有很多AB 、AC 类型的消息在等候处理,这个时候如果有少量AA 类型的消息加人,就会排在AB 、AC 类型消息后面,需要等候很长时间才能被处理。

解决:
如果业务需要AA 类型的消息被及时处理,可以把这三种相似类型的消息分拆到两个Topic 里,比如AA 类型的消息在一个单独的Topic, AB 、AC 类型的消息在另外一个Topic 。把消息分到两个Topic 中以后,应用程序创建两个Consumer ,分别订阅不同的Topic ,这样消息AA 在单独的Topic 里,不会因为AB 、AC 类型的消息太多而被长时间延时处理。

创建多个MessageQueue

情况二:
第二种情况和第一种情况类似,但是不用创建大量的Topic 。举个实际应用场景:一个订单处理系统,接收从100 家快递门店过来的请求,把这些请求通过Producer 写人RocketMQ ;订单处理程序通过Consumer 从队列里读取消息并处理,每天最多处理1 万单。如果这100 个快递门店中某几个门店订单量大增,比如门店一接了个大客户,一个上午就发出2 万单消息请求,这样其他的99 家门店可能被迫等待门店一的2 万单处理完,也就是两天后订单才能被处理,显然很不公平。

解决:
这时可以创建一个Topic ,设置Topic 的MessageQueue 数量超过100 个,Producer 根据订单的门店号,把每个门店的订单写人一个MessageQueue 。DefaultMQPushConsumer 默认是采用循环的方式逐个读取一个Topic 的所有MessageQueue ,这样如果某家门店订单量大增,这家门店对应的MessageQueue 消息数增多,等待时间增长,但不会造成其他家门店等待时间增长。
另外为了公平,可以将DefaultMQPushConsumer的pullBatchSize参数设为1,参数默认值是32,也就是每次从某个MessageQueue 读取消息的时候,最多可以读32个。

吞吐量优先

在Broker端进行消息过滤

通过Tag进行过滤

发送消息设置了Tag 以后,消费方在订阅消息时,才可以利用Tag 在Broker 端做消息过滤。

Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
consumer.subscribe("TopicTest", "TagA || TagC || TagD");

对一个应用来说,尽可能只用一个Topic ,不同的消息子类型用Tag 来标识

通过SQL表达式进行过滤

通过FilterServer进行过滤

提高Consumer处理能力

增加Consumer实例数量,增加消费线程数量

  • 加机器,或者在已有机器中启动多个Consumer进程都可以增加Consumer实例数量
  • 修改consumeThreadMin 和consumeThreadMax 提高单个 Consumer 实例并行处理的线程数

以批量方式消费

一次update10条的时间会大大小于10次update 1条数据的时间,可以设置Consumer 的consumeMessageBatchMaxSize 这个参数,默认是1 ,如果设置为N,每次收到长度为N 的消息链表。

跳过非重要消息

Consumer 在消费的过程中,如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer 尽快追上Producer 的进度

consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                long Offset = msgs.get(0).getQueueOffset();
                String maxOffset = msgs.get(0).getProperty(MessageConst.PROPERTY_MAX_OFFSET);
                long diff = Long.parseLong(maxOffset) - Offset;
                if (diff > 90000) {
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                //正常消费消息
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }

        });

如代码所示,当某个队列的消息数堆积到90000 条以上,就直接丢弃,以便快速追上发送消息的进度。

提高Producer发送速度

通过OneWay方式发送

在一些对速度要求高,但是可靠性要求不高的场景下,比如日志收集类应用,可以采用Oneway 方式发送。

多个Producer同时发送

增加Producer 的并发量,使用多个Producer同时发送

消息发送超时

  • send方法默认超时时间为3秒
  • 若3秒内发送失败,则:
    • 重试2次(可通过参数retryTimesWhenSendFailed调整,默认值2)
    • 重试另一个broker(可通过参数retryAnotherBrokerWhenNotStoreOK调整,默认false不重试)
  • 若超过3秒,则直接抛出异常

所以最好先把消息存储到db,后台启线程定时重试,确保消息一定存储到broker。

消费失败

并发消费失败

在Consumer使用的时候需要注册MessageListener,对于PushConsumer来说需要注册MessageListenerConcurrently,其中消费消息的接口会返回处理状态,分别是,

  • ConsumeConcurrentlyStatus.CONSUME_SUCCESS,消费成功
  • ConsumeConcurrentlyStatus.RECONSUME_LATER,延时消费,首次延时10s

如果一条消息在消费端处理没有返回这2个状态,那么相当于这条消息没有达到消费者,势必会再次发送给消费者!也即是消息的处理必须有返回值,否则就进行重发。

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;
            }
        });

返回ConsumeConcurrentlyStatus.RECONSUME_LATER状态之后的处理策略是将该组消息发送回Broker,等待后续消息。发送回的消息会设置重试Topic,重试Topic命名为:"%RETRY%" + ConsumerGroupName。原先实际的Topic会暂存到消息属性当中,以及设置delayLevel和reconsumeTimes。
重试消息的重新投递逻辑与延迟消息一致,等待delayLevel对应的延时一到,Broker会尝试重新进行投递处理。如下图:
在这里插入图片描述
1、消息重试和延迟消息的处理流程是一样的都需要创建一个延迟消息的主题队列(SCHEDULE_TOPIC_XXXX)。后台启动定时任务定时扫描需要的发送的消息将其发送到原有的主题和消息队列中供消费,只是其重试消息的主题是%RETRY_TOPIC%+ consumerGroup并且其队列只有一个queue0,延迟消息和普通消息一样发送到原主题的原队列中。
2、和普通消息不一样的是,consumer拉取消息的主题不是原本订阅的topic,而是%RETRY%+ConsumerGroupName。consumer发送重试消息给broker以后,当延时时间到,消息被转移至%RETRY_TOPIC%+ consumerGroup下,consume会拉取这个新的topic的消息。consumer拉取到这个retryTopic的消息之后再把topic换成原来的topic:org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService#resetRetryTopic,然后交给consume的listener处理。
3、根据业务的需要,定义消费的最大重试次数,每次消费的时候判断当前消费次数是否等于最大重试次数的阈值。如:重试3次就认为当前业务存在异常,继续重试下去也没有意义了,那么我们就可以将当前的这条消息进行提交,返回broker状态ConsumeConcurrentlyStatus.CONSUME_SUCCES,让消息不再重发将消息存入消费失败的数据库表,读取数据库表,展示在主页上。

consumer.registerMessageListener(new MessageListenerConcurrently() {
			@Override
			public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
				try {
					MessageExt msg = msgs.get(0);
					String msgbody = new String(msg.getBody(), "utf-8");
					System.out.println(msgbody + " Receive New Messages: " + msgs);
					if (msgbody.equals("HelloWorld - RocketMQ4")) {
						System.out.println("======错误=======");
						int a = 1 / 0;
					}
				} catch (Exception e) {
					e.printStackTrace();
					if (msgs.get(0).getReconsumeTimes() == 3) {
						// 该条消息可以存储到DB或者LOG日志中,或其他处理方式
						return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 成功
					} else {
						return ConsumeConcurrentlyStatus.RECONSUME_LATER;// 重试
					}
				}
				return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
			}
		});

顺序消费失败

在消息失败处理上,顺序消息与非顺序消息是有明显差异的。对于顺序消息来说,如果消费失败后将其延迟消费,那么顺序性实际就被破坏掉了。

所以顺序消息消费失败的话,消息消费不会再推进,直到失败的消息消费成功为止。

死信队列

当重试次数超过所有延迟级别之后。消息会进入死信,死信Topic的命名为:%DLQ% + Consumer组名。
可以通过接口去查询当前RocketMQ中私信队列的消息,如有必要,可以将消息从死信队列中移出并重新投递。

与Kafka差异

去掉了对ZK的依赖

在早期的RocketMQ版本中,是有依赖ZK的。而现在的版本中,是去掉了对ZK的依赖,转而使用自己开发的NameSrv。

在Kafka里面,Maser/Slave是选举出来的!!!RocketMQ不需要选举!!!
而在RocketMQ中,不需要选举,Master/Slave的角色也是固定的。当一个Master挂了之后,你可以写到其他Master上,但不会说一个Slave切换成Master。

存储上的差异

Kafka的存储结构: 其中每个topic_partition对应一个日志文件,Producer对该日志文件进行“顺序写”,Consumer对该文件进行“顺序读”。

这种存储方式,对于每个文件来说是顺序IO,但是当并发的读写多个partition的时候,对应多个文件的顺序IO,表现在文件系统的磁盘层面,还是随机IO。因此出现了当partition或者topic个数过多时,Kafka的性能急剧下降。
在这里插入图片描述
RocketMQ的存储
为了解决上述问题,RocketMQ采用了单一的日志文件,即把同1台机器上面所有topic的所有queue的消息,存放在一个文件里面,从而避免了随机的磁盘写入。其存储结构如下:
在这里插入图片描述
在这里插入图片描述

Kafka针对Producer和Consumer使用了同1份存储结构,而RocketMQ却为Producer和Consumer分别设计了不同的存储结构,Producer对应CommitLog, Consumer对应ConsumeQueue。

对于CommitLog,Producer对其“顺序写”,Consumer却是对其“随机读”。

对于这样的一个大型文件,又要随机读,如何提高读写效率呢?
答案就是“内存映射文件”。

读写时,根据offset定位到CommitLog链表中的MappedFile,进行读写。
通过MappedFile,就很好的解决了大文件随机读的性能问题。

参考:https://kuaibao.qq.com/s/20180830A12PTQ00?refer=cp_1026
https://www.iteye.com/blog/m635674608-2396042

发布了20 篇原创文章 · 获赞 0 · 访问量 1165

猜你喜欢

转载自blog.csdn.net/fengyq17290/article/details/104082784