# 消息队列面试题


大家好,我是小林。

消息队列这块,在面试里出现的频率越来越高,尤其是做过有一定规模业务的同学,基本逃不掉。很多人平时用 MQ 就是往里发消息、消费消息,问起来能说出个"解耦、异步、削峰",但一旦被追问"消息丢了怎么办""消息重复了怎么处理""积压了几百万条消息怎么解决",就开始说不清楚了。这块知识其实不算难,但要答得有深度,还是得把原理搞透。

这篇文章整理了消息队列面试中最常被问到的知识点,涵盖 MQ 的使用场景、消息可靠性、幂等处理、消息积压,以及 RocketMQ、Kafka、RabbitMQ 三大主流 MQ 的核心原理和对比,覆盖比较全面。

有几块在面试里特别容易被深挖,建议重点花时间:

  • 消息可靠性:生产者、Broker、消费者三个环节各自怎么保证不丢消息,这是必考题,而且考官很喜欢追问每个环节的细节。
  • 重复消费和幂等:为什么会重复、怎么设计幂等方案,这个和业务结合很紧,答起来容易流于表面。
  • 消息积压:如何快速处理积压,尤其是 RocketMQ 里队列数和消费者数的关系,是很多人的盲区。
  • Kafka 和 RocketMQ 的区别:存储模型、协调机制、适用场景,这是高频对比题,面试里问得很多。
  • 事务消息:怎么保证本地事务和消息发送的一致性,半消息机制是怎么工作的,这块能和分布式事务挂钩,答好了很加分。

如果你是第一次系统准备这块,建议先搞清楚 MQ 的基本原理(为什么用、怎么用),再去看可靠性和幂等这两个核心问题,然后再对比几个主流 MQ 的差异,这个顺序学下来会比较顺。


# 消息队列场景

# 什么是消息队列?

你可以把消息队列理解为一个使用队列来通信的组件。它的本质,就是个转发器,包含发消息、存消息、消费消息的过程。最简单的消息队列模型如下:

我们通常说的消息队列,简称MQ(Message Queue),它其实就指消息中间件,当前业界比较流行的开源消息中间件包括:RabbitMQ、RocketMQ、Kafka。

# 为什么需要消息队列?

如果用一句话概括为什么我们要大费周章地引入 MQ,核心其实就是为了解决系统架构中的三大痛点:解耦、异步、削峰。

我可以结合电商的业务场景,给你拆开讲一下这三点:

第一点,最核心的作用是为了「解耦」(把绑在一起的系统拆开)。 在没有 MQ 的时候,假设我们做个电商系统,用户付完款后,系统要干好几件事:触发发货、发优惠券、加积分、还要发短信通知。如果订单系统直接用 RPC 调用去挨个通知这些子系统,那代码就高度耦合在了一起。万一有一天不需要发短信了,或者新加了一个自动发货服务,订单系统的研发就得整天去改代码重新发版;更别提如果哪天「发积分服务」挂了报错,甚至会连累整个订单支付流程跟着失败。 引入 MQ 后,整个逻辑就变了。订单系统付完款,它只需要对着 MQ 丢一句话(发一条消息):「用户A支付成功了」,后面的非核心流程就可以交给各个消费者处理了。后续不管有几个子系统关心这件事,它们自己去订阅 MQ 就好了,哪怕以后新加再多依赖服务,订单系统通常不需要逐个增加远程调用。不过,事件的字段和含义仍然要与下游约定好,消息发送也要有可靠性保障。这样就降低了系统之间的直接依赖,下游暂时故障也不会马上拖住主流程。

第二点,顺理成章带来的性能提升叫作「异步」。 紧接上面的例子,如果要串行调用发短信、加积分等等这么多非核心流程,哪怕每个接口只用 50 毫秒,多个接口加起来,就会拉长用户在页面等待的时间,疯狂转圈圈,体验极差。 有了 MQ,主流程其实只需要把状态改掉,然后往 MQ 里抛一条消息(具体耗时取决于网络和消息确认配置),立刻就可以给前端响应「恭喜你支付成功」了。像发短信、加积分这种「不是非得在这零点几秒内完成」的操作,完全可以让系统在后台顺着 MQ 慢慢执行,这样前台接口的响应时间就大幅缩短了。

第三点,是高并发场景下的保命神器,叫作「削峰填谷」。 假设我们通过压测,知道底层数据库每秒能够处理一两千个这类请求。但是一旦搞促销、秒杀,瞬间如果有两万个请求涌进来,如果直接打到数据库上,数据库就可能被压垮,出现响应变慢、超时甚至不可用。 这个时候,MQ 就扮演了一个极其关键的**「蓄水池」或者「排队区」**的作用。这两万个瞬时狂暴请求过来,我们统一先扔进 MQ 里暂存着。然后后端的订单处理程序,根据自己数据库真实的抗压能力,平稳地、以一秒钟一两千个的速度,慢慢从 MQ 里往外拉任务来消化。这就像游乐场门口的蛇形排队通道,把瞬间爆发的流量洪峰(削峰),分散到了后续慢慢处理(填谷)。这样把短时间的流量峰值分散到后续处理。不过,MQ 只能缓冲短时高峰,如果流量长期高于消费能力,消息仍然会持续积压,还需要限流、扩容和容量保护。

总结一下: 我们引入 MQ,是为了通过解耦降低直接依赖,通过异步缩短主流程响应时间,通过削峰缓冲突发流量。但它也会让系统变复杂,消息可靠性、重复消费、积压这些问题都得处理。所以要不要用,还是看业务收益是否值得额外的复杂性。

# 消息队列有什么缺点?

关于消息队列(MQ)的优缺点,在软件架构设计里有一句老话叫「没有银弹」。引入 MQ 绝对是一次典型的 Trade-off(技术权衡),它既能拯救系统,又能带来一堆让人头疼的麻烦。

我们可以分两面来看:

首先是它的优点,总结起来就是「系统架构救命的三板斧」:

  1. 解耦:把各个原本互相关联的系统拆解开。A 系统只要把消息丢给 MQ 就完事了,B 系统或者日后新加的 C 系统想要数据,自己去 MQ 拿就行。A 系统的代码不用再为了别的系统频繁修改,大家各干各的。
  2. 异步:提升了用户的响应速度。一些非核心、但又很费时间的操作(比如发短信、送积分),全部扔进 MQ 后台慢慢执行。主流程不再等待所有下游操作,用户能更快得到响应。
  3. 削峰:这是大促、秒杀时挡在数据库前面的超级盾牌。把洪水猛兽般瞬间涌入的并发请求,先拦截在 MQ 的队列里,变成了细水长流,让数据库能按照自己的节奏平稳处理,保住系统不崩溃。

但是,既然它这么好,为什么不每个地方都加呢?这就不得不提它带来的三大缺点或者说是巨大挑战:

第一,系统多了一个需要保障可用性的组件。 在没有 MQ 时,A 系统直接调 B 系统,只要它俩不出问题就行。现在中间横插了一个 MQ 组件,多拉了一个「中间人」。一旦这个机器宕机、网络故障,依赖它的发送和消费流程就可能受影响。所以要设计多副本、故障恢复,以及本地消息表等补偿方案。另一方面,异步隔离也能减轻下游故障对主流程的影响,整体可用性不能只凭多了一个组件来判断。

第二,系统的开发和排查复杂性会上升。 原本很简单的一个方法调用,换成发消息之后,开发人员就面临无数个让人掉头发的异常场景。比如:网络抖动导致一条消息发了两次,B 系统怎么保证不把老王的钱扣两次?(这就需要做业务侧的防重和幂等性设计);再比如,MQ 自己重启了,还没处理完的消息怎么保证不丢失?这些都需要我们在写代码时增加非常多的确认机制、补偿机制。

第三,最头疼的「数据一致性」问题。 A 系统本地业务执行成功了,消息发给 MQ 了,就高高兴兴告诉前端「操作成功」。结果下游的 B 系统拿到这条消息后,由于一条空指针异常没处理好,业务失败了。现在好了,A 以为成功了,B 实际失败了,两边的数据就暂时不一致了。如果 B 只是短暂失败,可以通过重试恢复;如果一直失败,就要通过死信处理、对账和补偿解决。本地事务与发消息之间,还要通过事务消息或本地消息表避免只成功一半。

总结一下: MQ 就像是一把威力巨大的重型武器,优点极其耀眼。但它的缺点决定了,我们绝不是逢坑就用。只有当业务场景真的对解耦、异步、抗高并发有强烈需求,且这部分收益远远大过引入这些复杂性带来的麻烦时,我们才会去谨慎地使用它。

# 消息队列怎么选型?

选 MQ 的时候,我会先看业务到底需要什么,再看吞吐量和运维成本。不能只背一张「谁能扛十万 QPS」的表,因为消息大小、刷盘方式、副本数、批量参数一变,测试结果就可能差很多。

可以先从下面几个维度来比较:

维度 RabbitMQ RocketMQ Kafka
核心模型 Exchange 把消息路由到 Queue Topic 下划分消息队列,支持多种业务消息 Topic 下划分 Partition,按日志追加存储
比较擅长 灵活路由、任务分发 订单、支付等业务事件,事务消息、延迟消息 日志采集、行为数据、流式处理
顺序性 队列有入队顺序,处理顺序还要控制消费者并发和重投 按队列或消息组保证局部顺序,取决于版本和消费方式 分区内有序,跨分区不保证
重试方式 通过 nack、重投、死信等机制组合设计 提供消费重试和死信机制 通常由应用或消费框架设计重试流程
消息回溯 普通队列确认后消息会被删除;Streams 是另一种模型 保留期内可以重置消费进度 保留期内可以按 offset 重新消费
选型重点 路由规则、队列类型、可靠投递 业务特性、客户端版本、部署模式 分区规划、吞吐、保留时间、流处理生态

如果是日志采集、埋点分析,数据量大,还需要让多个系统反复读取历史数据,我会优先评估 Kafka。 它的分区日志模型和流处理生态比较适合这类场景。

如果是订单、支付、库存这些业务事件,而且需要事务消息、延迟消息、失败重试,我会优先评估 RocketMQ。 这些能力可以减少业务侧自己搭一套补偿流程的工作量。

如果重点是复杂路由、任务分发,比如一条消息要按不同规则投到多个队列,我会评估 RabbitMQ。 它的交换机和绑定机制很灵活,但也要结合队列类型、消息规模来做压测。

ActiveMQ 也会出现在一些已有的 JMS 系统里,这时候需要先区分 ActiveMQ Classic 和 Artemis,再评估协议兼容和迁移成本,不能把它们当成完全一样的产品。

最后还要看团队会不会运维、故障能不能排查、有没有监控和容灾能力。选型是选一套能支撑业务的方案,不能把「金融业务」直接等同于某一种 MQ,也不能把 Topic 或队列数量当成固定上限。

# 消息队列使用场景有哪些?

  • 解耦:可以在多个系统之间进行解耦,将原本通过网络之间的调用的方式改为使用MQ进行消息的异步通讯,对于允许异步完成的操作,可以考虑使用 MQ 进行不同系统之间的联系,这样可以降低系统之间的直接依赖。一个下游暂时故障时,消息先积压在 MQ 里,主流程不必等待它;但积压仍然会消耗容量,需要监控和处理。
  • 异步:假如客户创建订单后,还要记录客户轨迹、发通知、更新统计信息,如果主流程挨个调用这些服务,用户就得一直等。像记录轨迹这类允许延后完成的操作,可以交给 MQ 异步处理,加快主流程的响应速度。至于库存和关键订单状态是否也异步,要看业务的一致性要求,不能把所有步骤都一股脑扔到后台。
  • 削峰:一个系统访问流量有高峰时期,也有低峰时期,比如说,中午整点有一个抢购活动等等。比如系统平时流量并不高,一秒钟只有 100 多个并发请求,系统处理没有任何压力,一切风平浪静,到了某个抢购活动时间,系统并发访问剧增,比如达到了每秒 5000 个并发请求,而我们的系统每秒只能处理 2000 个请求,那么由于流量太大,我们的系统、数据库可能就会崩溃。这时如果使用 MQ 进行流量削峰,将用户的大量请求对应的消息直接放到 MQ 里面,然后我们的系统按自己的最大消费能力去消费这些消息,就可以保证系统的稳定,只是可能要根据业务逻辑,给用户返回特定页面或者稍后通过其他方式通知其结果。

# 消息重复消费怎么解决?

先说结论:消息重复在实际系统里很常见,通常靠消费者做业务幂等来解决。

为什么会重复呢?我举两个最常见的情况:

  • 生产者重试:消息已经写到 Broker 了,但返回的确认在网络中丢了。生产者不知道有没有成功,就再发一次,这样可能产生两条内容相同的消息。
  • 消费者重投:消费者已经把积分加上了,结果还没来得及提交消费进度就宕机了。重启后 MQ 会再次投递这条消息,积分就可能被加两次。

有些 MQ 提供生产者幂等能力,比如 Kafka 的幂等生产者,可以减少发送重试产生的重复。但它不能识别业务代码主动发出的两条相同订单事件,也不能替消费者保证 MySQL 只更新一次。

所以消费端要给操作设计一个稳定的业务幂等键,比如「订单号 + 发积分」或者支付流水号。重试时继续用这个键,不能每次重新生成 UUID。

以支付成功后发积分为例,可以在数据库里建一张消费记录表,对「消费方 + 业务幂等键」加唯一索引。消费时,把插入消费记录和增加积分放在同一个本地事务里:插入成功才加积分,已经有记录就说明处理过了,直接确认消息。

这里不能只做「先查有没有,再执行业务」,因为两个消费者可能同时查到没有,然后都去加积分。真正挡住并发重复的是唯一约束,而同一个事务保证了去重记录和业务结果一起成功、一起失败。遇到重复键时,要确认对应记录已经提交、确实代表处理成功;数据库异常则应回滚并重试,不能全部当成重复消息忽略。

总结一下: MQ 负责重投,业务负责幂等。我们要保证的,是同一笔业务只生效一次。

# 消息丢失怎么解决的?

使用一个消息队列,其实就分为三大块:生产者、Broker、消费者。要保证消息不丢,就得沿着这三个环节逐个检查。

img

第一,生产者端要确认消息真的发出去了。 同步发送要检查结果,异步发送要处理成功和失败回调,不能调用完发送方法就认为一定成功。遇到超时,结果可能是「没发成功」,也可能是「发成功了但确认丢了」,所以重试时要沿用业务幂等键。

还有一个容易漏掉的地方:订单已经写入数据库,但程序在发消息前宕机了。这时候 MQ 再可靠也没用,因为消息根本没到 MQ。需要用事务消息或者本地消息表,把这段发送意图保存下来,后续才能补发。

第二,Broker 端要配置好持久化和副本。 收到 ACK 到底代表什么,要看具体产品和配置。比如 Kafka 常见的可靠性配置是多副本、acks=all、合理设置 min.insync.replicas,并禁止不干净的 Leader 选举。RabbitMQ 则要结合持久化队列、持久化消息、Publisher Confirm,以及需要时使用仲裁队列。不能只说一句「部署了集群,所以不会丢」。

第三,消费者端要先把业务处理成功,再确认消息。 比如积分真正入库后再 ACK,或者再提交 offset。处理失败要进入重试流程,超过次数进入死信或异常记录,后续告警、补偿,不能捕获异常后仍然返回消费成功。

另外,消息保留时间、TTL、磁盘清理策略也要检查。消息一直积压,超过保留期被删除,业务一样会感知为丢消息。

总结一下: 生产端保证「有记录、可补发」,Broker 保证「持久化、有副本」,消费端保证「成功后再确认」,再配合监控和对账,才能形成完整的可靠投递流程。

# 使用消息队列还应该注意哪些问题?

除了消息可靠性和顺序性,我还会关注这几个问题:

  • 重复消费:重试、宕机恢复都会带来重复,消费者要做幂等。
  • 一致性和时效性:主流程成功不代表下游已经完成,要明确业务能接受多长时间的延迟。
  • 积压和过期:关注最老消息的等待时间、保留期和磁盘空间,不能只盯着消息条数。
  • 重试和死信:暂时性故障可以重试,永久性错误要隔离,死信消息还得有人处理。
  • 消息格式兼容:生产者和消费者不会同时发版,新增字段尽量向后兼容,不能随意改字段含义。
  • 排查能力:消息里带业务 ID、事件 ID、发生时间和 traceId,方便串起发送、接收、处理的完整过程。

这些问题最好在上线前设计好,等线上积压了几百万条消息再补方案,处理起来会很被动。

# 消息队列的可靠性、顺序性怎么保证?

这两个问题要分开看:可靠性回答的是「消息能不能最终处理到」,顺序性回答的是「处理的先后有没有乱」。

可靠性前面已经讲过,核心就是生产者确认、Broker 持久化和副本、消费者成功后再确认,再加上重试和补偿。需要注意,RabbitMQ 的队列持久化用 durable=true,普通队列里的消息持久化则通过消息属性设置,比如 delivery_mode=2,这是两回事。Kafka 提交 offset 也只是更新消费进度,并不会马上删除消息。

顺序性可以拿订单来理解。一个订单先创建、再付款、最后完成,这三条消息必须按这个顺序处理;但订单 A 和订单 B 通常不需要互相排队。

所以一般保证的是同一个业务键的局部顺序,而不是整个系统的全局顺序:

  1. 生产者有序发送:同一个订单的事件先后要明确。两个服务同时发消息,不能指望 MQ 根据时间戳自动判断谁应该先到。
  2. 发送到同一条有序通道:比如 Kafka 的同一分区、RocketMQ 的同一队列或同一消息组。
  3. 消费者串行处理:读取顺序正确,不代表扔进线程池后完成顺序也正确。同一业务键的消息要串行,不同业务键可以并行。
  4. 失败重试不能越过去:付款消息失败时,不能让完成消息先更新业务状态。需要阻塞这条有序通道,或者通过业务状态机、版本号处理乱序。

全局顺序通常意味着更少的并行度,吞吐量会受限制。面试里先说清楚「谁和谁之间需要有序」,比直接说「用单线程」更有价值。

# 如何保证幂等写?

幂等性是指 同一操作执行多次,对系统状态的影响与执行一次相同。比如同一笔支付通知来了三次,最终只能给用户加一次积分。

常用方案可以按业务类型来选:

第一种,插入类业务,用唯一约束。 比如按订单号创建发货单,对订单号加唯一索引,重复插入会被数据库挡住。如果一次消费要改多张表,就把消费记录和这些业务写入放在同一个事务里。

第二种,状态流转,用条件更新。 比如把订单从「待支付」改成「已支付」:

UPDATE orders
SET status = 'PAID'
WHERE order_id = ? AND status = 'UNPAID';

第一次更新成功,后面重复执行影响行数就是 0。附带的后续操作只能由成功改变状态的那次请求触发;如果还要发消息,可以在同一事务里写本地消息表。其他状态导致的 0 行更新,还要按业务区分,不能一律当成成功。

第三种,累加类业务,用业务流水去重。 积分 = 积分 + 100 本身不幂等,单纯加乐观锁也不代表同一笔加积分不会再执行。需要对积分流水的业务幂等键加唯一约束,再和积分更新一起提交。

Redis 的 SET NX 可以用来挡并发请求,但要考虑两个窗口:先记录再执行业务,中途宕机会留下「记录存在、业务没做」;先执行业务再记录,中途宕机会导致重试重复执行。数据库业务一般优先依靠数据库事务,Redis 可以作为辅助防重。

分布式锁也一样,它解决的是「同一时刻谁能执行」,锁释放后重复消息仍然能再次执行,所以锁不能单独代替幂等记录。如果调用的是外部支付、短信接口,还要把幂等键传给下游,或者通过流水查询、对账来确认结果。

# 如何处理消息队列的消息积压问题?

消息积压,本质上就是生产速度持续超过消费速度。但处理时不能一上来就加机器,要先确定堵在哪儿。

第一步,先定位。 看是所有队列都积压,还是某一个队列积压;生产量有没有突增,消费者有没有掉线;再看单条消息耗时、失败率、数据库慢查询和下游接口超时。如果是错误消息反复重试,扩容只会把错误放大。

第二步,提升真正的消费能力。 能优化 SQL、减少远程调用,就先优化;业务允许的话,可以批量处理。消费者扩容则要看消费模型:Kafka 的普通消费者组受分区数限制,RocketMQ 传统客户端的队列分配模式受队列数限制。下游数据库已经满负荷时,还要先限流或扩容下游。

第三步,再评估是否需要临时转发。 对没有顺序要求、可以并行处理的消息,可以消费旧 Topic,转发到一个有更多队列或分区的新 Topic,再由更多业务消费者处理。但转发程序必须在新 Topic 发送确认成功后,才确认旧消息,否则就可能丢消息;转发过程中也可能重复,所以业务幂等键必须保留。

这里有个容易答错的地方:新增分区或队列,不会自动把原来的积压消息拆过去。 新增资源主要帮助后续消息,旧消息还在旧分区或队列里。而且重分发会增加 Broker 的读写压力,也可能打乱顺序,不能当成万能方案。

恢复时间也可以估一下:假设积压 100 万条,恢复后每秒消费 5000 条,但每秒还在新增 3000 条,那实际每秒只能消掉 2000 条积压,大约需要 500 秒,而不是 200 秒。

最后还要盯住最老消息的时间和消息保留期。对于订单、支付这些关键消息,不能直接跳过 offset;对于业务明确允许丢弃的非关键数据,才可以评估跳过或归档。处理完之后,再针对根因补监控和容量规划。

# 如何保证数据一致性,事务消息如何实现?

这个问题主要是在问:本地数据库更新和发送 MQ 消息,是两个独立操作,怎么避免只成功一半?

一条普通 MQ 消息,大概会经过下面这些环节:

image-20250407142107477

  1. 生产者发送消息,Broker 接收并按配置存储。
  2. Broker 返回发送结果,消息进入可消费状态。
  3. 消费者获取消息,执行自己的业务逻辑。
  4. 业务成功后,消费者确认消息或提交消费进度。

注意,Kafka、RocketMQ 通常按保留策略清理消息,消费成功不会立即删除消息;也不是所有 MQ 都由 Broker 主动 push 给消费者。

我们举个下订单的例子。订单创建成功了,程序却在发消息前宕机,库存系统就不知道这笔订单。反过来,先发消息再创建订单,消息发成功了但订单回滚,库存系统又会处理一个不存在的订单。

RocketMQ 可以用事务消息解决本地事务和消息发送的一致性,下面这张图展示的就是半消息流程:

image-20250407142122992

  1. 生产者先发送半消息,Broker 保存下来,但暂时不给消费者看。
  2. 半消息发送成功后,生产者执行本地数据库事务。
  3. 本地事务成功,通知 Broker Commit;明确回滚,通知 Broker Rollback;结果不确定则保留 Unknown。
  4. Broker 收到 Commit,才让下游消费;收到 Rollback,则不再投递这条半消息。
  5. 如果迟迟没收到确定结果,Broker 会回查生产者,由生产者查询持久化的事务状态,再告诉 Broker 最终结果。

回查不能靠进程内存里的标记,要能在服务重启后查到本地事务结果。比如把事务流水和订单写入放在同一个数据库事务里,回查时按业务 ID 查这张表。

事务消息的目标是让「本地事务结果」和「消息是否可投递」保持一致,并不保证下游业务一定成功。 这还依赖事务状态可回查、回查能在限定时间内完成。库存系统仍然需要重试、幂等和补偿,系统才能达到最终一致性。

如果用的 MQ 没有这类事务消息能力,也可以用本地消息表,也就是 Outbox 方案。Kafka 的事务主要解决 Kafka 内部消息和消费进度的原子提交,不能直接把普通 MySQL 事务一起包进去。

# 本地消息表怎么保证数据库和 MQ 的一致性?

本地消息表的思路很直接:先把「要发消息」这件事和业务数据一起写进数据库,再慢慢投递。

比如创建订单时,在同一个数据库事务里做两件事:插入订单,插入一条待发送事件。这样事务成功,两条记录一起存在;事务失败,两条记录一起回滚,不会出现订单成功却没有发送记录的情况。

事务提交后,由后台任务扫描待发送记录,发送到 MQ。收到发送成功的确认后,再把记录标记为已发送。也可以通过 CDC 捕获这张表的变更来投递,但核心仍然是同一个本地事务。

如果发送成功了,但更新「已发送」状态前宕机了怎么办? 下次扫描会再发一次。所以这个方案提供的是可重试的投递,消费者仍然要幂等,事件 ID 在重发时也不能变。

多个发送任务同时扫描时,要通过领取状态、锁或租约避免无序抢任务,并给卡住的任务设置恢复机制。如果还要求同一订单的消息有序,就要按订单和事件序号控制发送顺序。

最后,待发送记录超过一定时间要告警,失败要重试,成功记录要定期归档。本地消息表把「可能漏发」变成了「有记录可以补发」,最终一致性还需要下游消费和对账配合。

# 最多一次、至少一次、恰好一次有什么区别?

这三个词说的是消息投递或处理的语义,我会这样理解:

  • 最多一次(At Most Once):允许消息没处理到,但不会为了保证处理而重投。比如先提交消费进度再执行业务,中途宕机就可能漏处理。
  • 至少一次(At Least Once):为了不漏处理,失败会重试,所以可能重复。比如先执行业务再提交进度,中间宕机,业务就可能再执行一次。
  • 恰好一次(Exactly Once):在明确的范围内,处理结果只生效一次。这里一定要说明范围,不能看到 MQ 支持这个词,就认为外部数据库和支付接口也自动获得了这个保证。

为什么普通 ACK 很难同时做到不丢又不重?因为「业务提交」和「通知 MQ」之间总有一个宕机窗口:先确认可能漏业务,后确认可能重复业务。

所以业务系统里比较常见的做法是至少一次投递 + 消费幂等。消息可能重复到达,但同一笔订单、同一笔流水只生效一次。

# 消费失败怎么重试,死信消息怎么处理?

先区分失败原因。数据库短暂超时、下游暂时不可用,这类故障可以重试;消息格式错误、字段缺失、业务规则明确不允许,这类错误一直重试通常也不会成功。

重试要有间隔、有次数上限。 比如逐渐拉长等待时间,再加一点随机抖动,避免下游恢复时所有消息同时冲过去。不要在消费线程里无限循环,更不要失败后马上重新入队形成空转。

超过次数或者确认是永久性错误,就转到死信队列或异常表,保留原消息、业务 ID、失败原因和重试次数,同时发出告警。死信只是把问题隔离出来,后面还要修复数据或代码,再有控制地重放。

如果重试 Topic 是自己实现的,要先确认失败消息已经可靠写入重试 Topic,再提交原消费进度;不然原消息确认了,重试消息又没发出去,就会漏处理。

还有个取舍:转到重试队列后继续处理后续消息,可能破坏顺序。需要严格有序的业务,就要阻塞对应的业务键,或者通过状态机校验。不能既说失败随便跳过,又说后面的消息严格有序。

# 怎么判断消息队列是否健康,需要监控哪些指标?

我会沿着生产者、Broker、消费者三个环节看,而不是只看 MQ 进程有没有启动。

  • 生产者:发送失败率、超时率、重试次数、发送确认耗时,以及本地消息表里待发送记录的数量和年龄。
  • Broker:磁盘空间、读写延迟、网络、复制延迟、可用副本数,以及 Kafka 的 ISR 是否频繁缩减。
  • 消费者:消费速率、处理耗时、失败率、重试和死信数量、消费者是否频繁掉线或重新分配任务。
  • 业务时效:积压条数、最老未处理消息的等待时间,以及从事件产生到业务完成的总耗时。

比如积压 1 万条,消费速度每秒 10 万条,可能很快就能清完;但一个分区只积压 100 条,最老的消息已经等了半小时,反而可能是某个订单流程卡住了。

所以告警要结合业务时限和积压增长趋势,关键业务再补对账,检查订单事件有没有对应的库存、积分或发货结果。

# 消息队列是参考哪种设计模式?

消息队列的发布订阅思路和观察者模式都能实现一对多通知,但依赖关系不同,可以举个生活例子:

  • 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是「公司」和「员工」是一个整体
  • 发布-订阅模式:某公司要给其他人发各种快递,因为「公司」和「其他人」是独立的,其唯一的桥梁是「快递」,所以这件事适合交给第三方快递公司解决

上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务。对应到代码里,观察者模式通常由主题直接维护观察者,发布订阅模式则增加一个消息中介,让发布者不必直接认识订阅者。是否异步、是否可靠投递,还要看具体实现。

观察者模式

观察者模式实际上就是一个一对多的关系,在观察者模式中存在一个主题和多个观察者,主题也是被观察者,当我们主题发布消息时,会通知各个观察者,观察者将会收到最新消息,图解如下:每个观察者首先订阅主题,订阅成功后当主题发送消息时会循环整个观察者列表,逐一发送消息通知。 img

发布订阅模式

发布订阅模式通过中间的发布订阅中心通知消息,发布者不直接维护订阅者列表,也不需要知道具体会通知给谁,因此有三个重要角色:发布者 -> 发布订阅中心 -> 订阅者。双方仍然要约定事件格式和含义,解耦的是直接调用关系。

图解如下:当发布者发布消息到发布订阅中心后,发布订阅中心会将消息通知给所有订阅该发布者的订阅者 img

# 让你写一个消息队列,该如何进行架构设计?

这种设计题,我会先明确需求:消息量有多大,允许多长延迟,要不要顺序、回溯、延迟消息,以及能接受什么样的丢失和重复风险。

设计时可以沿着生产者 -> Broker -> 消费者这条线往下拆:

  1. 消息模型:按 Topic 组织消息,再拆成多个分区或队列。生产者选择目标分区,不同消费者组各自维护消费进度,同组消费者分担任务。
  2. 通信协议:定义发送、拉取、确认等接口,考虑长连接、序列化、批量请求、超时、鉴权和限流。客户端还要能发现 Broker,并在故障后刷新路由。
  3. 存储模型:消息顺序追加到日志文件,配合分段文件和索引定位消息。明确刷盘策略、保留时间、容量上限,不能让消息无限增长。
  4. 可靠投递:生产者有发送确认和重试,消费者成功后才确认。确认丢失会带来重复,要明确业务幂等责任;数据库更新和消息发送的一致性,可以配合事务消息或本地消息表。
  5. 消费调度:记录消费者组成员和进度,消费者加入、退出时重新分配任务。消费慢时要有流控,失败时要有重试、死信和告警。
  6. 高可用:消息多副本复制,明确哪些副本确认后才能返回成功,以及故障后哪些副本有资格接管。协调组件本身也要高可用,避免出现两个节点同时接受同一分区的写入。
  7. 扩容:新消息可以分配到新增分区,已有分区的副本可以迁移到新 Broker。这里要区分「增加分区」和「迁移已有分区」,增加分区并不会自动重分布历史消息。
  8. 可运维性:补上消费延迟、磁盘水位、复制状态、死信监控,以及消息查询、重放和限流工具。

面试里不需要上来就把所有高级功能都设计出来。先把收发、存储、确认和故障恢复讲清楚,再解释顺序与并发、可靠性与性能之间的取舍,回答会更完整。

# RocketMQ

# 消息队列为什么选择RocketMQ的?

如果项目里选择 RocketMQ,我会结合业务需求来回答,主要是下面几个原因:

  • 业务特性比较齐全。像订单超时关闭需要延迟消息,本地事务和消息发送的一致性需要事务消息,消费失败需要重试和死信。这些能力与电商类业务比较贴近。
  • 支持局部顺序和消息过滤。比如同一订单的状态事件要顺序处理,不同类型事件又可以按 Tag 过滤,不必所有消息都交给业务代码判断。
  • Java 技术栈比较容易接手。团队熟悉 Java,排查客户端和 Broker 的问题、阅读源码会更方便,但这只是团队成本的优势,不代表 Java 开发的 MQ 就一定更可靠。
  • 具备分布式部署和扩展能力。通过多个 Broker、消息队列和副本支撑业务规模,具体吞吐量和可靠性要结合刷盘、复制配置做压测。

面试里最好把选型落到具体需求上,比如「订单流程需要事务消息和延迟消息,团队也熟悉 Java」。只说「大厂用过、吞吐高」,还不足以解释为什么适合自己的项目。

# RocketMQ和Kafka的区别是什么?如何做技术选型?

这两个都能做可靠的消息传递,区别主要在于消息模型、业务特性和生态侧重点,不能简单理解为「Kafka 快但会丢,RocketMQ 慢但不会丢」。

先看 Kafka。 它把 Topic 拆成 Partition,每个分区都是可回溯的追加日志,不同消费者组独立记录 offset。配合批量、压缩、Page Cache 等机制,比较适合日志采集、行为数据和流式处理。

Kafka 支持幂等生产者和事务,但事务主要用于 Kafka 内部的消息生产、消费进度提交。像订单延迟关闭、消费失败后按次数重试这些业务需求,通常还要结合应用或框架实现。

再看 RocketMQ。 它围绕业务消息提供事务消息、延迟消息、顺序消息、消费重试和死信等能力。典型的本地存储模型是 CommitLog + ConsumeQueue,Java 技术栈也方便一些团队排查问题。

可靠性上,两者都要看配置。Kafka 要考虑 acks、ISR 和最小同步副本数;RocketMQ 要考虑刷盘、复制以及部署模式。消息确认之后到底能抵御什么故障,要把条件说清楚。

那怎么选呢?

  • 日志量大,需要流式计算、历史数据反复消费,我会优先评估 Kafka。
  • 订单、库存、支付等业务事件,需要事务消息、延迟触发和内置重试,我会优先评估 RocketMQ。
  • 两种场景都有,也可以按业务分别使用,不必强行只选一种。

至于谁的性能更好,要在相同消息大小、副本数和可靠性要求下比较。不能只凭一个单机 QPS,或者一个零拷贝技术,就推出所有场景下的结论。

# RocketMQ 延迟消息的底层原理是什么?

延迟消息的核心是:消息先存起来,到指定时间后再变成消费者可见的普通消息。 这里要区分传统的延迟等级机制和 5.x 的定时消息机制。

img

传统 4.x 的延迟等级机制,可以结合上图理解:

  1. 生产者设置延迟等级,Broker 保存原 Topic 和队列等信息,把消息转存到内部延迟 Topic SCHEDULE_TOPIC_XXXX。
  2. 不同延迟等级对应不同队列,ScheduleMessageService 按消费进度检查队头消息的到期时间。
  3. 到时间后,恢复原 Topic 等信息,把消息重新写入正常的投递路径。
  4. 原 Topic 的消费者才能获取并处理消息。

这种方式通常是配置好的固定延迟等级,比如几秒、几分钟,并不是任意指定一个时间都能直接对应。

5.x 的定时消息可以指定投递时间戳,在支持该能力的 Broker 配置下,底层通过定时存储、时间轮等机制组织待到期消息,到期后再进入正常消费流程。要结合服务端能力、Topic 消息类型和客户端版本使用,不能只升级 SDK 就认为一定支持。

还有一点:到期代表可以投递,不代表业务必然在那一毫秒完成。 Broker 调度、网络和消费积压都可能带来延迟,所以订单关闭这类业务仍然要检查数据库里的实际状态和截止时间。

# 用延迟消息关闭订单,怎么避免把已支付订单关掉?

可以在创建订单时发送一条延迟消息,等到支付截止时间后,消费者收到消息,再去尝试关闭订单。

但这条消息表达的是「到时候检查一下」,不能直接代表「订单一定没支付」。用户可能已经付了钱,消息也可能因为积压晚到,所以最终要以数据库里的订单状态为准。

关闭时,可以用条件更新把「待支付」改成「已关闭」,同时校验截止时间:

UPDATE orders
SET status = 'CLOSED'
WHERE order_id = ?
  AND status = 'UNPAID'
  AND expire_time <= CURRENT_TIMESTAMP;

支付回调也要通过条件更新推进状态。支付和关单并发时,由数据库的状态条件保证只有符合当前状态的操作才能成功,不能先查状态再无条件更新。

关闭成功后,释放库存的动作也要幂等。如果库存是独立服务,可以在关单事务里记录待发送的释放事件,再可靠投递。

还有个边界:支付渠道可能扣款成功,但回调到达时订单已经关闭。这时候要按业务规则查询支付结果、退款或人工补偿,不能强行把关闭状态覆盖成已支付。

最后保留定时扫描作为兜底,处理延迟消息漏发、投递延后等异常。这样消息负责触发,订单状态机负责正确性,对账和补偿负责收尾。

# RocketMQ 怎么处理分布式事务?

RocketMQ 的事务消息可以帮助业务实现最终一致性,但它不会把多个服务的数据库操作变成一个同时提交、同时回滚的大事务。

假设 A 服务扣款 100 元,B 服务到账 100 元,我们希望的是:A 确认扣款成功后,可靠地通知 B 去入账。

img

流程可以这样理解:

  1. A 先向 Broker 发送半消息,消息里包含转账流水号和 B 要入账的信息。此时 B 还不能消费。
  2. 半消息发送成功后,A 执行本地事务,把扣款和转账流水一起提交。
  3. 本地事务成功,A 返回 Commit,Broker 让消息对 B 可见;明确回滚,A 返回 Rollback。
  4. 如果 A 宕机或者提交结果丢了,Broker 会发起事务回查。回查根据持久化的转账流水判断成功、失败或仍不确定,不能只查内存标记。
  5. B 收到消息后,用转账流水号做幂等,完成入账后再确认消费。

如果 A 扣款成功,但 B 一直入账失败,怎么办? 这不一定是代码问题,也可能是数据库故障、账户被冻结、业务校验不通过。暂时性故障可以重试,永久性故障则要记录异常、告警,并按业务规则决定修复后重放还是执行退款等补偿。

事务回查、消费重试都有边界,不能认为系统会无期限替我们兜底。因此还要通过转账流水对账,找出「已扣款、未到账」的记录。

总结一下: 事务消息解决 A 的本地事务与消息投递的一致性;B 的幂等、重试和补偿解决下游处理问题。两边配合,才能让业务最终收敛到一致状态。

# RocketMQ消息顺序怎么保证?

消息有序,一般指同一个业务对象的事件按先后顺序处理。比如同一订单要先创建、再付款、最后完成;不同订单之间可以并行。

传统 RocketMQ 客户端里,可以通过下面几个环节来保证:

  • 发送端先有序:同一订单的消息按业务顺序发送,重试也不能让后一条越过前一条。多个生产者并发发送同一订单事件时,要先在业务侧确定顺序。
  • 选同一个队列:比如根据订单号选择 MessageQueue,让同一订单的消息进入同一队列。队列数量变化时,原来的取模映射可能改变,需要评估迁移方案。
  • 使用顺序消费:传统 Java 客户端使用 MessageListenerOrderly。集群消费时,Broker 的队列锁配合客户端的串行控制,避免同一队列被多个消费者同时乱序处理。
  • 失败时保住顺序:顺序消费失败通常要先暂停当前队列并重试,不能随意把失败消息扔到另一个队列,然后继续处理同一订单的后续事件。

5.x 的顺序消息还引入了消息组(MessageGroup),可以把订单号作为消息组,表达同一组消息的顺序要求。具体发送和消费 API 要与客户端版本匹配,不能把传统队列锁机制套到所有消费方式上。

顺序消息的代价是,某一条处理慢或一直失败,可能阻塞后续消息。所以尽量只对确实需要有序的业务键保证顺序,并用状态机校验作为业务兜底。

# RocketMQ怎么保证消息不被重复消费?

RocketMQ 的消息也可能重复,不能靠它的消费成功状态替业务保证幂等。

比如消费者已经把款项入账了,却在返回消费结果之前宕机。Broker 不知道业务已经成功,就会再次投递。同一条业务消息如果被生产者主动重发,也可能有不同的消息 ID。

所以我会用订单号、支付流水号等业务标识做幂等,而不是完全依赖 Broker 生成的 messageId。比如「转账流水号 + 入账」加唯一约束,并把入账流水与余额更新放在同一个数据库事务里。

如果幂等记录已经存在,说明这笔业务已经处理成功,可以返回消费成功;如果数据库事务失败,则返回失败让消息重试。外部接口调用还要结合下游幂等键和结果查询。

核心还是前面那句话:允许消息重复到达,但同一笔业务只能生效一次。

# RocketMQ消息积压了,怎么办?

我会先看消费模式,再判断积压原因。不能看到积压就认定「多加几台消费者一定有效」。

第一步,确认到底堵在哪个环节。 是生产流量突然增大,还是消费者掉线、下游接口超时、数据库变慢?是所有队列都积压,还是某个订单号形成热点?先看消费耗时、失败率、线程池和下游容量。

第二步,根据客户端和负载均衡方式扩容。 在传统客户端的集群消费、队列分配模式下,同一消费组的一条队列同一时刻分配给一个消费者实例。比如总共 4 条队列,已经有 4 个实例,再加实例就可能分不到队列。但一个实例内部仍然可以有多个消费线程,所以不能说「4 个实例就达到所有场景的吞吐上限」。

5.x 的部分消费方式支持消息粒度的负载均衡,同一队列的不同消息可以分配给多个消费者。因此,队列数与消费者实例数的限制,要结合实际客户端和模式来回答。顺序消息仍然要遵守消息组的顺序约束。

第三步,优化或隔离耗时业务。 调整并发、批量处理、优化 SQL 都要结合下游容量。若下游已经被压满,消费者无限加线程只会增加超时和重试。

对于无需顺序、旧队列确实限制处理能力的海量积压,可以评估转发到多队列的临时 Topic。但要保留业务幂等键,确认新 Topic 写入成功后才提交旧进度,同时评估 Broker、数据库的额外压力。新增队列不会自动搬走旧消息,也不能保证扩容十倍就快十倍。

处理期间还要观察最老消息时间、消息保留期和磁盘水位。关键业务不能直接跳过消费进度,恢复后要对账,确认重试队列、死信队列里的消息也得到处理。

# RocketMQ 的 CommitLog、ConsumeQueue 和 IndexFile 有什么作用?

可以把这三者理解成正文、目录和查询索引。

CommitLog 存正文。 一个 Broker 上不同 Topic、不同队列的消息,通常一起顺序追加到 CommitLog 的分段文件中,消息体和消息属性都在这里。这样写入路径比较集中。

ConsumeQueue 存消费目录。 每个 Topic 的每条队列有自己的逻辑索引,记录消息在 CommitLog 里的位置、大小等信息。消费者按队列的逻辑 offset 找到索引,再去 CommitLog 读取消息。

IndexFile 用于按 Key 查消息。 比如按订单号辅助查询相关消息,它不是正常顺序消费必须经过的路径,也不能代替业务数据库的唯一约束。

所以,RocketMQ 的队列并不是每条队列各存一份完整消息体。正文集中写,消费目录按队列组织,两者承担不同职责。

不过,集中写入也不代表 Topic 可以无限增加,ConsumeQueue、索引、路由元数据和后台处理一样会消耗资源。具体容量仍然要压测。

# RocketMQ 的 NameServer 是干什么的,挂了会怎样?

NameServer 主要负责路由发现:某个 Topic 有哪些队列,这些队列在哪些 Broker 上,客户端要到哪里发消息、取消息。

Broker 会定期向 NameServer 注册和发送心跳,客户端从 NameServer 获取路由并缓存。普通 NameServer 的路由服务是无状态的,各节点之间不需要像数据库副本那样复制同一份日志。

如果只挂一个 NameServer,客户端通常还可以从其他节点获取路由。如果全部挂了,已有客户端在缓存路由仍有效、Broker 地址没变的情况下,可能继续收发一段时间;但新客户端、首次访问新 Topic,以及 Broker 故障后的路由刷新会受影响。

还有一点:普通 NameServer 不是 Broker 自动选主的控制器。 RocketMQ 是否能够自动切主,取决于部署模式,比如 DLedger 或 Controller 模式;Controller 可以单独部署,也可以嵌入 NameServer,但它和路由服务承担的是不同职责。

# RocketMQ 的集群消费和广播消费有什么区别?

传统客户端里,这两种模式最大的区别是:同一条消息,是让组内某个实例处理,还是让每个实例都处理。

集群消费:同一个消费组里的实例分担消息,一条消息通常交给其中一个实例处理,失败时仍可能重复投递。适合订单处理、积分发放,不能让每台机器都给用户加一次积分。

广播消费:同一个组里的每个实例都要消费这条消息,适合各实例更新本地缓存这类场景。在传统客户端中,广播消费的消费进度和失败重试方式与集群模式不同,不能默认认为都有 Broker 端的重试和死信兜底。

如果库存和积分两个系统都需要处理订单支付事件,通常应该建立两个不同的消费组,每个组内部用集群消费。这样两个业务都能收到事件,各自还可以通过多个实例分担任务。

不同版本和客户端支持的模式有差异,尤其使用 5.x 新客户端时,要先确认能力,不能照搬传统广播消费配置。

# Kafka

# 对Kafka有什么了解吗?

Kafka 可以理解为一个分布式、可持久化、可回溯的消息日志系统,常用于日志采集、行为数据和流式处理,也能承载业务事件。

它的几个核心概念可以串起来说:

  • Topic:消息按主题分类,比如订单事件、用户行为。
  • Partition:Topic 拆成多个分区,每个分区是一份追加日志,分区内消息有自己的 offset,也是并行消费的基本单位。
  • Broker:负责存储和收发消息的服务器,一台 Broker 可以承载多个分区的副本。
  • Leader 和 Follower:分区通过多副本实现容灾,Leader 处理写入,Follower 复制数据;副本能否接管,要看是否满足选举条件。
  • Consumer Group:组内消费者分担分区,组与组之间各自维护进度,因此同一份数据可以给多个业务独立消费。

Kafka 的特点是吞吐能力强、支持批量和压缩、可以按保留策略保存历史数据,并通过分区和副本扩展容量。

但不能直接说「有 n 个副本就能挂 n-1 台机器」。消息有没有丢、分区还能不能读写,还要看 ISR、最小同步副本数、选举配置,以及 KRaft 控制器是否保有多数派。

# Kafka 为什么这么快?

Kafka 快,主要是因为它把存储和网络传输里的很多小操作,变成了顺序、批量、可复用的操作。

第一,追加写日志。 消息按分区顺序追加到日志文件,减少随机寻道和零碎写入,比大量随机更新更容易获得稳定吞吐。SSD 上同样能受益于批量和连续访问。

第二,利用 Page Cache。 写入先经过操作系统的页缓存,由操作系统管理刷盘;消费者读取近期消息时,也可能直接命中缓存,减少实际磁盘读取。可靠性还要由副本和确认配置配合,不能把页缓存当成断电后仍然存在的磁盘。

第三,批量收发。 生产者通过 batch.size、linger.ms 等参数组织批次,消费者批量拉取,把网络请求、协议处理和 I/O 的成本分摊到多条消息上。但等待凑批会增加延迟,要按业务目标调整。

第四,减少数据拷贝。 在适用的传输路径中,可以通过 sendfile 等机制把文件数据传给网络,减少用户态和内核态之间的拷贝。开启 TLS 等情况会影响这条优化路径,不能说任何场景都完全零拷贝。

第五,批量压缩和分区并行。 压缩减少网络、存储的数据量,多分区把负载分散到多个 Broker 和消费者。但压缩也消耗 CPU,分区也不是越多越好。

所以,Kafka 的高吞吐是这些机制一起作用的结果。具体能跑多少 QPS,要看消息大小、硬件、批量参数、副本和可靠性配置。

# Kafka 的模型介绍一下,Kafka 是推送还是拉取?

Kafka 的普通消费者采用拉取(pull)模型,由客户端主动向 Broker 请求消息,比较适合消费者自己控制批量和处理节奏。

推送模型和拉取模型

Push 是 Broker 主动投递,Pull 是消费者主动请求。Push 可以通过 ACK 和 prefetch 做可靠性和流控,Pull 则便于消费者自己控制拉取节奏,两种模式各有适合的场景。

Kafka 的客户端通过 poll() 等 API 获取数据,底层发送 Fetch 请求。如果暂时没有足够数据,Broker 可以等待到指定条件或超时再响应,避免消费者不停发空请求。这个等待由 Fetch 相关配置和客户端调用共同控制,不只是一个 poll 超时参数。

消费进度

img

每个消费者组独立维护各分区的已提交 offset,通常保存在 Kafka 的内部主题 __consumer_offsets 里,不是只存在消费者自己的内存中。

比如积分组处理到 offset 3,风控组处理到 offset 6,它们互不影响。保留期内还可以通过重置 offset 重新处理历史数据,但要评估业务幂等和重复触发的影响。

普通读取只能看到高水位以内、满足复制提交条件的数据。消费事务消息时,如果设置 read_committed,还会受事务提交状态和 LSO(最后稳定偏移量)的限制,不能把所有已写入 Leader 的消息都当成马上可见。

消费者组

img

在普通消费者组模型里,同一时刻一个分区分配给组内一个消费者实例,一个实例可以负责多个分区;不同消费组则可以独立消费同一分区。

所以组内体现的是分担任务,组间体现的是同一份消息服务多个业务。消费者加入或退出后,会重新分配分区,让剩余实例接手任务。

# Kafka 如何保证顺序读取消息?

Kafka 保证的是单个分区内按日志顺序读取,不保证一个 Topic 所有分区的全局顺序。业务上要有序,还得把发送、分区选择和处理都串起来。

比如同一订单的创建、付款、完成事件,可以把订单号作为 Key,让它们在分区数量和分区策略不变时进入同一个分区。

发送端要先确定事件顺序,并考虑重试。关闭幂等且允许多个请求同时在途时,前一批失败重试,后一批先写入,就可能乱序。启用幂等生产者后,还要使用满足要求的 acks、retries 和 max.in.flight.requests.per.connection 配置;不能简单把任何重试配置都说成有序。

消费端按分区串行处理最容易保证顺序。把同一分区的消息全部丢进线程池,虽然拉取顺序正确,处理完成顺序却可能乱掉。需要并发时,可以按业务键分配到固定的串行执行通道,同时只提交已经连续处理完成的消费进度。

扩分区时也要注意。 相同 Key 的映射可能改变,旧消息留在旧分区,新消息进入新分区,两个分区一起消费就可能打乱同一订单的顺序。

如果确实要全局有序,可以只用一个分区,并在生产端明确总顺序,但并行能力会受到限制。业务自己按序号重排也不是简单排序一下,还要处理缺失消息、等待超时和补偿;仅凭时间戳通常无法保证严格顺序。

# Kafka 消息积压怎么办?

Kafka 积压一般看 Consumer Lag,但还要结合处理耗时和消息年龄,确认是生产突然增加,还是消费能力下降。

如果所有分区都慢,优先排查消费者掉线、下游超时、慢 SQL、线程池阻塞,以及消费失败后的重试。如果分区还有空余,可以增加同组消费者实例;没有空余时,可以优化处理逻辑、批量写入,或者对不要求顺序的业务合理增加内部并行度。

如果只有个别分区慢,可能是某个 Key 流量过大形成热点,或者某条消息一直失败。单纯增加消费者实例,不能把这个已经积压的分区自动拆开。

新增分区主要影响后续消息,历史消息不会自动搬到新分区,所以不能把「增加分区数」当成直接清理旧积压的方案。必要时可以评估转发到新 Topic,但要处理发送确认、幂等、顺序以及额外读写压力。

调大拉取批量、优化压缩或增加消费者线程,都要看瓶颈是否真在这里,并保证处理时间与 max.poll.interval.ms 等配置匹配,避免因为处理太久又触发重新分配。

最后关注保留期,避免旧消息还没处理就被删除。恢复后检查 Lag 是否持续下降,再对关键业务做对账。

# Kafka为什么一个分区只能由消费者组的一个消费者消费?这样设计的意义是什么?

这里说的是 Kafka 普通消费者组的分区分配模型:同一时刻,同一组内,一个分区只分配给一个消费者实例。不同消费组可以同时读这个分区。

img

为什么这样设计?主要是让任务归属、消费进度和顺序控制都比较清楚。

如果同组两个消费者同时处理同一分区,就得额外协调「谁处理哪些 offset、谁能提交进度、某一条失败后怎么重试」。假设一个处理完 offset 20,另一个还卡在 offset 10,直接提交较大的进度就可能跳过没完成的消息。

以分区为单位分配后,一个实例负责自己的分区,组内无需为每条消息再抢一次任务,按分区顺序处理也更简单。这是用分区粒度换取协调成本和顺序管理上的简化。

不过,这不代表底层不允许多个独立客户端读同一分区,也不代表宕机、重平衡后绝不会重复消费。组内的任务分配和业务幂等,是两件不同的事。

# 如果有一个消费主题topic,有一个消费组group,topic有10个分区,消费线程数和分区数的关系是怎么样的?

这个问题要先区分 KafkaConsumer 实例数和业务处理线程数,这两个不是一回事。

在普通消费者组里,假设订阅的 Topic 一共有 10 个分区:

  • 只有 1 个消费者实例,它可以负责全部 10 个分区。
  • 有 5 个实例,合理分配时每个可以负责 2 个分区。
  • 有 10 个实例,可以做到每个负责 1 个分区。
  • 有 15 个实例,多出的实例会没有分区可分配。

img

所以,分区数限制的是同组能分到任务的消费者实例数。不是说业务线程池也只能开 10 个线程。

一个消费者实例拉取消息后,可以交给业务线程池处理,但同一分区要不要串行、什么时候提交 offset,都得设计好。KafkaConsumer 本身也不是线程安全的,不能让多个工作线程随意同时调用它。

至于 10 个实例是不是一定最快,要看消息量、单条处理耗时、Broker 和下游容量。消费者数与分区数相等是一种配置选择,不是吞吐量必然最大的公式。

# Kafka 的 offset 应该什么时候提交?自动提交有什么问题?

普通消费者组提交的 offset,表示下次恢复消费的位置。比如 offset 0 到 9 都处理完了,应提交 10,而不是 9。

最关键的原则是:先完成业务,再提交对应进度。 如果先提交 10,再去处理消息,中途宕机,重启会从 10 开始,前面没完成的业务就被跳过去了。

但反过来,业务成功后、提交前宕机,重启又会消费一遍。所以手动提交可以控制漏处理风险,仍然需要业务幂等。

自动提交并不知道业务数据库有没有提交。尤其把 poll() 到的消息交给异步线程池后,消费线程继续轮询,自动提交可能越过尚未完成的任务,因此不能把「自动提交」等同于「业务成功后提交」。

手动提交时也不能只看最大的已完成 offset。假设 0 到 4 已完成,5 失败,6 到 9 成功,只能推进到 5 这个待处理位置,不能直接提交 10。只有失败消息已经被可靠转存到重试路径,且业务允许后续消息先走时,才可以按设计推进。

commitSync 等待提交结果,commitAsync 通过回调处理结果,它们决定的是提交方式,都不会自动检查业务是否完成。

# Kafka 的 ISR 是什么,acks=all 就一定不丢消息吗?

ISR 是 In-Sync Replicas,同步副本集合,包括 Leader 和满足同步条件的 Follower。副本落后到一定程度,会被移出 ISR。

acks=all 表示一次写入需要满足 ISR 副本的确认条件,不是所有配置出来的副本都必须确认,更不是所有副本都已经物理刷盘。

举个例子:配置了 3 个副本,但两个 Follower 都落后,被移出 ISR,ISR 只剩 Leader。如果 min.insync.replicas=1,即使设置 acks=all,也可能只靠 Leader 就返回成功。此时 Leader 再故障,风险就很大。

关键消息常见的组合是:3 副本 + acks=all + min.insync.replicas=2 + 禁止不干净的 Leader 选举。ISR 少于 2 时宁可拒绝写入,也不降低到单副本确认。

这也体现了可靠性和可用性的取舍:同步副本不够时,拒绝写入会影响可用性;放宽条件可能增加丢失风险。即使配置合理,也不能覆盖所有副本同时损坏、错误删除等所有情况,还要配合监控、容灾和对账。

# Kafka 的 Rebalance 是什么,会带来什么问题?

Rebalance 就是重新分配消费者组里各实例负责的分区。比如新增消费者、原消费者退出或失联、订阅的 Topic 分区发生变化,都可能触发任务重新分配。

它的好处是消费者扩容、故障后,任务能重新交给存活的实例。代价是分区交接可能造成短暂消费停顿,如果之前完成的业务没提交进度,新实例接手后还可能重复处理。

传统 eager 分配方式通常会撤销全部分区再分配;cooperative 等方式可以减少不必要的分区交接。不同协议和版本的停顿范围不同,不能说所有 Rebalance 都一定让整个组完全停止。

如果业务处理太久,迟迟不调用 poll(),超过 max.poll.interval.ms,即使进程还活着,也可能被认为无法正常消费。频繁扩缩容、网络问题也会引起反复重平衡。

应对时,要控制每批消息数量和处理耗时,合理配置轮询与心跳相关参数,避免频繁扩缩容,并在分区交接时协调未完成任务和已完成进度。幂等依然是兜底,不能只靠提高超时时间掩盖消费卡住的问题。

# Kafka 的幂等生产者和事务,能保证业务恰好执行一次吗?

不能直接保证,要看你说的是哪一层。

幂等生产者主要处理生产者内部发送重试产生的重复。Broker 通过生产者 ID、分区序列号等信息判断重复批次。但业务代码主动再调用一次发送、重新创建生产者,不能指望它自动识别成同一笔订单事件。

Kafka 事务可以把多条 Kafka 消息的写入,以及消费进度提交,放到一个原子提交范围里。比如从 Topic A 读取数据,处理后写到 Topic B,同时用 sendOffsetsToTransaction 提交消费进度,配合 read_committed 读取已提交结果。

这适合 Kafka 内部的「读 -> 处理 -> 写」链路,但事务中途失败,处理代码仍然可能重新执行。保证的是相应范围内的结果语义,不是函数永远只调用一次。

如果业务是消费消息后更新 MySQL、调用支付或发短信,Kafka 事务不会自动把这些外部副作用一起提交。 数据库写入需要业务幂等、唯一约束或把消费进度与结果一起落库的方案;外部接口需要幂等键、结果查询和补偿。

所以面试里不要只说「开了事务就是 Exactly Once」,还要补一句:保证覆盖到哪里,外部系统如何配合。

# 消息中间件如何做到高可用?

消息中间件的高可用,我会拆成三件事:数据有副本、故障后能接管、客户端能找到新的服务节点。

第一,多副本。 单机持久化只能应对一部分进程重启问题,机器或磁盘损坏还需要其他节点保存消息。异步复制通常性能更好,但确认后、复制前的故障窗口可能丢消息;等待足够副本确认,可以提高可靠性,但会增加延迟,副本不足时还可能拒绝写入。

这里要区分「副本收到数据」和「数据已经刷盘」。同步复制并不天然等同于每个副本都同步刷盘,两个条件要分别看。

第二,故障检测和接管。 不同产品的实现并不完全一样:

  • Kafka 按分区管理 Leader 和副本,由控制器协调故障后的 Leader 切换。4.x 只支持 KRaft,控制器通过多数派维护元数据;分区数据复制本身不能直接当成同一套 Raft 投票机制。
  • RocketMQ 的传统主从、DLedger、Controller 等部署模式,切主能力和复制机制有差异。不能说普通 NameServer 会自动选主,也不能说传统 Slave 一定能无缝接管写入。
  • RabbitMQ 的仲裁队列通过多数副本保证队列可用性。普通集群里的经典队列不会因为加入了集群,就自动有消息副本。

第三,客户端恢复。 服务端切换之后,客户端还要刷新元数据或路由、重新连接并重试。这个过程中可能短暂不可用,也可能发生重复发送、重复消费,所以业务幂等仍然要保留。

部署上还要避免把所有副本放在同一故障域,比如同一台物理机,并监控复制延迟、剩余磁盘和协调节点状态。高可用的关键是明确能容忍什么故障、故障时怎样恢复,不能只回答「多部署几台机器」。

# Kafka 和 RocketMQ 消息确认机制有什么不同?

我会分成生产者发送确认和消费者处理确认两部分回答,这样不容易把存储可靠性和业务处理混在一起。

先看生产者端。

  • Kafka 通过 acks 控制写入确认:0 不等 Broker 确认,1 等 Leader 写入确认,all 要满足 ISR 的确认条件。再配合副本数和 min.insync.replicas,决定副本不足时是否还能写。ACK 不直接代表每个副本都物理刷盘。
  • RocketMQ 返回发送结果,确认条件取决于刷盘、复制和部署模式。传统主从要区分同步/异步刷盘、同步/异步复制;DLedger、Controller 等模式又有自己的确认条件。比如传统 Java 客户端还要检查刷盘或复制超时等状态,不能只看发送方法没有抛异常。

再看消费者端。 可以借用一个比喻:Kafka 更像「提交读书进度」,RocketMQ 的常见业务消费接口更像「报告这批任务的处理结果」。

  • Kafka 提交的是每个分区的下一条消费位置。如果 offset 99 失败、100 成功,直接提交 101 就可能跳过 99。重试、连续完成进度、失败消息转存等流程通常由应用或框架安排。
  • RocketMQ 传统并发消费监听器返回 CONSUME_SUCCESS 或 RECONSUME_LATER,失败后可以进入内置重试流程,超过限制进入死信。顺序消费的失败处理不同,5.x 的 PushConsumer、SimpleConsumer API 也有差异,不能把一个接口当成所有版本通用的 ACK。

还有个共同点:Kafka 和 RocketMQ 通常保留消息正文,确认消费主要推进消费状态,不会马上删除消息。

所以差异主要在确认条件和消费 API:Kafka 以分区进度为核心,RocketMQ 的业务接口集成了较多失败重试能力。两者都可能因为确认丢失而重复消费,最终还是需要业务幂等。

# Kafka 和 RocketMQ 的 Broker 架构有什么区别?

两者最大的区别,可以从存储模型、元数据管理、故障接管粒度三个方面来讲。

第一,存储模型不同。

  • Kafka 按 Partition 组织独立的日志,每个分区由多个日志段和索引文件组成。可以理解为每个分区有自己的「小本子」,消息顺序追加,副本也按分区管理。
  • RocketMQ 典型的本地存储模型是所有队列的消息集中追加到 CommitLog,再按 Topic、Queue 构建 ConsumeQueue 索引。可以理解为正文写进公共的「大本子」,各队列通过自己的目录找消息。

Kafka 的分区数量增多,会增加文件、元数据、复制和调度开销,但不能只凭 Topic 多就断定性能一定暴跌。RocketMQ 集中写正文,也不代表队列和 Topic 的索引开销可以忽略。实际要看活跃分区数、磁盘类型和负载。

第二,元数据与协调机制不同。

  • Kafka 的控制器管理 Broker、分区副本、Leader 等元数据。早期用 ZooKeeper,4.x 已经只支持 KRaft;控制器的 Raft 管的是集群元数据,不是让每份消息都经过控制器转发。
  • RocketMQ 的普通 NameServer 主要提供路由发现,Broker 上报路由,客户端获取并缓存。普通 NameServer 节点之间不依赖复制日志达成一致;自动切主则要看 DLedger、Controller 等部署模式,不能把选主职责都算到 NameServer 头上。

第三,故障接管粒度不同。 Kafka 以分区副本为单位切换 Leader,一台 Broker 上可以同时放多个分区的 Leader 和 Follower。RocketMQ 的传统主从以及 Controller 模式,通常围绕 Broker 副本组进行复制和角色切换。传统 Slave 能提供某些读取能力,不代表 Master 宕机后一定自动接管写入。

总结来说,Kafka 以分区日志为核心,RocketMQ 以公共 CommitLog 和队列索引为核心。比较架构时,把读写路径和故障恢复讲清楚,比单纯给它们贴上「大数据」或「电商」标签更有说服力。

# RabbitMQ

# RabbitMQ 和 AMQP 是什么关系?

可以用「协议和实现」来理解:AMQP 定义通信规则,RabbitMQ 是支持这些规则的消息中间件。

如果写过 Java,可以类比接口和实现类。AMQP 本身不是安装后就能收发消息的软件,它规定客户端和服务端如何通信;RabbitMQ 把相关协议能力落到服务里。

我们平时讲 RabbitMQ 的 Exchange、Queue、Binding、RoutingKey,以及 Channel、ACK,通常是在讲 AMQP 0-9-1 的模型。生产者先把消息发给交换机,交换机再按绑定规则路由到队列。

这里要注意版本:AMQP 1.0 和 AMQP 0-9-1 是不同的协议,不能把交换机和绑定模型当成所有 AMQP 版本都要求的统一结构。 RabbitMQ 还支持其他协议,不只是 AMQP。

所以面试里可以先说「AMQP 是协议,RabbitMQ 是消息中间件实现」,再补上常用的 AMQP 0-9-1 消息模型,关系就比较清楚了。

# RabbitMQ 核心组件有哪些?

RabbitMQ 的核心组件,都是消息从发出到消费必不可少的部分:

  • 首先是生产者(Producer),它就是发送消息的一方,比如我们的业务服务,负责把需要异步处理的消息,交给 RabbitMQ,不会直接去找消费者。

  • 然后是消费者(Consumer),它是监听并处理消息的一方,一直盯着队列,一旦队列里有新消息,就会取出来执行业务逻辑。

  • 接下来是队列(Queue),这是 RabbitMQ 里真正存储消息的地方,也是消息投递给消费者的载体。通常按入队顺序投递,但优先级、多个消费者和重投等因素会影响顺序。在手动确认模式下,取走消息不代表处理成功,Broker 还会跟踪未确认状态。

  • 然后是交换机(Exchange),生产者不会直接把消息发给队列,而是先发给交换机,它相当于一个消息分发器,会按照我们设定的规则,把消息路由到对应的队列里,比如下面会讲到的四种常见交换机类型。

  • 还有路由键(RoutingKey) 和绑定(Binding),绑定是把交换机和队列关联起来的纽带,路由键则是生产者发消息时带的标识,交换机会根据路由键和绑定的规则,判断消息该投到哪个队列。

  • 最后是连接(Connection) 和信道(Channel),Connection 是客户端和 RabbitMQ 之间的 TCP 长连接,而 Channel 是建立在连接之上的轻量级信道,因为 TCP 连接创建销毁开销很大,所以用多个信道来传输消息,能大大节省资源。

简单总结就是:生产者发消息给交换机,交换机通过路由键和绑定,把消息投到队列,最后消费者从队列取消息消费,整个流程就靠这些核心组件完成。

# RabbitMQ 有哪几种交换机类型?

RabbitMQ 里核心的交换机一共四种,分别是直连、扇形、主题和头交换机,其中前三种是工作中最常用的,头交换机基本很少用到。

  • 第一种是直连交换机(Direct),它是精准匹配的模式,消息会根据路由键完全一致,才转发到对应的队列。比如路由键设成 user.login,就只会发给绑定了这个相同键的队列,适合一对一、精准投递的场景。

  • 第二种是扇形交换机(Fanout),它就是广播模式,完全不看路由键,只要队列绑定了这个交换机,所有消息都会无脑发给所有队列。适合群发通知、多服务同步更新这类场景,效率很高。

  • 第三种是主题交换机(Topic),它支持路由键模糊匹配,路由键按点号分隔成词段,用 * 匹配一个词段、# 匹配零个或多个词段。比如 order.* 能匹配 order.pay,但不能匹配 order.pay.success;order.# 则可以匹配这两种。适合按业务主题分类、批量路由的场景。

  • 第四种是头交换机(Headers),它不看路由键,而是根据消息头里的键值对来匹配。比如通过 x-match=all 要求所有指定条件都匹配,或者用 x-match=any 让任意条件匹配即可。适合基于多个消息属性路由的场景。

# RabbitMQ 的特性你知道哪些?

RabbitMQ 的特点,我会围绕路由、可靠投递、流控和高可用队列来回答。

  • 路由灵活:通过 Direct、Fanout、Topic、Headers 等交换机,以及绑定关系,把消息发送到一个或多个队列。
  • 支持持久化:队列、交换机可以持久化,消息也有持久化属性。但普通队列设置 durable=true 只代表队列声明能恢复,不等于所有消息都已经持久化。
  • 两端都有确认机制:Publisher Confirm 用于发送端确认,消费者 ACK 用于确认处理结果,这两种确认互相独立。
  • 流控和失败处理:prefetch 限制未确认消息数量,nack、TTL、死信等能力可以组合成消费失败和重试方案。
  • 支持不同队列类型:经典队列适合一般队列场景,仲裁队列通过副本和多数派提高可靠性,Streams 则提供日志式存储和回溯能力。

比如普通持久化队列的持久化发送,可以这样写:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='durable_queue', durable=True)
channel.confirm_delivery()

# 无法路由或 Broker 拒绝发布时,BlockingChannel 会抛出相应异常
# 实际业务还要记录发送意图、处理异常,并按稳定的幂等键重试
channel.basic_publish(
    exchange='',
    routing_key='durable_queue',
    body=b'order event',
    properties=pika.BasicProperties(delivery_mode=2),
    mandatory=True,
)
connection.close()

队列持久化、消息持久化、发送确认,这三个要一起理解。重要消息还要考虑副本,不能只靠单机重启恢复。

另外,经典镜像队列已经在 RabbitMQ 4.0 被移除,讨论新版本高可用时,应该重点讲仲裁队列或 Streams,不能继续把镜像队列当成当前通用方案。

# RabbitMQ 的底层架构是什么?

RabbitMQ 的消息路径可以概括成:Producer -> Exchange -> Queue -> Consumer,客户端通过 Connection 和 Channel 与 Broker 通信。

img

Exchange 负责路由,Queue 负责保存和投递消息。默认交换机虽然可以让我们按队列名发送,但内部仍然走交换机路由,不是绕过交换机直接写队列。

消费者使用 basic.consume 时,Broker 主动投递消息;使用 basic.get 时,则是主动拉取。消费者确认、prefetch、重投等机制控制消息在处理过程中的状态。

队列的存储和复制要看队列类型:普通经典队列的消息通常放在某一个节点上,仲裁队列则在多个成员之间复制,并通过多数派维护安全的状态。

集群也要分清共享什么。 节点之间共享队列、交换机、绑定等元数据信息,并不代表每个节点都保存所有经典队列的消息。消费者连到另一个节点,可能只是由它转发请求到实际承载队列的节点。

所以讲 RabbitMQ 架构时,要把「路由模型」「队列存储」「队列复制」分开,不能只画三台 Broker 就认为消息已经高可用了。

# RabbitMQ 的可靠性保障怎么做?

RabbitMQ 的可靠性,也可以按生产者、Broker、消费者三个环节来讲。核心是可靠投递,同时通过业务幂等处理重复。

首先是生产者环节。 开启 Publisher Confirm,跟踪每条消息或批次的发送结果。还要处理无法路由的情况,比如 mandatory=true 配合 Return 回调,或者配置备用交换机。Confirm 和 Return 的含义不同,收到 Confirm 的 ACK 不一定说明消息已经进入业务队列。

发送超时或连接中断时,结果可能不确定,需要依靠发送记录补偿,重发沿用业务幂等键。RabbitMQ 的 AMQP 事务与 Confirm 不是在同一个 Channel 上叠加使用的保障机制,也不能自动把 MySQL 事务和 MQ 发布合并成一个事务。

然后是 Broker 环节。 重要拓扑要持久化,普通持久化队列中的消息也要设置持久化属性,并结合发送确认。需要抵御节点故障时,使用适合的副本队列,比如仲裁队列,并保证足够成员可用。普通集群本身不会自动复制每条经典队列的数据。

最后是消费者环节。 关闭自动确认,业务事务提交后再手动 ACK。暂时性故障安排有间隔、有上限的重试;无法处理的消息可以拒绝重入队,通过配置好的死信交换机转存。

这里不能把「不 ACK」理解为「马上重入队」。消息会先保持未确认状态,连接或 Channel 关闭等情况才会触发重投;要明确处理失败,就使用 nack/reject 并按策略决定是否重新入队。

消费端还要做业务幂等、控制 prefetch、监控死信和积压。消息 TTL、队列容量和死信转发策略也可能影响可靠性,所以最后仍然需要异常处理和关键业务对账。

# 讲一下 RabbitMQ 的延迟队列和死信机制

这两个机制经常一起用,但职责不同:延迟队列管「什么时候处理」,死信机制管「消息为什么退出原队列、接下来送到哪里」。

先说延迟队列。 一个常见场景是订单创建后 30 分钟仍未支付,就尝试关闭订单。RabbitMQ 常见的实现方式有两类:

img

  • TTL + 死信交换机:把消息放进没有正常消费者的等待队列,设置消息 TTL,并为该队列配置死信交换机。消息过期后转到业务队列,业务消费者再检查订单是否需要关闭。
  • 延迟交换机插件:安装与 RabbitMQ 版本匹配的插件后,可以使用 x-delayed-message 交换机和 x-delay 消息头,到期后再路由。它不是声明一个普通交换机就自带的能力,还要评估插件维护状态和存储限制。

TTL 方案有一个容易被追问的点:同一等待队列里混用不同的单条消息 TTL,可能产生队头阻塞。 前面消息要等 30 分钟,后面消息只等 1 分钟,后者通常要到队头才被实际死信转发。因此不同延迟档位可以使用不同的等待队列,不应认为 TTL 一到就精确调度。

再说死信机制。 常见触发条件包括:

img

  1. 消费者通过 nack/reject 拒绝,且 requeue=false。
  2. 消息在队列里超过 TTL。
  3. 超过队列长度限制,并按相应溢出策略移出消息;不是所有溢出策略都一样。
  4. 仲裁队列达到配置的投递次数限制。

队列预先配置 DLX 后,RabbitMQ 会尝试把这些消息重新发布到死信交换机,再路由到死信队列。如果没配置 DLX,不能指望消息自动有个兜底去处。无法从普通交换机路由到队列,则要通过 Return 或备用交换机处理,不属于消息从队列里变成死信的场景。

死信转发本身也有可靠性条件。默认转发策略不能理解为绝不丢,重要消息可以评估仲裁队列支持的至少一次死信转发,并监控目标队列是否可用。

最后回到订单例子:延迟消息到达后,消费者查订单状态,仍然待支付且已过期才尝试关单;已经支付就确认并结束,不需要依靠「订单支付后删除那条延迟消息」来保证正确性。

# RabbitMQ 的 Confirm、Return 和消费者 ACK 有什么区别?

这三个名字都像「确认」,但确认的是三个不同环节。

Confirm 是 Broker 告诉生产者:消息发布已经按相关条件处理。对于成功路由的持久化消息、仲裁队列,确认条件还与持久化和副本有关,但它不代表消费者已经完成业务。

Return 是告诉生产者:消息无法路由到队列。例如交换机存在,但 RoutingKey 没有匹配的绑定,启用 mandatory=true 后,通过 basic.return 返回消息,应用再决定告警、修正路由或补发。

消费者 ACK 则是消费者告诉 Broker:这次投递对应的业务已经处理成功,可以移除或标记该投递完成。它与生产者 Confirm 相互独立。

举个例子,消息发给一个存在但没有匹配队列的交换机,可能先收到 Return,再收到 Confirm 的 ACK。因此不能只在 Confirm 回调看到 ACK,就把发送记录标记成业务投递成功。

还有,发布到根本不存在的交换机,会产生 Channel 级错误,不能只靠 Return 回调处理。把这些异常区分开,才能正确补偿。

# RabbitMQ 的 prefetch 是什么,为什么不是越大越好?

prefetch 可以理解为消费者手里的未确认任务额度。在常见手动确认消费模式下,设置为 10,意味着达到相应作用范围内的 10 条未确认消息后,Broker 暂停继续投递,等有消息 ACK 后再补充。

它不是「拿 10 条,必须全部做完才拿下一批」。RabbitMQ 的投递和 ACK 是异步的,可以边处理、边确认、边补充;限制范围还要看 basic.qos 的配置以及是按消费者还是按 Channel 设置。

太小,消费者可能一直等待网络投递,吞吐量上不去;太大,慢消费者会囤积大量未确认消息,占内存,也让其他消费者分不到足够任务。实例宕机时,还会有更多消息需要重新投递。

所以要结合处理耗时、并发线程数、消息大小和内存做压测。prefetch 是流控手段,不能凭空提高下游数据库的处理能力,也不能单独保证业务顺序。

# RabbitMQ 的集群和仲裁队列有什么区别?

集群说的是多个节点如何共同工作,仲裁队列说的是一条队列如何复制数据。 这两个概念不能混在一起。

比如三台 RabbitMQ 组成集群,但创建的是普通经典队列,队列消息可能只保存在其中一台上。那台机器故障后,即使另外两台还活着,也不代表这条队列的数据立刻可用。

仲裁队列则通过多个成员复制队列状态,使用 Raft 机制,由多数派参与安全地提交和选举。通常部署 3 个成员,允许其中 1 个不可用时,剩下的多数派继续服务。

如果只剩 1 个成员,虽然它可能保存着消息,也不能简单让它继续接管写入,因为无法形成多数派。这里是用一部分可用性换取数据安全和一致性。

新版本设计高可用时,应明确选用了什么队列类型、副本分布和故障恢复策略,而不是只回答「搭了三节点集群」。


最新的图解文章都在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

img

上次更新: 10/4/2026