RocketMQ常见问题和面试题详细解析(只讲干货)

大家出去面试的时候,针对RocketMQ,面试官通常会问一些共性的问题,其实这些问题的解决方案思路大致是一致的,下面就针对这些常见面试题,做一个分析

一、RocketMQ如何保证消息的顺序性

RocketMQ中的顺序消息类似于kafka的实现方式,kafka只针对同一个Partition内的消息是有序的,而RocketMQ是针对同一个队列中消息是有序的,代码示例如下:

public class Producer {
    public static void main(String[] args) throws UnsupportedEncodingException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.setNamesrvAddr("192.168.1.112:9876");
            producer.start();
            for (int i = 0; i < 10; i++) {
                int orderId = i;
                for (int j = 0; j < 5; j++) {
                    Message message = new Message("OrderTopic", "orderTag:" + orderId, "keys:" + orderId, ("order_id:" + orderId + "第" + j + "条消息").getBytes());
                    // 这里发送的时候,要传入MessageQueueSelector去选择队列
                    SendResult sendResult =  producer.send(message, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                            // 这里的arg就是传入的orderId,根据orderId对队列的数量取模,这样同一个orderId的消息就会发送到同一个队列
                            Integer id = (Integer) arg;
                            int index = id % mqs.size();
                            return mqs.get(index);
                        }
                    },orderId);
                    System.out.printf("%s%n", sendResult);
                }
            }
            producer.shutdown();
        } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Consumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("192.168.1.112:9876");
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("OrderTopic","*");
        // 这里要传入MessageListenerOrderly
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for(MessageExt msg : msgs){
                    System.out.println("收到的消息是:" + new String(msg.getBody(), Charset.defaultCharset()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

运行结果如下:

是不是有点奇怪,顺序是乱的,其实RocketMQ的顺序消息,它只能保证局部有序,不能保证全局有序,看结果你会知道orderId是1的消息,在整个队列中的顺序是乱的,但是针对orderId是1的这条记录,它的5条消息一定是有序的。

针对上述的实力总结一下,如何保证消息的顺序性:

1、生产者将一批有顺序的消息,通过MessageQueueSelector将消息发送到同一个队列,但是这样只能保证局部有序,如果要实现全局有序,那么只能保留一个队列,性能会非常低

2、消费者在消费消息的时候一次只锁定一个队列,通过MessageListenerOrderly方式,记得一定要手动提交,消费者端在处理失败的时候,只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,如果消费者⼀直处理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。

二、RocketMQ如何保证消息不丢失

        RocketMQ想保证消息不丢失,那就得分析有哪些步骤会造成消息丢失,这个问题其实我自己认为还是比较复杂的一个环节。

        

第1、2、4步是跨网络的,如果网络出现异常就会产生消息丢失

第3步是持久化消息,RocketMQ在持久化消息的时候,先写入操作系统的page cache中,在由操作系统进行异步刷盘写入磁盘,如果这时候服务挂了,没来得及写入磁盘,也会出现消息丢失

通过以上分析可能出现消息丢失的场景,那就可以针对性的解决这些问题。

1、生产者通过事务消息,保证消息零丢失解决方案

事务消息是RocketMQ比较重要且复杂的消息模型,通过事务消息是可以保证生产者往MQ发送消息零丢失的

1、发送half消息

        发送half消息,其实就是先探测一下RocketMQ服务是否正常,这个消息也称为半消息,消费者是感知不到的

2、half消息写入失败怎么办

        如果half消息写入失败,我们就认为这时候MQ的服务是不可用的,只能将这条消息的状态标记一下,不推送给下游服务,等MQ可用时,在进行补偿操作

3、本地事务写入失败

        如果本地事务写入失败,那我们可以给MQ一个unknown状态,然后把本地事务的信息存储到redis或者数据库,另外起线程去操作,unkonwn状态下,MQ过一段时间就回来检查事务状态,如果这时候事务状态恢复了,就可以通知MQ可以往下游推送了

2、nameserver服务不可用

        如果nameserve服务挂了,这里说的是整个集群都挂了,这时候我们肯定是访问不到MQ的,那只能采取降级方案,将消息存储到本地文件或者数据库中,等服务恢复后,在进行消息推送

3、dledger集群+同步刷盘

        配置一主多从的集群部署方式,将数据的同步方式改为同步同步

        flushDiskType=SYNC_FLUSH

4、消费者不要采用异步消费的机制

        正常情况下,消费者先执行本地事务,然后给MQ一个ack的响应,这时MQ就会修改offset,将消息标记为已消费,但是在执行本地事务的时候,一定不要新起线程去操作

总结:个人觉得想解决消息0丢失,付出的代价有点大,只能说最大程度上解决消息不丢失

三、RocketMQ如何处理消息积压

        消息积压的话,可以通过RocketMQ的web工作台查看,如果产生了消息积压解决方案如下:

1、查看消费者的代码逻辑,看是哪个地方产生了性能问题

2、如果队列足够多,可增加消费者的数量来加快消息的消费

3、如果消息不重要,可以直接跳过堆积

4、可以新建一个topic,将积压的消息转移到这个新建topic上面,在进行处理

四、RocketMQ如何实现延时消息

        延迟消息是指生产生发送消息后,不想消息立马被消费,而是延迟一段时间再推送给消费者

发送延迟消息,只需要在代码中指定:

msg.setDelayTimeLevel(3);

RocketMQ内置了18个延迟级别,每个延迟级别对应如下:

延迟消息是将消息存储到一个叫SCHEDULE_TOPIC_XXXX的topic下面,每次只针对这18个队列里面的消息进行延迟操作,这样就不用一直扫描了,当时间到了以后,就会将延迟队列的消息转发到真正的topic下面,RocketMQ5.0以后,支持指定时间戳的方式投递延迟消息

五、RocketMQ事务消息详细解释

        

1、首先向RocketMQ发送一个half消息,half消息是半消息,消费者感知不到

2、MQ返回half消息投递成功

3、开始执行本地事务,也就是我们的数据库操作

4、执行完成本地事务以后,告诉broker是commit,rollback,unknown

5、如果本地事务返回的是commit状态,那就将消息推送给消费者,如果是rollback状态,那就舍弃掉消息,不在投递,如果因为网络或者其他问题,没有返回消息,那就是unkonwn状态,这个状态会触发回查

6、重新检查本地事务的状态,继续走第4步的流程,本地事务回查次数通过参数transactionCheckMax设定,默认15次。本地事务回查的间隔通过 参数transactionCheckInterval设定,默认60秒。超过回查次数后,消息将会被丢弃。

7、推送给消费者以后,消费者自行处理消息,如果失败,那么就会触发重试

RocketMQ的事务消息,其实是半事务消息,只能保证生产者和本地事务的事务,消费者需要靠重试机制来保证事务的最终一致性。

六、RocketMQ注意事项

1、合理分配Topic、Tag

       一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags("TagA")。

2、使用key加快消息索引

        每个消息在业务层面的唯一标识码要设置到keys字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过topic、key来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证key尽可能唯一,这样可以避免潜在的哈希冲突。

 3、关注错误消息

producer的send方法本身支持内部重试,重试逻辑如下:

  • 至多重试2次(同步发送为2次,异步发送为0次)。
  • 如果发送失败,则轮转到下一个Broker。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。
  • 如果本身向broker发送消息产生超时异常,就不会再重试。

以上策略也是在一定程度上保证了消息可以发送成功。如果业务对消息可靠性要求比较高,建议应用增加相应的重试逻辑:比如调用send同步方法发送失败时,则尝试将消息存储到db,然后由后台线程定时重试,确保消息一定到达Broker。

4、手动处理死信队列,死信队列的名称是%DLQ%+ConsumGroup

5、消费者端进⾏幂等控制

        RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。

七、RocketMQ生产环境服务器配置优化

1、JVM选项

推荐使用最新发布的 JDK 1.8 版本。通过设置相同的 Xms 和 Xmx 值来防止 JVM 调整堆大小以获得更好的性能。生产环境 JVM 配置如下所示:

-server -Xms8g -Xmx8g 

当 JVM 是默认 8 字节对齐,建议配置最大堆内存不要超过 32 G,否则会影响 JVM 的指针压缩技术,浪费内存。

2 Linux内核参数

os.sh 脚本在 bin 文件夹中列出了许多内核参数,可以进行微小的更改然后用于生产用途。下面的参数需要注意,更多细节请参考 /proc/sys/vm/*的 文档

  • vm.extra_free_kbytes 告诉 VM 在后台回收(kswapd)启动的阈值与直接回收(通过分配进程)的阈值之间保留额外的可用内存。RocketMQ 使用此参数来避免内存分配中的长延迟。(与具体内核版本相关)
  • vm.min_free_kbytes 如果将其设置为低于 1024 KB,将会巧妙的将系统破坏,并且系统在高负载下容易出现死锁。
  • vm.max_map_count 限制一个进程可能具有的最大内存映射区域数。RocketMQ 将使用 MMAP 加载 CommitLog 和 ConsumeQueue,因此建议将为此参数设置较大的值。
  • vm.swappiness 定义内核交换内存页面的积极程度。较高的值会增加攻击性,较低的值会减少交换量。建议将值设置为 10 来避免交换延迟。
  • File descriptor limits RocketMQ 需要为文件( CommitLog 和 ConsumeQueue )和网络连接打开文件描述符。我们建议设置文件描述符的值为 655350。
  • Disk scheduler RocketMQ建议使用I/O截止时间调度器,它试图为请求提供有保证的延迟。

到此,RocketMQ的篇章就结束了,以上部分内容来源于官网,大家也可以去官网查看文档,不得不说,国内开源的软件的文档还是比较详细的,符合国人的阅读习惯

官网地址:https://rocketmq.apache.org/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sun初一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值