消息队列常见面试题

本文详细探讨了消息队列在解耦、异步、削峰等场景的应用,阐述了生产者-消费者模式,以及如何在RabbitMQ、Kafka、RocketMQ中保证消息顺序消费。同时,分析了消息队列如何防止消息丢失,提出RabbitMQ和Kafka在确保消息顺序和避免重复消费方面的策略。最后,讨论了MQ的推拉模式、RabbitMQ与Kafka的区别以及Kafka的高性能原因。

2. 消息队列

2.1 MQ有什么用?

参考答案

消息队列有很多使用场景,比较常见的有3个:解耦、异步、削峰。

  1. 解耦:传统的软件开发模式,各个模块之间相互调用,数据共享,每个模块都要时刻关注其他模块的是否更改或者是否挂掉等等,使用消息队列,可以避免模块之间直接调用,将所需共享的数据放在消息队列中,对于新增业务模块,只要对该类消息感兴趣,即可订阅该类消息,对原有系统和业务没有任何影响,降低了系统各个模块的耦合度,提高了系统的可扩展性。
  2. 异步:消息队列提供了异步处理机制,在很多时候应用不想也不需要立即处理消息,允许应用把一些消息放入消息中间件中,并不立即处理它,在之后需要的时候再慢慢处理。
  3. 削峰:在访问量骤增的场景下,需要保证应用系统的平稳性,但是这样突发流量并不常见,如果以这类峰值的标准而投放资源的话,那无疑是巨大的浪费。使用消息队列能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。消息队列的容量可以配置的很大,如果采用磁盘存储消息,则几乎等于“无限”容量,这样一来,高峰期的消息可以被积压起来,在随后的时间内进行平滑的处理完成,而不至于让系统短时间内无法承载而导致崩溃。在电商网站的秒杀抢购这种突发性流量很强的业务场景中,消息队列的强大缓冲能力可以很好的起到削峰作用。
2.2 说一说生产者与消费者模式

参考答案

所谓生产者-消费者问题,实际上主要是包含了两类线程。一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:

  1. 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
  2. 如果共享数据区为空的话,阻塞消费者继续消费数据。

在Java语言中,实现生产者消费者问题时,可以采用三种方式:

  1. 使用 Object 的 wait/notify 的消息通知机制;
  2. 使用 Lock 的 Condition 的 await/signal 的消息通知机制;
  3. 使用 BlockingQueue 实现。
2.3 消息队列如何保证顺序消费?

参考答案

在生产中经常会有一些类似报表系统这样的系统,需要做 MySQL 的 binlog 同步。比如订单系统要同步订单表的数据到大数据部门的 MySQL 库中用于报表统计分析,通常的做法是基于 Canal 这样的中间件去监听订单数据库的 binlog,然后把这些 binlog 发送到 MQ 中,再由消费者从 MQ 中获取 binlog 落地到大数据部门的 MySQL 中。

在这个过程中,可能会有对某个订单的增删改操作,比如有三条 binlog 执行顺序是增加、修改、删除。消费者愣是换了顺序给执行成删除、修改、增加,这样能行吗?肯定是不行的。不同的消息队列产品,产生消息错乱的原因,以及解决方案是不同的。下面我们以RabbitMQ、Kafka、RocketMQ为例,来说明保证顺序消费的办法。

RabbitMQ:

对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息。如消费者A执行了增加,消费者B执行了修改,消费者C执行了删除,但是消费者C执行比消费者B快,消费者B又比消费者A快,就会导致消费 binlog 执行到数据库的时候顺序错乱,本该顺序是增加、修改、删除,变成了删除、修改、增加。如下图:

img

RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,生产者发送消息的时候,同一个订单号的消息发送到同一个 queue 中,由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。如下图:

img

Kafka:

对于 Kafka 来说,一个 topic 下同一个 partition 中的消息肯定是有序的,生产者在写的时候可以指定一个 key,通过我们会用订单号作为 key,这个 key 对应的消息都会发送到同一个 partition 中,所以消费者消费到的消息也一定是有序的。

那么为什么 Kafka 还会存在消息错乱的问题呢?问题就出在消费者身上。通常我们消费到同一个 key 的多条消息后,会使用多线程技术去并发处理来提高消息处理速度,否则一条消息的处理需要耗时几十 毫秒,1 秒也就只能处理几十条消息,吞吐量就太低了。而多线程并发处理的话,binlog 执行到数据库的时候就不一定还是原来的顺序了。如下图:

img

Kafka 从生产者到消费者消费消息这一整个过程其实都是可以保证有序的,导致最终乱序是由于消费者端需要使用多线程并发处理消息来提高吞吐量,比如消费者消费到了消息以后,开启 32 个线程处理消息,每个线程线程处理消息的快慢是不一致的,所以才会导致最终消息有可能不一致。

所以对于 Kafka 的消息顺序性保证,其实我们只需要保证同一个订单号的消息只被同一个线程处理的就可以了。由此我们可以在线程处理前增加个内存队列,每个线程只负责处理其中一个内存队列的消息,同一个订单号的消息发送到同一个内存队列中即可。如下图:

img

RocketMQ:

对于 RocketMQ 来说,每个 Topic 可以指定多个 MessageQueue,当我们写入消息的时候,会把消息均匀地分发到不同的 MessageQueue 中,比如同一个订单号的消息,增加 binlog 写入到 MessageQueue1 中,修改 binlog 写入到 MessageQueue2 中,删除 binlog 写入到 MessageQueue3 中。

但是当消费者有多台机器的时候,会组成一个 Consumer Group,Consumer Group 中的每台机器都会负责消费一部分 MessageQ

### 基础概念类 - 什么是消息队列消息队列是一种在不同组件或服务之间传递消息的通信机制,它可以实现异步通信、解耦系统组件、缓冲流量等功能[^1]。 - 消息队列有哪些常见的使用场景? 常见场景包括异步处理、系统解耦、流量削峰、日志收集等。例如,在电商系统中,用户下单后,将订单信息放入消息队列,后续的库存扣减、物流通知等操作可以异步处理,实现系统解耦和提高响应速度。 ### 消息处理类 - 消息队列如何解决消息丢失问题? 可以从生产者、消息队列本身和消费者三个方面来解决。生产者可以采用确认机制,确保消息成功发送到消息队列消息队列可以进行持久化配置,将消息存储到磁盘;消费者采用手动确认机制,处理完消息后再向消息队列确认,避免消息丢失[^1]。 - 消息队列如何保证消息的顺序性? 一种方式是将相关联的消息发送到同一个队列或分区中,例如,同一个用户的操作消息发送到同一个队列。另一种方式是在消费者端进行顺序处理,按照消息的顺序依次处理。 - 消息队列有可能发生重复消费吗?如何幂等处理? 消息队列可能会因为网络波动、消费者故障等原因导致重复消费。幂等处理可以通过唯一标识(如消息ID)来实现,在处理消息前,先检查该消息是否已经处理过,如果已经处理过则直接忽略。例如,在数据库中使用唯一索引,插入消息ID时,如果已经存在则不进行重复插入。 ```python # 伪代码示例,使用 Redis 实现幂等处理 import redis redis_client = redis.Redis(host='localhost', port=6379, db=0) def process_message(message_id, message): if redis_client.setnx(message_id, 1): # 处理消息的逻辑 print(f"Processing message: {message}") # 处理完成后可以设置过期时间,避免占用过多内存 redis_client.expire(message_id, 3600) else: print(f"Message {message_id} has already been processed.") ``` - 如何处理消息队列的消息积压问题? 可以增加消费者数量,提高消费能力;优化消费者的处理逻辑,减少处理时间;检查消息队列的配置,确保其性能达到最优;如果积压严重,可以将部分消息转移到其他队列或存储中,后续再进行处理。 ### 技术选型与高可用类 - 消息队列技术选型,Kafka、RocketMQ、RabbitMQ 如何选择? Kafka 具有高吞吐量、分布式、分区容错等特点,适合处理大规模的流式数据和日志收集;RocketMQ 具有高性能、高可靠、分布式事务等特性,在金融、电商等领域应用广泛;RabbitMQ 功能丰富,支持多种消息协议,易于集成,适合对消息可靠性要求较高的场景。 - 消息中间件如何做到高可用? 可以采用集群部署的方式,例如,Kafka 通过多个 Broker 组成集群,RocketMQ 通过主从复制和多副本机制,RabbitMQ 通过镜像队列等方式来保证高可用。同时,需要监控系统对消息队列的运行状态进行实时监控,及时发现和处理故障。 ### 数据一致性类 - 如何保证数据一致性,事务消息如何实现? 可以使用事务消息来保证数据一致性。以 RocketMQ 为例,实现步骤包括:生产者发送半消息到消息队列;生产者执行本地事务;根据本地事务的执行结果,向消息队列发送确认消息,消息队列根据确认结果决定是否将消息投递给消费者。 ```java // Java 代码示例,使用 RocketMQ 实现事务消息 import org.apache.rocketmq.client.producer.*; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; public class TransactionProducer { public static void main(String[] args) throws Exception { TransactionMQProducer producer = new TransactionMQProducer("producer_group"); producer.setNamesrvAddr("localhost:9876"); producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { // 执行本地事务 try { // 模拟本地事务操作 System.out.println("Executing local transaction..."); return LocalTransactionState.COMMIT_MESSAGE; } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; } } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 消息队列回查本地事务状态 System.out.println("Checking local transaction state..."); return LocalTransactionState.COMMIT_MESSAGE; } }); producer.start(); Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes()); producer.sendMessageInTransaction(message, null); // 关闭生产者 producer.shutdown(); } } ``` ### 架构设计类 - 如果让你写一个消息队列,该如何进行架构设计? 架构设计可以包括以下几个部分:网络通信层,负责生产者和消费者与消息队列的通信;消息存储层,将消息持久化存储;消息分发层,根据路由规则将消息分发到相应的队列或分区;元数据管理,管理队列、主题等元数据信息;高可用和容错机制,如集群部署、数据备份等。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值