消息中间件面试题之RocketMQ

为什么使用消息队列?

解耦、异步、削峰

消息队列有什么优点和缺点?

优点:解耦、异步、削峰

缺点:系统的可用性降低、系统的复杂性提高了、一致性问题。

RabbitMQ上的一个queue中存放的message是否有数量限制?限制是多少

默认情况下一般是无限制,因为限制取决于机器的内存,但是消息过多会导致处理效率的下降。

可以通过参数来限制, x-max-length :对队列中消息的条数进行限制 , x-max-length-bytes :对队列中消息的总量进行限制

如何解决重复消费?

所有的MQ 是无法保证消息不被重复消费的,只能业务系统层面考虑。利用redis进行业务方面的去重。

Rocketmq如何保证高可用性?

1、架构层面

避免用单节点或者简单的一主一从架构,可以采取多主多从的架构,并且主从之间采用同步复制的方式进行数据双写。

2、刷盘策略

RocketMQ默认的异步刷盘,可以改成同步刷盘SYNC_FLUSH。

3、生产消息的高可用

当消息发送失败了,在消息重试的时候,会尽量规避上一次发送的 Broker,选择还没推送过该消息的Broker,以增大消息发送的成功率。

4、消费消息的高可用

消费者获取到消息之后,可以等到整个业务处理完成,再进行CONSUME_SUCCESS状态确认,如果业务处理过程中发生了异常那么就会触发broker的重试机制。

image

RocketMq的存储机制了解吗(这个下来再好好看看)?

消息生产者发送消息到broker,都是会按照顺序存储在CommitLog文件中,每个commitLog文件的大小为1G。

image

image

CommitLog-存储所有的消息元数据,包括Topic、QueueId以及message。文件

CosumerQueue-消费逻辑队列:存储消息在CommitLog的offset。文件

IndexFile-索引文件:存储消息的key和时间戳等信息,使得RocketMq可以采用key和时间区间来查询消息 。

也就是说,rocketMq将消息均存储在CommitLog中,并分别提供了CosumerQueue和IndexFile两个索引,来快速检索消息。

RocketMq性能比较高的原因?

1.顺序写

顺序写比随机写的性能会高很多,不会有大量寻址的过程

2.异步刷盘

相比较于同步刷盘,异步刷盘的性能会高很多

3.零拷贝

使用mmap的方式进行零拷贝,提高了数据传输的效率

有几百万消息持续积压几小时,说说怎么解决?

1.一般情况下,是消费者的问题,赶紧修复消费者。

2.如果你将消费者修复好了之后,但是mq里面积压的消息量比较大,消费完了可能需要很长的世间,在业务上不能接受,这个时候,

我们可以将原来的消费者停掉。然后征用10倍的机器来部署producer,将原来生产者里面积压的消息快速消费掉。

然后将原来的生产者也停掉。

再征用10倍的机器部署consumer,然后再快速消费倒腾过来的积压数据。

等快速消费完积压的数据之后,将征用过来的机器都释放掉,最后再恢复原来的生产者和消费者就可以了。

Rocketmq中Broker的部署方式

1.单Master 部署;

2.多Master部署

3.多Master多Slave部署

Rocketmq中Broker的刷盘策略有哪些?

同步刷盘

异步刷盘

什么是路由注册?RocketMQ如何进行路由注册与发现?

RocketMQ的路由注册是通过broker向NameServer发送心跳包实现的,首先borker每隔30s向nameserver发送心跳语句,nameserver处理。

RocketMQ的路由发现不是实时的,NameServer不会主动向客户端推送,而是客户端定时拉取主题最新的路由,然后更新。

step1:调用RouterInfoManager的方法,从路由表topicQueueTable、brokerAddrTable、filterServerTable分别填充信息;

step2:如果主题对应的消息为顺序消息,则从NameServerKVconfig中获取关于顺序消息相关的配置填充路由信息;

什么是路由剔除?RocketMQ如何进行路由剔除?

路由删除有两个触发节点:

1)NameServer定时扫描brokerLiveTable检测上次心跳包与当前系统时间的时间差,如果大于120S,就需要删除;

2)Broker在正常关闭使,会执行unregisterBroker命令。

两种方法删除的逻辑都是一致的。

step1:申请写锁

step2:从brokerLiveTable、filterServerTable移除,从brokerAddrTable、clusterAddrTable、topicQueueTable移除

step3:释放锁

image

使用RocketMQ过程中遇到过什么问题?

1、消息挤压问题

2、消息丢失问题

3、消息重复消费问题

4、RocketMQ内存不够OOM问题

RocketMQ的总体架构,以及每个组件的功能?

image

讲一讲RocketMQ中的分布式事务及实现

image

总结:使用的是半消息+事务回查机制来做的。

讲一讲RocketMQ中事务回查机制的实现

TODO: 太难了,先放过。

你们生产环境的rocketmq集群是怎么搭建的

就说是三主三从的架构。

面试官可能会问你为什么从从节点消费数据呀,因为主节点不是每次来一条数据就同步给从节点,而是够了一个批次之后,一起提交给从节点,这种效率比较高效。

nameserver节点之间是不会相互进行通信的,每个nameserver都维护了全量的borker信息。

https://www.bilibili.com/video/BV1p44y1Y7AR/?spm_id_from=333.337.search-card.all.click&vd_source=273847a809b909b44923e3af1a7ef0b1

image

如果borker-a主节点挂了之后,borker-slave-a节点是不会被提升为主节点的,它一辈子就是从节点。如果主从同步的方式你指定的是同步复制,那么基本省不会丢失数据,但是如果你指定的是异步复制的方式,那么会丢数据的。

image

rocketmq中有重试机制,他会看borker-a发送不成功了,会将消息发送到borker-b中。

image

image

nameserver1对应的ip是192.168.31.103

nameserver2对应的ip是192.168.31.104

rocketmq的架构是怎么样的,组件有哪些?

image

RocketMQ 和 kafka 之间有什么区别

适用场景:

rocketmq适合做业务的处理。

kafka适合做日志的处理。

性能:

rocketmq:tps 10w

kafka:tps 100w

结论:kafka性能更高

可靠性:

rocketmq:支持同步刷盘、异步刷盘、同步复制,异步复制

kafka:支持异步刷盘,异步复制

结论:rocketmq的可靠性较好

顺序性:

RocketMQ:支持严格的顺序消息,在顺序消费的场景下,一台broker宕机后,发送消息失败,但不会轮序·

Kakfa:kafka在某些配置下,支持顺序消息,但在一台broker宕机后,消息会乱序
结论:RocketMQ的顺序性较好

消费失败重试

rocketmq:支持失败重试,支持重试间隔时间顺延。

kafka:不支持

结论:rocketmq胜出

延时消息/定时消息

rocketmq:支持

kafka:不支持

结论:rocketmq胜出

分布式事务:

rocketmq:支持

kafka:不支持

结论:rocketmq胜出

消息回溯
RocketMQ:支持某个时间戳(毫秒级)来回溯消息
Kakfa:支持某个偏移量offset来回溯消息
结论:各有千秋,不分伯仲

消息查询机制
RocketMQ:支持messageid和消息内容查询消息
Kakfa:不支持
结论:RocketMQ胜出

rocketmq几个核心知识点(面试有被问到)

rocketmq发送消息支持同步发送和异步发送。

rocketmq中是不会自动进行主从切换的。说白了就是你只要是从节点,你这辈子也就是从节点,不可能提升为主节点。

rocketmq消息发送失败处理:

最多重试两次,这是rocketmq内部实现的

rocketmqmq主节点和从节点的数据同步方式可以采用同步复制或者是异步复制。

rocketmq是通过注册消息监听器的方式来消费消息的,消息监听器大概有这么几种

image

从这张图中我们能够看到有支持并发消费消息的监听器,有顺序消费消息的监听器。

rocketmq的顺序消息(面试有被问到)

顺序消息指生产者局部有序发送到一个queue,但多个queue之间四全局无序的。

image

生产者在发送消息的时候我们可以指定MessageQueueSelector的接口,然后在这个接口里面实现我们自定义的将消息发送到哪个messagequeue里面。

public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
    throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    msg.setTopic(withNamespace(msg.getTopic()));
    return this.defaultMQProducerImpl.send(msg, selector, arg);
}

然后我们在消费消息的时候,一般使用的是注册一个监听器的方式进行消息的消费,这个时候我们使用MessageListenerOrderly这个顺序消费消息的监听器。

image

rocketmq怎样保证消息不丢失(面试有被问到)

消息发送到生产者是采用同步发送还是再用异步发送,如果采用的是同步发送的方式,这个时候会给我们返回一个结果,我们需要判断这个结果是否成功,如果失败就重新发送。如果采用的是异步发送,这个时候会在callback接口中提供两个方法,一个是onSuccess方法,会传给我们处理结果,这个时候我们可以根据这个结果进行判断,是否需要重新发送。还有一个方法是onException方法,也可以在这个方法里面进行消息的重新发送。说白了就是不管是同步发送还是异步发送,我们都可以根据返回的结果做相应的补偿机制。

mq采用同步刷盘还是异步刷盘,异步刷盘是消息写到缓存中就立刻返回了,为了保证消息不丢失,这个时候我们可以使用同步刷盘的方式。

rocketmq如果是集群模式部署的话,这个时候主从复制我们可以采用同步复制,不要采用异步复制。

消费者在拿到消息之后,处理完成消息,会自动提交ack应答,比方说我们在处理消息的时候出现了异常,这个时候还想重复消费这条消息,我们就需要关闭自动应答,改为手动应答的方式。

还有就是消息在发送之前,可能因为网络抖动的原因,压根就没有到达mq,这个时候我们可以在消息发送之前设置一张消息日志表,将消息先存到这张表里面(采用顺序写的这种方式),然后再进行发送。

ROCKETMQ 消息队列中 集群模式和广播模式 有什么区别(面试有被问到)

RocketMQ 中的集群模式(Clustering)广播模式(Broadcasting) 是消息消费的两种核心模式。

集群模式(默认模式)
同一条消息只会被消费组内的一个消费者实例消费

  • 原理:RocketMQ 会将消息队列(Message Queue)平均分配给消费组内的消费者,每个队列只被一个消费者消费,因此消息只会被投递到负责该队列的消费者。
  • 示例:消费组有 3 个实例,主题有 6 个队列,则每个实例负责 2 个队列,同一条消息仅在其所属队列对应的实例上被消费。

广播模式
同一条消息会被消费组内的所有消费者实例消费。

  • 原理:消息会被复制到消费组内所有消费者的本地队列,每个消费者都会收到并处理同一条消息。
  • 示例:消费组有 3 个实例,同一条消息会被 3 个实例分别消费一次。
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class ClusterBroadcastDemo {
    // 名称服务器地址
    private static final String NAMESRV_ADDR = "localhost:9876";
    // 主题名称
    private static final String TOPIC = "ClusterBroadcastDemoTopic";
    // 消费组名称
    private static final String CONSUMER_GROUP = "ClusterBroadcastDemoGroup";

    public static void main(String[] args) throws Exception {
        // 启动生产者
        new Thread(() -> {
            try {
                startProducer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        // 等待生产者启动
        Thread.sleep(1000);

        // 启动集群模式消费者1
        new Thread(() -> {
            try {
                startClusterConsumer("ClusterConsumer1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();

        // 启动集群模式消费者2
        new Thread(() -> {
            try {
                startClusterConsumer("ClusterConsumer2");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();

        // 启动广播模式消费者1
        new Thread(() -> {
            try {
                startBroadcastConsumer("BroadcastConsumer1");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();

        // 启动广播模式消费者2
        new Thread(() -> {
            try {
                startBroadcastConsumer("BroadcastConsumer2");
            } catch (MQClientException e) {
                e.printStackTrace();
            }
        }).start();
    }

    /**
     * 启动生产者
     */
    private static void startProducer() throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
        producer.setNamesrvAddr(NAMESRV_ADDR);
        producer.start();
        System.out.println("生产者启动成功");

        // 发送5条消息
        for (int i = 0; i < 5; i++) {
            Message msg = new Message(TOPIC, "TagA", ("这是第" + (i + 1) + "条测试消息").getBytes(StandardCharsets.UTF_8));
            SendResult sendResult = producer.send(msg);
            System.out.printf("发送消息: %s, 状态: %s%n", new String(msg.getBody()), sendResult.getSendStatus());
            Thread.sleep(1000);
        }

        producer.shutdown();
        System.out.println("生产者关闭");
    }

    /**
     * 启动集群模式消费者
     */
    private static void startClusterConsumer(String consumerName) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        consumer.setNamesrvAddr(NAMESRV_ADDR);
        // 设置消费模式为集群模式(默认就是集群模式)
        consumer.setMessageModel(MessageModel.CLUSTERING);
        // 从最新位置开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 订阅主题
        consumer.subscribe(TOPIC, "*");

        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("[集群模式] %s 接收到消息: %s, 消息ID: %s%n",
                            consumerName, new String(msg.getBody(), StandardCharsets.UTF_8), msg.getMsgId());
                }
                // 消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("[集群模式] %s 启动成功%n", consumerName);
    }

    /**
     * 启动广播模式消费者
     */
    private static void startBroadcastConsumer(String consumerName) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        consumer.setNamesrvAddr(NAMESRV_ADDR);
        // 设置消费模式为广播模式
        consumer.setMessageModel(MessageModel.BROADCASTING);
        // 从最新位置开始消费
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 订阅主题
        consumer.subscribe(TOPIC, "*");

        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("[广播模式] %s 接收到消息: %s, 消息ID: %s%n",
                            consumerName, new String(msg.getBody(), StandardCharsets.UTF_8), msg.getMsgId());
                }
                // 消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("[广播模式] %s 启动成功%n", consumerName);
    }
}

posted on 2024-12-01 18:06  ~码铃薯~  阅读(972)  评论(0)    收藏  举报

导航