Java 热门面试题200道之消息队列(16题)
(1)RabbitMQ 怎么实现延迟队列?
RabbitMQ本身不支持延迟消息,但是可以通过它提供的两个特性TTL(Time-To-Live and Expiration,消息存活时间)、DLX(Dead Letter Exchanges,死信交换器)来实现。还可以利用RabbitMQ插件来实现。
使用TTL+死信队列:
在RabbitMQ中,通过设置消息的TTL和死信交换器可以实现延迟队列。
不给原队列(正常队列)设置消费者,当消息在原队列中达到TTL后,由于还未被消费,则会被转发到绑定的死信交换器,消费者从死信队列中消费消息,从而实现消息的延迟处理。
使用RabbitMQ插件:延迟消息插件(rabbitmq-delayed-message-exchange):
通过安装RabbitMQ的延迟消息插件,可以直接创建延迟交换器(Delayed Exchange)。
在发送消息时,指定消息的延迟时间,RabbitMQ会在消息达到延迟时间后将其转发到对应的队列进行消费。
(2)RabbitMQ 中消息什么时候会进入死信交换机?
消息会在以下几种情况下进入死信交换机(Dead Letter Exchange,DLX):
- 消息被拒绝:当消费者使用basic.reject或basic.nack明确拒绝信息,并且不要求重写投递(request设置为false)时,消息会被直接投递给死信交换机。
- 消息过期(TTL Expiration):RabbitMQ支持为消息或队列设置TTL(Time-To-Live),即生存时间。当消息超过指定的存活时间后还未被消费,它会自动变为死信并被发送到死信交换机。
- 队列达到最大长度(或总大小):如果队列设置了最大长度(x-max-length或x-max-length-bytes),当消息数量或总大小超过限制时,最早进入队列的消息会被移入死信交换机.
这些条件进入死信交换机的消息,可以再通过死信队列进行日志记录、重新处理或监控。
死信队列的设置
- 绑定死信交换机:在声明队列时,可以为该队列配置一个死信交换机,通过参数x-dead-letter-exchange和x-dead-letter-routing-key指定死信交换机和路由键。
(3)RabbitMQ 中无法路由的消息会去到哪里?
在RabbitMQ中,无法被路由的消息通常有以下几种处理方式:
- 丢弃消息:默认情况下,若消息无法找到符合条件的队列(即没有匹配的绑定关系),RabbitMQ会直接丢弃消息,不会进行特殊处理。
- 备份交换机:可以为交换机配置一个备份交换机,无法被路由的消息将被发送到备份交换机,再由备份交换机根据其绑定关系决定如何处理消息。例如,可以将这些消息发送到指定队列进行保存或处理。
- 消息回退:在使用mandatory参数的情况下,如果消息无法路由,则会触发返回机制,将消息退回到生产者,这样生产者可以自动处理未路由的消息。
(4)Kafka 为什么要抛弃 Zookeeper?
主要是为了简化架构、提升可扩展性和降低运维复杂性。Kafka引入了KRaft(KafkaRaft)模式,即使用Kafka自身实现的Raft共识算法替代Zookeeper。
以下是Kafka抛弃Zookeeper的几个主要原因:
- 简化架构:Zookeeper是一个独立的分布式协调服务,Kafka需要依赖它来管理元数据。引入Zookeeper增加了系统复杂度和运维成本,尤其在处理集群的动态扩展和管理时,Kafka和Zookeeper之间的协调带来了额外的开销。通过KRaft,Kafka可以直接管理元数据,消除了对外部协调服务的依赖。
- 提升可扩展性:Zookeeper的写入性能有限,随着Kafka集群的规模增大,元数据的读写操作可能会对Zookeeper造成压力,进而成为Kafka扩展性的瓶颈。KRaft模式通过将元数据存储在Kafka自身中,并使用Raft协议来确保一致性,使Kafka的扩展能力得到提升。
- 降低运维成本:在生产环境中,Kafka和Zookeeper之间需要进行一致性管理和维护,运维人员需要掌握两套系统的部署、监控和故障排查。去掉Zookeeper后,Kafka只需维护自身的节点和协议,简化了运维流程。
(5)Kafka 中 Zookeeper 的作用?
在Kafka中,Zookeeper扮演了集群协调和管理的核心角色。它的主要作用是管理和协调Kafka集群中的元数据,帮助Kafka实现高可用性、负载均衡和容错性。
以下是Kafka中Zookeeper的几个关键作用:
- 管理Broker元数据:Zookeeper负责管理Kafka集群中的Broker的注册、状态监控。当有新的Broker加入或离开集群时,Zookeeper能够及时更新集群状态。
- 协调分区副本Leader选举:当某个分区的Leader副本故障时,Zookeeper协调副本的选举过程,为该分区选出新的Leader,确保分区高可用。
- 管理消费者的Offset:在早期版本的Kafka中,消费者的Offset信息存储在Zookeeper中,以便消费者在重启后可以从上次消费的位置继续消费。最新的Kafka版本将Offset存储移至Kafka自身的内部主_consumer_offsets,减少了对Zookeeper 的依赖。
- 动态配置和负载均衡:Zookeeper保存着Kafka配置和拓扑信息,当集群发生变化时(如增加或减少分区、调整副本因子),Zookeeper协助完成负载均衡。
(6)说一下 Kafka 中关于事务消息的实现?
Kafka的事务消息不同于我们理解的分布式事务消息,它的事务消息时实现了消息的ExactylOnce语义,即保证消息在生产、传输和消费过程中的“仅一次”传递。
Kafka的事务消息主要通过以下几个核心组件来实现:
- 事务协调器:负责事务的启动、提交和中止管理,并将事务状态记录到__transaction_state内部主题。
- 幂等生产者:Kafka生产者通过Producer ID(PID)识别每个事务的唯一性,确保同一事务的每条消息只写入一次。
- 事务性消费:在消费过程中,消费者可以选择隔离未完成事务的数据(通过read_committed设置)只消费已提交的事务消息,确保数据的最终一致性。
Kafka事务消息流程:
- 启动事务:事务性生产者向Transaction Coordinator(事务协调者)请求启动事务。
- 提交事务:在所有消息写入完成后,生产者向事务协调器发送commit或abort请求,提交或中止事务。
- 事务性消费:消费者可以通过设置read_committed隔离级别,仅消费已提交的消息,实现最终数据一致性。
(7)说一下 RocketMQ 中关于事务消息的实现?
Kafka的事务消息不同于我们理解的分布式事务消息,它的事务消息时实现了消息的ExactylOnce语义,即保证消息在生产、传输和消费过程中的“仅一次”传递。
Kafka的事务消息主要通过以下几个核心组件来实现:
- 事务协调器:负责事务的启动、提交和中止管理,并将事务状态记录到__transaction_state内部主题。
- 幂等生产者:Kafka生产者通过Producer ID(PID)识别每个事务的唯一性,确保同一事务的每条消息只写入一次。
- 事务性消费:在消费过程中,消费者可以选择隔离未完成事务的数据(通过read_committed设置)只消费已提交的事务消息,确保数据的最终一致性。
Kafka事务消息流程:
- 启动事务:事务性生产者向Transaction Coordinator(事务协调者)请求启动事务。
- 提交事务:在所有消息写入完成后,生产者向事务协调器发送commit或abort请求,提交或中止事务。
- 事务性消费:消费者可以通过设置read_committed隔离级别,仅消费已提交的消息,实现最终数据一致性。
(8)RocketMQ 的事务消息有什么缺点?你还了解过别的事务消息实现吗?
从事务消息的改造成本来看,RocketMQ的事务消息改造成本不小,需要改造原始逻辑实现特定的接口,且在应用层处理复杂的回查逻辑,确保回查不会重复或丢失。
从事务消息功能性来看,RocketMQ仅支持单事务消息。
从可用性角度来看,如果MQ集群挂了,事务就无法继续进行了,等于整个应用无法正常执行了。因为RocketMQ的事务消息的实现是先发半消息,如果MQ集群挂了,则半消息无法发送成本,后续的逻辑就无法执行。
(9)为什么需要消息队列?
从本质上来说是因为互联网的快速发展,业务不断扩张,促使技术架构需要不断地演进。
从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有100个在线用户已经很了不得,到现在坐拥10亿日活的微信。
我们需要有一个中间件来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。
消息队列就应运而生了。它常用来实现:异步处理、服务解耦、流量控制。
消息队列的经典应用场景:
- 订单系统:在电商系统中,订单的创建、支付、发货等步骤可以通过消息队列进行异步处理和解耦。
- 日志处理:使用消息队列将日志从应用系统传输到日志处理系统,实现实时分析和监控。
- 任务调度:在批量任务处理、任务调度系统中,通过消息队列将任务分发给多个工作节点,进行并行处理。
- 数据同步:在数据同步系统中,消息队列可以用于将变更的数据异步同步到不同的存储系统或服务。
(10)说一下消息队列的模型有哪些?
常见的消息队列模型主要有以下两种:
队列模型(也称点对点模型):
- 在队列模型中,消息从生产者发送到队列,并且每条消息只能被一个消费者消费一次。消费之后,消息在队列中被删除。
- 适用于任务处理类场景,如一个任务只需要一个处理者执行。
发布/订阅模型(Publish/Subscibe): - 在发布/订阅模型中,生产者将消息发布到某个主题,所有订阅了该主题的消费者都会接收到该消息。
- 每个订阅者都会接收到相同的消息,适用于广播通知、实时推送等场景。
(11)如何处理重复消息?
消息队列处理重复消息的方法主要包括生产者防重、Broker防重和消费者防重:
生产者防重
- 唯一消息ID生成:在消息生产阶段为每条消息添加唯一ID,如UUID,确保消息在整个系统中的唯一性。
- 事务性消息保障:使用事务性消息,如RocketMQ的事务消息,确保消息发送与本地事务的原子性,避免重复发送。
Broker防重
- Broker去重功能:部分消息中间件(如Pulsar)支持Broker端去重,通过消息的sequenceId等机制,避免重复消息的存储。
消费者防重
- 幂等性处理:设计业务逻辑时确保幂等性,即多次处理相同消息的结果与处理一次相同。可通过以下方式实现:
- 数据库唯一约束:在业务表中添加唯一索引,利用数据库约束避免重复数据。
- Redis缓存去重:使用Redis记录已处理消息的ID,通过SETNX命令判断消息是否已处理。
- 状态机判断:根据业务状态判断消息是否需要处理,如订单状态机,忽略旧消息。
- 正确执行ACK确认:确保消息处理完成后才发送ACK确认,避免因未确认导致消息被重新投递。
(12)如何保证消息的有序性?
保证消息有序性的常见方法如下:
单一生产者和单一消费者:
- 使用单个生产者发送消息到单个队列,并由单个消费者处理消息。这样就可以确保消息按照生产者的发送顺序消费。
- 这种方法简单但容易成为性能瓶颈,无法充分利用并发的优势。
分区与顺序键:
- 在支持分区的消息队列中,可以通过PartitionKey将消息发送到特定的分区。每个分区内部都是有序的,这样就可以保证相同的PartitionKey的消息按顺序消费。
- 例如,在订单处理系统中,可以使用订单号作为PartitionKey,将同一个订单的所有消息路由到同一个分区,确保该订单的消息顺序。
顺序队列:
- 一些消息队列系统支持顺序队列,消息在队列中的存储顺序与投递顺序一致,如果使用单个顺序队列,消息将按顺序被消费。
- 可以使用多个顺序队列来提高并发处理能力,并使用特定规则将消息分配到不同的顺序队列中。
(13)如何处理消息堆积?
消息堆积是指在消息队列中,消息的生产速度远大于消费速度,导致大量消息积压在队列中。我们需要先定位消费满的原因,如果是bug则处理bug,同时可以临时扩容增加消费速率,减少线上的资损。
如果是因为本身消费能力较弱,则可以优化以下消费逻辑。
常见有以下几种方式提升消费者的消费能力:
- 增加消费者线程数量:提高并发消费能力。
- 增加消费实例:在分布式系统中,可以水平扩展多个消费实例,从而提高消费速率。
- 优化消费者逻辑:检查消费者的代码,减少单个消息的处理时间。例如,减少I/O操作、使用批量处理等。
注意上述的第二点:
- 增加消费实例,一定要注意Topic对应的分区/队列数需要大于等于消费实例数,不然新增加的消费者是没东西消费的。因为一个Topic中,一个分区/队列只会分配给一个消费实例。(会造成rebalance其实)
除此之外还可以进行限流和降级处理:
- 对消息生产端进行限流,降低生产速率,避免消息积压进一步恶化。
- 对非关键消息进行丢弃或延迟处理,只保留高优先级的消息,提高系统的响应速度。
(14)如何保证消息不丢失?
这需要生产消息、存储消息和消费消息这三个阶段通过努力才能保证消息不丢失。
- 生产者的消息确认:生产者在发送消息时,需要通过消息确认机制来确保消息成功到达。
- 存储消息:broker收到消息后,需要将消息持久化到磁盘上,避免消息因内存丢失。即使消息队列服务器重启或宕机,也可以从磁盘中恢复消息。
- 消费者的消息确认:消费者在处理完消息后,再向消费队列发送确认(ACK),如果消费者未发送确认,消息对垒需要重新投递该消息。
除此之外,如果消费者持续消费失败,消费队列可以自动进行重试或将消息发送到死信队列(DLQ)或通过日志等其他手段记录异常的消息,避免因一时的异常导致消息丢失。
(15)消息队列设计成推消息还是拉消息?推拉模式的优缺点?
推模式(Push):消费队列将消息主动推送给消费者,适合实时性要求高、消费者能够及时处理消息的场景。
- 优点:实时性好,消息可立即送达消费者。
- 缺点:难以控制消费速度,容易导致消费者过载,尤其是在高并发时。
拉模式(Pull):消费者主动从消费队列中拉取消息,适合消费能力有限、需要根据自身处理能力调控速率的场景。
- 优点:消费者可以根据自身负载决定拉取频率,避免过载;更适合批量处理。
- 缺点:可能会导致消息延迟,实时性不如推模式,尤其是拉取频率较低时。
(16)为什么 RocketMQ 不使用 Zookeeper 作为注册中心?
RocketMQ在开发的时候充分吸取了前人的教训,特意轻量化注册中心的实现。
RocketMQ之所以没有选择Zookeeper作为注册中心,而是自己实现了一个轻量级的NameServer,主要原因有以下几点:
- 轻量级和高性能需求:RocketMQ需要一个高性能、低延迟的注册中心,而Zookeeper的强一致性和复杂的管理机制带来了一定的开销。NameServer是一个简单的、轻量的服务发现和路由管理组件,更适合于消息队列的需求。
- 动态拓扑和高扩展性:NameServer采用无状态设计,可以随时增加或减少节点,且NameServer节点间不需要进行同步,减少了集群复杂度,适应动态的Broker集群环境,便于拓展和维护。
- 弱一致性要求:RocketMQ的服务发现和路由信息允许短时间内的不一致,NameServer只提供基础的服务发现功能,不需要像Zookeeper那样强一致的分布式一致性算法,这样可以提高性能和减少管理复杂度。
- 独立性与可靠性:RocketMQ希望尽可能减少对外部组件的依赖,以便在复杂场景中保证系统的稳定性和可靠性。NameServer独立运作,不需要外部分布式协调系统支持,降低了系统耦合度。
更多推荐


所有评论(0)