【WebWiki】14 · 消息队列:Kafka / RabbitMQ / RocketMQ
06 消息队列(Kafka / RabbitMQ / RocketMQ)
WebWiki · 中间件篇
1. 为什么要用消息队列
三大价值:异步(下单主流程 100ms 内返回,扣库存/发券/通知异步化)、解耦(生产者不关心谁消费)、削峰(秒杀洪峰先入队,消费端按能力处理)。
选型对比:
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 吞吐 | 百万级/秒 | 万级/秒 | 十万级/秒 |
| 延迟 | ms 级 | μs 级(低延迟见长) | ms 级 |
| 功能 | 分区/消费组,生态强 | 路由灵活、延迟队列插件 | 事务消息、延迟等级、原生 |
| 典型场景 | 日志采集、大数据管道、事件流 | 业务消息、任务分发 | 电商交易、订单事件 |
简单原则:大数据量、日志流选 Kafka;传统业务集成、低延迟选 RabbitMQ;国内电商类业务 RocketMQ 也很主流。
补充:消息队列不是银弹。引入 MQ 会带来系统复杂度上升——要保证不丢消息、不重复消费、消息顺序、积压监控,还要维护一套中间件集群。小项目如果调用量不大,直接同步调用或本地事件表往往更简单。是否引入 MQ,取决于「异步/解耦/削峰」带来的收益是否大于运维成本。
1.1、消息队列解决的具体问题
- 异步:用户注册后,需要发短信、发邮件、初始化积分、送优惠券。若同步串行执行可能要 1s+;把后续动作丢进 MQ,主流程只做落库+发消息(几十毫秒)就返回,提升用户体验。
- 解耦:A 系统产出一个订单,原来要调 B(库存)、C(营销)、D(风控)。任一下游改接口,A 都要跟着改。引入 MQ 后,A 只管发「订单创建事件」,B/C/D 自己订阅,互不影响。
- 削峰填谷:秒杀瞬时 10w QPS,数据库只能扛 2k。让请求先进 MQ,下游按 2k/s 匀速消费,洪峰过后慢慢消化,保护后端不被打垮。
1.2、引入 MQ 的代价
- 系统可用性降低(MQ 挂了全链路受影响,需要保证 MQ 高可用);
- 一致性问题(异步后主流程成功、下游失败,需要补偿/最终一致);
- 复杂度上升(重复消费、丢失、顺序、积压都要处理,见第 5 节)。
2. Kafka
Apache Kafka 是一个分布式流处理平台,本质是「分布式提交日志(commit log)」。它把消息以分区(partition) 顺序追加写入磁盘,靠顺序 IO 达到极高吞吐。
2.1 核心模型

- Topic 逻辑分类,物理上分成多个 Partition(并行度单位,分区数 ≥ 消费者数);
- 每条消息按 key 哈希(或轮询)进入某分区,分区内有序;
- Consumer Group:组内竞争(一条消息组内只被一个消费者处理),组间广播;
- Offset 由消费者提交管理,支持重复消费与跳过。
2.1.1、Broker / Topic / Partition / Replica 详解
- Broker:Kafka 集群中的单个服务节点(一台机器上的一个进程)。多个 Broker 组成一个集群,由 ZooKeeper(或新版 KRaft)协调。
- Topic:消息的逻辑主题,如
order-events、user-behavior。 - Partition(分区):Topic 的物理分片,是 Kafka 并行度与吞吐的基石。消息写入时按
key哈希取模(hash(key) % partitionCount)分配到某分区;key 为 null 时轮询。每个分区是一个有序、不可变的消息序列,每条消息在分区内有唯一、递增的 offset。 - Replica(副本):每个分区可配置多个副本(如
replication.factor=3),分布在不同 Broker 上实现容错。副本分两类:- Leader 副本:对外提供读写,生产者/消费者只和 Leader 交互;
- Follower 副本:被动从 Leader 拉取数据同步,只做备份,不对外服务。
- ISR(In-Sync Replicas,同步副本集合):和 Leader 保持「同步」的副本集合(包含 Leader 自己)。判定标准是「在
replica.lag.time.max.ms内追上 Leader 的最新数据」。若某个 Follower 落后太多或心跳超时,会被踢出 ISR;追上后重新加入。 - HW(High Watermark,高水位)与 LEO(Log End Offset):HW 是「已提交」位置,消费者只能读到 HW 之前的消息,保证「已读到的消息在 Leader 切换后不丢失」;LEO 是各副本当前日志末端。只有 ISR 中所有副本都写入了的位移才能成为 HW。
选举与高可用:Leader 挂掉后,Controller 会从 ISR 中选举一个新 Leader(优先副本)。如果
acks=all且 min.insync.replicas=2,只要 ISR 里还有副本,数据就不丢。若unclean.leader.election.enable=true(默认 false),允许非 ISR 副本当 Leader,可能丢数据但可用性更高——生产务必保持 false。
2.1.2、消费者组与再均衡(Rebalance)
- 一个 Consumer Group 内多个消费者共同消费一个 Topic,Kafka 把分区均分给组内消费者。原则:一个分区同一时刻只能被组内一个消费者消费(否则无法保证分区内顺序);因此消费者数 ≤ 分区数,多出来的消费者会空闲。
- 再均衡(Rebalance):当组内成员变化(新消费者加入、某消费者宕机/主动退出、订阅 Topic 分区数变化、心跳超时被踢)时,Kafka 重新分配分区归属。期间整个组会暂停消费(stop-the-world),直到分配完成。
- 再均衡的坑与优化:
- 消费逻辑太慢、超过
max.poll.interval.ms(默认 5 分钟)没调用poll(),会被认为已死,触发 Rebalance,然后它又被拉回分区,又超时……形成「消费—踢出—再均衡」死循环,消息一直重复消费。解决:调大max.poll.interval.ms、减少单次poll拉取量、把重逻辑异步化或限流。 - 心跳
session.timeout.ms与heartbeat.interval.ms配合:心跳丢失超过 session 超时即被踢出。 - 新版 Cooperative Sticky 分配策略(CooperativeStickyAssignor) 支持「增量再均衡」,只迁移必要的分区,减少停顿,推荐开启。
- 用
group.instance.id配置静态成员,短暂重启不会触发再均衡。
- 消费逻辑太慢、超过
2.1.3、生产端可靠性参数
spring:
kafka:
producer:
acks: all # 0=不等待; 1=Leader写盘; all=所有ISR副本写盘(最可靠)
retries: 2147483647 # 配合幂等,失败无限重试
enable.idempotence: true # 幂等生产者:broker 端去重,避免重试导致重复
properties:
max.in.flight.requests.per.connection: 5 # 幂等开启时仍保序的上限
acks=all+min.insync.replicas=2+ 副本数 3:可容忍 1 个 Broker 挂掉且不丢消息。- 幂等生产者只能保证单分区、单会话内不重复;跨分区/跨会话的去重需要业务层用唯一键(如
orderId)做消费幂等。
2.2 Spring Kafka 实战
// 生产者
@Service
@RequiredArgsConstructor
public class OrderEventProducer {
private final KafkaTemplate<String, String> kafkaTemplate;
public void send(OrderCreatedEvent event) {
// key 用 orderId:同订单消息进同一分区,保证局部有序
kafkaTemplate.send("order-events", String.valueOf(event.orderId()),
JSON.toJSONString(event));
}
}
// 消费者
@KafkaListener(topics = "order-events", groupId = "order-service",
concurrency = "3") // 一般 ≤ 分区数
public void onMessage(ConsumerRecord<String, String> record) {
OrderCreatedEvent event = JSON.parseObject(record.value(), OrderCreatedEvent.class);
orderService.handle(event); // 失败抛出 → 触发重试/进入死信
}
spring:
kafka:
bootstrap-servers: kafka1:9092,kafka2:9092
producer:
acks: all # 等所有副本确认
properties.enable.idempotence: true # 生产端幂等,防重复写入
consumer:
auto-offset-reset: earliest
enable-auto-commit: false # 手动提交
listener:
ack-mode: record # 处理完一条提交一条
消费端提交策略:
enable-auto-commit=false时由业务控制 offset 提交时机。
ack-mode=record:每条处理完即提交,处理中宕机 → 这条会被重复消费(至少一次语义);ack-mode=manual:在代码里ack.acknowledge()手动提交,可做到「处理成功才提交」;- 想要「精确一次(exactly-once)」需结合事务(
isolation.level=read_committed+ 生产者事务 + 消费幂等),Kafka 提供的是「读已提交 + 幂等」的有限 exactly-once,跨系统仍要业务兜底。
2.3 常用运维配置
- 分区数:目标吞吐 ÷ 单消费者能力,预留扩容余量(分区只能加不能减);
- key 设计:同业务实体聚合到同分区;避免 key 过于集中造成数据倾斜;
- 消息积压:加消费者(不超过分区数)、临时"扩分区+批量搬运"、消费者降级只记录后补;
- 消息保留
retention.ms(默认 7 天),Compact 模式用于状态类 topic。
更多运维要点:
log.retention.hours/log.segment.bytes:控制日志段滚动与清理,避免磁盘写满;- 监控指标:Under Replicated Partitions(非空=有副本掉队,危险)、Consumer Lag(消费滞后,核心告警项)、Request Handler Idle(Broker 繁忙度);
- 磁盘顺序写 + 页缓存(page cache)是 Kafka 高吞吐的关键,不要用 RAID 5,建议多块独立盘挂
log.dirs。
3. RabbitMQ
RabbitMQ 是基于 Erlang/AMQP 实现的消息中间件,特点是路由灵活、协议规范、低延迟,适合业务系统间的可靠消息投递。
3.1 交换机类型
| 类型 | 路由规则 | 场景 |
|---|---|---|
| Direct | 精确匹配 routing key | 点对点任务 |
| Topic | 通配符匹配(order.* / #.error) |
分类订阅 |
| Fanout | 广播给所有绑定队列 | 配置刷新、事件广播 |
| Headers | 按消息头匹配 | 少用 |
死信队列(DLX):消息被拒绝、过期(TTL)、队列超长时进入——用 TTL + DLX 组合可实现延迟队列(或直接用延迟插件 rabbitmq_delayed_message_exchange)。
3.1.1、四种交换机路由细节
- Direct:生产者发消息带
routingKey="order.create",只有绑定了相同 key 的队列收到。最常用,做「点对点」任务分发。 - Topic:key 用点分隔,
*匹配一个词,#匹配零到多个词。如绑定order.#能收order.create/order.pay.success;*.error只收末尾是 error 的。适合「分类订阅」(订单组关心order.*,风控组关心*.risk)。 - Fanout:忽略 key,广播到所有绑定队列。适合「配置变更广播」「刷新缓存」等一对多场景。
- Headers:按消息头键值对匹配(非 key),性能差、用得少,几乎被 Topic 取代。
3.1.2、确认机制(消息可靠性核心)
RabbitMQ 在三个环节提供可靠性保障:
- 生产者 → Broker(Confirm 机制):开启
publisher-confirm,Broker 收到消息后回 ACK;若路由不到队列可返回return( mandatory=true 时)。用于保证「发出去了」。 - Broker 持久化:队列
durable=true+ 消息deliveryMode=2(持久化),保证 Broker 重启不丢。 - Broker → 消费者(手动 ACK):消费者处理完再
basicAck;处理中宕机会被重新投递(可能重复,需幂等)。
// 生产者开启 confirm + return
@Configuration
public class RabbitConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory cf) {
RabbitTemplate t = new RabbitTemplate(cf);
t.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) log.error("消息未到达Exchange: {}", cause); // 重发或报警
});
t.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.error("消息路由失败 {}->{} : {}", exchange, routingKey, replyText); // 进补偿表
});
t.setMandatory(true); // 路由不到队列时触发 return
return t;
}
}
3.2 Spring AMQP 实战
@Configuration
public class MqConfig {
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.queue")
.withArgument("x-dead-letter-exchange", "dlx.exchange")
.withArgument("x-message-ttl", 60000) // 1 分钟后进死信 → 延迟检查
.build();
}
}
@RabbitListener(queues = "order.queue")
public void handle(OrderMessage msg, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
try {
orderService.handle(msg);
channel.basicAck(tag, false);
} catch (Exception e) {
// 重试 3 次后入死信
channel.basicNack(tag, false, false);
}
}
死信队列 + TTL 实现「延迟队列」的原理:业务消息先发到一个普通队列,该队列不绑定消费者,只设
x-message-ttl和x-dead-letter-exchange。消息到期后被自动投递到死信交换机,再由死信队列的消费者处理——相当于「延迟 N 分钟后再消费」。更优雅的做法是装rabbitmq_delayed_message_exchange插件,用x-delayed-message类型交换机直接延迟,避免 TTL 队列「队头阻塞」(队头消息未到期会阻塞后面已到期的消息)。
4. RocketMQ
RocketMQ 是阿里巴巴开源、后捐赠 Apache 的分布式消息中间件,主打金融级可靠、事务消息、海量堆积,在国内电商/交易场景是事实标准。
4.1 核心概念
- Producer / Consumer:生产者和消费者,按 Topic 收发。
- Broker:存储与转发节点,主从架构(Master 写、Slave 同步/异步复制)。
- NameServer:轻量级路由注册中心(无状态、彼此不通信,对比 Kafka 的 ZooKeeper 更简单),Producer/Consumer 从它获取 Topic 路由。
- Topic / MessageQueue:Topic 逻辑主题,物理上拆成多个 MessageQueue(类似 Kafka 的 partition,并行度单位)。
- Tag:Topic 下的二级分类,用于同 Topic 内细粒度过滤(如
order:create/order:pay)。
4.2 事务消息(RocketMQ 的招牌能力)
分布式事务经典难题:本地下单成功,但发 MQ 失败了,导致下游(扣库存)没收到。RocketMQ 用半事务消息 + 回查 实现最终一致:
- 生产者发送 Half Message(半消息):Broker 先存,但对消费者不可见;
- 生产者执行本地事务(写订单库);
- 本地事务成功 → 提交(Commit),半消息变为可消费;失败 → 回滚(Rollback),半消息丢弃;
- 若第 2、3 步期间 Producer 宕机,Broker 会定时回查(checkLocalTransaction)生产者本地事务状态,决定提交还是回滚。
// RocketMQ 事务消息示例(Spring Cloud Stream / rocketmq-spring-boot 风格)
@Component
public class OrderTxProducer {
@Autowired
private TransactionMQProducer producer;
public void createOrder(Order order) {
Message msg = new Message("order-topic", "create",
order.getId(), JSON.toJSONString(order).getBytes());
// 发送半事务消息,并指定本地事务执行器与回查监听器
producer.sendMessageInTransaction(msg, order);
}
}
// 本地事务 + 回查
@RocketMQTransactionListener
public class OrderTxListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
orderService.save((Order) arg); // 写本地库
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// Broker 回查:查订单库是否真的写入
return orderService.exists(msg.getKeys())
? RocketMQLocalTransactionState.COMMIT
: RocketMQLocalTransactionState.UNKNOWN; // UNKNOWN 会继续回查
}
}
对比:RabbitMQ/Kafka 没有原生事务消息,通常用「事务性发件箱(Transactional Outbox)」模式兜底——本地事务里同时往
outbox表插一条消息记录,再由独立进程扫表转发到 MQ,保证本地事务和「待发消息」原子性,是跨中间件的通用方案。
4.3 延迟消息与顺序消息
- 延迟消息:RocketMQ 原生支持 18 个延迟级别(
1s/5s/10s/30s/1m...2h),通过setDelayTimeLevel指定,比 RabbitMQ 的 TTL+DLX 更稳(无队头阻塞)。 - 顺序消息:同一业务实体(如同一订单)的消息进同一 MessageQueue,单队列单线程消费即保证全局/分区有序。
// 顺序发送:用订单id取模选队列
producer.send(msg, (mqs, m, arg) -> {
int id = (int) arg;
return mqs.get(id % mqs.size()); // 同订单进同队列
}, order.getId());
5. 生产实践清单:消息丢失 / 重复 / 积压三大问题
这是面试与线上最常被问的三类问题,下面逐一给出根因与方案,并对比三种中间件。
5.1、消息丢失(如何不丢)
| 环节 | 风险点 | 解决方案 |
|---|---|---|
| 生产者 | 网络抖动没发到 / 发到但 Broker 没落盘 | Kafka:acks=all + 幂等/重试;RabbitMQ:Confirm + Return 回调;RocketMQ:同步发送 + 发送状态校验 |
| Broker | 宕机丢内存中未刷盘消息 | Kafka:副本 replication.factor≥3 + min.insync.replicas≥2;RabbitMQ:队列 durable + 消息持久化;RocketMQ:同步刷盘 flushDiskType=SYNC_FLUSH + 主从同步复制 |
| 消费者 | 自动提交 offset 后处理崩了 | 关闭自动提交,处理成功再手动提交(Kafka enable.auto.commit=false;RabbitMQ basicAck);消费幂等兜底 |
5.2、消息重复(如何幂等)
消息队列普遍只保证「至少一次(at-least-once)」,重复不可避免,关键在消费端做幂等:
- 数据库唯一键/去重表:以消息业务主键(如
orderId)建唯一索引,重复插入直接忽略; - Redis 防重:处理前
SETNX orderId 1(带过期),存在则跳过; - 乐观锁版本号:更新时带
version,UPDATE ... WHERE id=? AND version=?,影响行数为 0 即重复; - Kafka 幂等生产者 + 精确一次:仅限单分区单会话,跨系统仍需业务幂等。
三种中间件对比:RocketMQ 提供「至少一次 + 幂等去重」;Kafka 0.11+ 支持幂等与事务;RabbitMQ 本身不提供,全靠业务幂等。结论:去重逻辑放在消费端最稳妥,与中间件无关。
5.3、消息积压(如何处理)
| 中间件 | 积压表象 | 处理手段 |
|---|---|---|
| Kafka | Consumer Lag 暴涨 | 增加消费者(≤ 分区数);临时扩分区 + 起「搬运消费者」把消息转存到新 Topic 多分区并行消费;紧急时消费者降级为「只记录 id 后续批量回查」 |
| RabbitMQ | 队列深度持续增长 | 加消费者实例提升并发;把单队列拆成多队列(按 key 散列)提升并行度;消费慢的任务用「临时topic + 多线程」加速;必要时丢弃非重要消息并补数 |
| RocketMQ | 堆积量(Diff)上涨 | 扩容 Consumer 实例(≤ MessageQueue 数);开启「批量消费」consumeMessageBatchMaxSize;临时建 Topic 多队列,用搬运程序重定向 |
通用原则:积压前先限流/降级源头,避免雪崩;消费端提升吞吐(批量、异步、去重后直写);监控 Lag 设阈值告警。
5.4、顺序性与大消息
- 顺序消息:Kafka 用同 key 进同分区;RabbitMQ 单队列单消费者;RocketMQ 同 key 进同 MessageQueue 并单线程消费。
- 大消息:消息体精简为事件(只带 id 与必要字段),消费方按需回查;超 1MB 走对象存储+引用(如把文件放 OSS,MQ 里只传 URL)。各中间件都不建议单条消息过大(Kafka 默认上限
message.max.bytes=1MB)。
5.5、其他实践要点
- 重试与死信:消费失败先本地/重试队列退避重试(1s/5s/30s),多次失败进死信并告警,人工兜底;
- 积压监控:Kafka 看 consumer lag,RabbitMQ 看队列深度,RocketMQ 看堆积量,超阈值告警;
- 消费耗时长:设置合适
max.poll.interval(Kafka),避免消费者被踢出组导致疯狂重复消费; - 消息 TTL/过期:RabbitMQ 队列/消息级 TTL;Kafka 用 retention;RocketMQ 用延迟级别。
6. 三中间件速查对比(面试版)
| 对比项 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 开发语言 | Scala/Java | Erlang | Java |
| 协议 | 自定义(TCP 二进制) | AMQP | 自定义(Remoting) |
| 吞吐量 | 极高(百万/s) | 中(万/s) | 高(十万/s) |
| 延迟 | ms | μs~ms(最低) | ms |
| 顺序性 | 分区内有序 | 单队列有序 | 队列内有序 |
| 事务消息 | 事务 API(有限) | 需 Outbox 模式 | 原生半事务消息 |
| 延迟队列 | 不支持(用外部) | TTL+DLX / 延迟插件 | 原生延迟级别 |
| 死信 | 无原生(业务实现) | 原生 DLX | 原生 |
| 重试 | 偏移量重放 | 原生重试/死信 | 原生重试(16 次递进) |
| 典型场景 | 日志、流处理、事件总线 | 业务解耦、任务队列 | 电商交易、金融 |
选型一句话:要吞吐与生态选 Kafka;要灵活路由与低延迟选 RabbitMQ;要事务消息与国内电商稳健选 RocketMQ。
7. 小结
- MQ 解决异步、解耦、削峰,但带来一致性、可用性、复杂度代价;
- Kafka 的核心是分区 + 副本 + ISR + 消费组再均衡,理解 ISR 与 Rebalance 是运维关键;
- RabbitMQ 强在灵活路由(4 类交换机)与 Confirm/Return/手动 ACK 可靠性链路,DLX 实现延迟;
- RocketMQ 的招牌是事务消息与延迟级别,适合交易场景;
- 三大必答题(丢失/重复/积压)本质是:生产确认 + Broker 持久化 + 消费手动提交 + 消费幂等 + 监控 Lag。

浙公网安备 33010602011771号