三大主流 MQ 的设计差异、可靠性投递、顺序消息、积压排查、Exactly-Once 实现全部讲透。
一、为什么需要消息队列?
四个核心价值:
1.1 解耦
紧耦合:A 服务直接调 B/C/D 三个下游
↓
解耦后:A 发消息到 MQ,B/C/D 各自订阅
收益:A 不用感知下游存在,新增订阅者无需改 A 的代码。
1.2 异步削峰
同步:用户下单 → 扣库存 → 减积分 → 发短信 → 写日志
全部串行,响应时间累加
异步:用户下单 → 写订单 + 发消息 → 立即返回
其他动作异步消费
典型场景:秒杀、双 11 流量洪峰,MQ 充当"水库"削峰。
1.3 数据通道
CDC(变更数据捕获)→ Kafka → 数仓、ES、缓存。一份数据多处消费。
1.4 事件驱动架构
订单创建 → 触发支付、库存、物流多个事件流。微服务的事件骨架。
1.5 引入 MQ 的代价
- 复杂度上升:多了一个中间件要维护
- 一致性问题:业务和消息可能不一致
- 可用性下降:MQ 挂了,下游收不到消息
- 运维成本:监控、扩容、参数调优
别为了用而用:单服务内部别用 MQ 做异步(用线程池/协程更轻)。跨服务、跨系统才有价值。
二、三大主流 MQ 全景对比
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 定位 | 流处理平台 | 通用消息队列 | 业务消息中间件 |
| 语言 | Scala/Java | Erlang | Java |
| 吞吐 | 百万级/秒 ⭐ | 万级/秒 | 十万级/秒 |
| 延迟 | ms 级 | μs 级 ⭐ | ms 级 |
| 协议 | 自定义 | AMQP、STOMP、MQTT | 自定义 |
| 路由 | 简单(主题分区) | 灵活(交换机+绑定)⭐ | 主题+Tag |
| 顺序消息 | 分区内有序 | 单队列单消费者 | 严格顺序 ⭐ |
| 事务消息 | 支持 | 弱 | 原生支持 ⭐ |
| 延迟消息 | 不支持(需插件) | 插件支持 | 原生支持 ⭐ |
| 死信 | 需自实现 | 原生 | 原生 |
| 副本机制 | ISR 副本 ⭐ | 镜像队列 | 主从 |
| 持久化 | 顺序磁盘 ⭐ | 内存为主 | 顺序磁盘 |
| 可靠性 | 极高 | 高 | 极高 |
| 适用 | 日志、流计算、大数据 | 业务路由复杂、低延迟 | 金融、订单、广告 |
选型一句话
- 大数据 / 日志收集 / 流处理 / 高吞吐 → Kafka
- 业务路由复杂 / 低延迟 / 跨语言 / 中小流量 → RabbitMQ
- 金融订单 / 顺序消息 / 事务消息 / 国内生态 → RocketMQ
三、Kafka 深入
3.1 核心概念
Producer → [Topic A]
├── Partition 0 ──→ Broker 1(Leader),Broker 2(Follower)
├── Partition 1 ──→ Broker 2(Leader),Broker 3(Follower)
└── Partition 2 ──→ Broker 3(Leader),Broker 1(Follower)
↑
Consumer Group A
[C1] [C2] [C3]
- Topic:逻辑主题
- Partition:分区,Kafka 高吞吐的核心。一个 topic 拆成 N 个分区分布在不同 broker
- Broker:Kafka 节点
- Consumer Group:消费者组,组内每个分区只被一个消费者消费(并行度 = 分区数)
- Offset:消费位移,存在
__consumer_offsets这个特殊 topic
3.2 为什么 Kafka 这么快?
① 顺序写磁盘
机械磁盘顺序写比内存随机写还快(600MB/s vs 400MB/s)。Kafka 日志只追加,不修改。
② Page Cache
不维护自己的内存缓存,直接用 OS 的 Page Cache。写入 = 写到 Page Cache(OS 异步刷盘),读取 = 直接从 Page Cache 读。
③ 零拷贝(sendfile)
传统:磁盘 → 内核 buffer → 用户 buffer → socket buffer → 网卡(4 次拷贝)
零拷贝:磁盘 → 内核 buffer → 网卡(DMA 直传,2 次拷贝)
④ 批量 + 压缩
Producer 批量打包发送(linger.ms + batch.size),broker 批量落盘,Consumer 批量拉取。配合 LZ4/Snappy 压缩,网络和磁盘 I/O 大幅降低。
⑤ 分区并行
Topic 分 N 个分区 → N 个消费者并行消费。水平扩展能力天花板极高。
3.3 ISR 副本机制
ISR(In-Sync Replicas):和 Leader 保持同步的副本集合。
Topic Partition 0:
Leader: Broker 1(处理读写)
Follower: Broker 2(同步中,在 ISR 内)
Follower: Broker 3(落后太多,被踢出 ISR)
关键参数 acks:
acks=0:Producer 不等确认,最快但可能丢acks=1:Leader 写完就确认(默认),Leader 宕机可能丢acks=all(-1):ISR 内全部 follower 写完才确认,最可靠
配合 min.insync.replicas=2(ISR 至少 2 个)才能保证不丢消息。
3.4 消费模式
拉模式(Pull)
Consumer 主动拉取,自己控制速度。Kafka 的选择。
- 优点:消费速度自主、无背压问题
- 缺点:实时性略差(poll 间隔)
推模式(Push)
Broker 主动推送。RabbitMQ 默认。
- 优点:实时性高
- 缺点:Broker 不知道 Consumer 处理能力,可能压垮消费者
3.5 Rebalance(再平衡)
消费者加入/退出/崩溃时,分区在组内重新分配。Rebalance 期间整组停止消费,是 Kafka 一大痛点。
触发场景:
- Consumer 加入或离开 Group
- Topic 分区数变化
- Consumer 心跳超时(
session.timeout.ms)
避免频繁 Rebalance:
- 调大
session.timeout.ms(默认 10s,可调到 30s) - 调小
max.poll.interval.ms防止假死 - 单次 poll 别处理太多消息
Kafka 2.4+ 的 Static Membership:固定 Consumer ID,重启不触发 Rebalance。
3.6 Kafka 不适合什么?
- 延迟消息:原生不支持(要外部调度器)
- 复杂路由:只能按 topic + partition,不支持业务标签过滤
- 超低延迟:μs 级要求选 RabbitMQ
- 事务消息:有但是流事务,业务事务不如 RocketMQ
四、RabbitMQ 深入
4.1 核心概念:AMQP 模型
Producer → Exchange → (Binding) → Queue → Consumer
↑
路由的核心
Exchange 四种类型:
Direct Exchange(精确匹配)
Routing Key = "error" → 绑定 key="error" 的队列
Fanout Exchange(广播)
绑定到该交换机的所有队列都收到消息。订阅模型。
Topic Exchange(模式匹配)⭐
Routing Key = "order.payment.success"
绑定模式:
"order.*" → 收到所有订单消息
"*.payment.*" → 收到所有支付相关
"order.#" → 收到订单下所有层级(# 匹配多段)
Headers Exchange
按消息头属性匹配,性能差,少用。
4.2 消息确认与可靠性
三层保障:
Producer → Exchange:Confirm 机制
channel.confirm_delivery()
channel.basic_publish(...) # 异步确认或同步等待
Exchange → Queue:Mandatory + Return
找不到队列时退回 Producer。
Queue → Consumer:手动 ACK
def callback(ch, method, properties, body):
try:
process(body)
ch.basic_ack(method.delivery_tag)
except Exception:
ch.basic_nack(method.delivery_tag, requeue=False) # 进死信
4.3 死信队列(DLX)
消息在以下情况进入死信队列:
- 被消费者 nack/reject 且
requeue=false - 消息 TTL 过期
- 队列长度超限
死信队列实际上是一个普通队列 + 一个 x-dead-letter-exchange 配置。
4.4 延迟消息:TTL + DLX 实现
RabbitMQ 原生不支持延迟消息,经典方案:
消息 → 设置 TTL=60s 的队列(无消费者)→ TTL 到期成为死信
→ 转发到 DLX → 真正的业务队列
插件方案:rabbitmq-delayed-message-exchange,更优雅。
4.5 镜像队列(HA)
普通队列只在一个节点,节点挂了消息丢。镜像队列把消息复制到多个节点。
RabbitMQ 3.8+ 的 Quorum Queue(基于 Raft)替代镜像队列,更可靠、更推荐。
4.6 RabbitMQ 的优势
- 路由极其灵活:Topic Exchange 能玩出花
- 延迟极低(μs 级)
- 跨语言:AMQP 是开放协议
- 管理界面友好
4.7 RabbitMQ 的劣势
- 吞吐有限(万级/秒),堆积怕死(性能下降)
- Erlang 写的,团队少有能改源码
- 集群扩展性差:节点越多性能反而下降(数据要同步)
五、RocketMQ 深入
5.1 核心概念
NameServer(轻量级注册中心,可多节点无状态)
↑
Broker(Master + Slave 主从)
↑
Producer / Consumer
- NameServer:替代 Kafka 的 ZK,轻量、无状态、可水平扩展
- Topic / MessageQueue:MessageQueue 类似 Kafka 的 Partition
- Tag:消息标签,比 Kafka 多的过滤维度
- Group:Producer Group / Consumer Group
5.2 RocketMQ 独有特性
顺序消息(严格保证)
# 把订单消息按 orderId hash 到同一个队列
producer.send(msg, MessageQueueSelector, orderId)
同一订单的所有消息进入同一队列,单队列内 FIFO。
延迟消息(原生)
msg.setDelayTimeLevel(3) # 预设级别:1s, 5s, 10s, 30s, 1m, 2m, 3m...
RocketMQ 5.0 支持任意延迟时间。
事务消息(业界最好的实现之一)
1. Producer 发送 half message(不可见)
2. Broker 返回 ack
3. Producer 执行本地事务
4. 提交:half message 变可见
回滚:删除 half message
超时:Broker 回查 Producer(Producer 实现回查接口)
完美解决"业务和消息一致性"。
消息回溯
按 offset 或时间点重新消费历史消息。Kafka 也有,但 RocketMQ 操作更友好。
5.3 存储设计
CommitLog(所有消息混合写入,顺序追加)
↓ 异步建索引
ConsumeQueue(每个 MessageQueue 一个,存 offset 索引)
IndexFile(按 key 检索)
和 Kafka 的"每分区独立日志"不同,RocketMQ 所有数据在一个 CommitLog,避免大量 topic 时随机 I/O。
5.4 RocketMQ 的优势
- 业务消息特性最全:顺序、延迟、事务、回溯
- 海量 topic 性能好(CommitLog 设计)
- 国内生态:阿里、字节、美团等大厂深度使用
- 金融级可靠性
5.5 RocketMQ 的劣势
- 国际生态弱(Kafka 是世界标准)
- 客户端语言少,Java 最完善
六、消息可靠性:怎么保证不丢消息?
6.1 三个丢失环节
Producer → [网络]→ Broker → [磁盘]→ Consumer → [处理]
↑ ↑ ↑
发送丢失 存储丢失 消费丢失
6.2 Producer 端:发送可靠
| 方案 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 同步发送 | ✅ | ✅ | ✅ |
| 异步 + 回调 | ✅ | ✅ | ✅ |
| 重试机制 | retries | 业务自实现 | retryTimes |
| 确认机制 | acks=all | confirm 模式 | SYNC_FLUSH |
关键:异步发送一定要带回调,否则失败无感知。
6.3 Broker 端:存储可靠
Kafka
acks=all # ISR 全部确认
min.insync.replicas=2 # ISR 至少 2 个
unclean.leader.election=false # 非 ISR 不能当 Leader
replication.factor=3 # 3 副本
RocketMQ
flushDiskType=SYNC_FLUSH # 同步刷盘(性能下降)
brokerRole=SYNC_MASTER # 主从同步
RabbitMQ
- 队列声明
durable=true - 消息属性
delivery_mode=2(持久化) - 用 Quorum Queue(Raft 副本)
取舍:可靠性 ↑ → 性能 ↓。金融级用同步刷盘 + 多副本同步,互联网通用用异步刷盘 + 多副本异步。
6.4 Consumer 端:消费可靠
核心:处理完成后再 ACK,不能 auto-commit。
# ❌ 危险:自动提交,处理一半崩溃,offset 已提交,消息丢了
enable.auto.commit=true
# ✅ 正确:手动提交
enable.auto.commit=false
for msg in consumer:
try:
process(msg)
consumer.commit_sync() # 处理完才提交
except Exception:
# 不提交,下次重新消费
pass
七、消息重复 & 幂等
7.1 为什么必然有重复?
MQ 至少投递一次(At-Least-Once)是默认语义:
- Producer 重试 → 重复发送
- Consumer 处理后 ACK 前崩溃 → 重启后重新消费
- Rebalance → 已处理的消息被另一个 Consumer 重新消费
结论:消费者必须实现幂等,不能依赖 MQ。
7.2 三种幂等方案
方案 A:唯一索引
CREATE UNIQUE INDEX uk_msg ON processed_messages(msg_id);
-- 处理前先 insert,主键冲突说明已处理
INSERT INTO processed_messages(msg_id) VALUES(?);
简单可靠,首选。
方案 B:状态机
# 订单只能从 created → paid,不能从 paid → paid
if order.status != "created":
return # 已处理过
order.status = "paid"
方案 C:Redis 去重
if redis.set(f"msg:{msg_id}", "1", nx=True, ex=86400):
process(msg)
else:
pass # 重复消息
注意:Redis 不可靠时仍要数据库兜底。
八、Exactly-Once:传说中的精确一次
8.1 真的能做到吗?
严格意义上做不到(FLP 不可能定理)。但可以做到业务上等价于精确一次 = At-Least-Once + 幂等。
8.2 Kafka 的 EOS(Exactly-Once Semantics)
Kafka 0.11+ 通过两个机制实现:
幂等 Producer
enable.idempotence=true
每个 Producer 有唯一 PID,每条消息有 sequence number。Broker 端对相同 PID + sequence 去重。解决 Producer 重试导致的重复。
事务
producer.init_transactions()
producer.begin_transaction()
producer.send(...)
producer.send_offsets_to_transaction(offsets, group_id)
producer.commit_transaction()
适用场景:Kafka Streams 的"消费 Kafka → 处理 → 写回 Kafka"。跨外部系统不适用。
8.3 工程实践
99% 场景:At-Least-Once + 业务幂等。别迷信 Exactly-Once。
九、顺序消息
9.1 全局顺序
只能单分区单消费者。Kafka 全局顺序 = 1 个 partition + 1 个 consumer,性能极差,几乎不用。
9.2 局部顺序(业务顺序)⭐
按业务 key 路由到同一分区。同一订单/用户的消息有序。
Kafka 实现
producer.send(topic, key=order_id, value=msg) # 同一 key 进同一分区
RocketMQ 实现
producer.send(msg, MessageQueueSelector, order_id)
9.3 消费端顺序的陷阱
单线程消费保顺序,但慢。多线程消费要保顺序:
方案:消息按 key hash 到不同的内部队列,每个队列单线程消费
# 伪代码
for msg in consumer:
queue_id = hash(msg.key) % WORKER_NUM
worker_queues[queue_id].put(msg)
# N 个 worker 各自串行消费自己的队列
十、消息积压排查与处理
10.1 现象
Lag(未消费消息数)持续增长,下游响应越来越慢。
10.2 定位思路
1. 是 Producer 暴增还是 Consumer 变慢?
→ 看 Producer QPS 和 Consumer 处理速率
2. Consumer 慢的根因?
→ 业务逻辑慢(CPU/IO)
→ 下游依赖慢(DB/外部服务)
→ 单条消息太大
→ 消费线程数太少
3. 是不是个别消息卡住了整个分区?
→ 看死信、看消费日志
10.3 紧急扩容方案
临时扩 Consumer
Kafka:分区数 = N,最多 N 个 Consumer 并行
→ 如果分区数不够,先扩分区(仅适用新消息)
→ 再扩 Consumer
紧急通道(最经典方案)
原 Topic(堆积严重)
↓
临时 Consumer(不处理业务,只转发)
↓
新建 Topic(多分区,比如 100 个)
↓
扩容 Consumer(100 个)并行消费
关键技巧:临时 Consumer 不做业务,只是把消息按 key 重新分发到更多分区。
10.4 跳过 vs 修复
- 可以跳过:日志、点赞、监控数据 → 直接丢弃,重置 offset
- 不能跳过:订单、支付 → 必须处理完,扩容硬扛
10.5 预防积压
- 监控 Lag:Prometheus + Grafana 看 consumer lag
- 告警阈值:Lag > 10 万触发告警
- 限流保护:Producer 端限流,避免 Consumer 被打挂
- 死信兜底:处理失败的消息进死信,人工处理或后续重试
十一、RocketMQ 事务消息深度实例
经典场景:下单 + 扣库存,用事务消息保证最终一致。
# Producer 端
class OrderTxListener(TransactionListener):
def execute_local_transaction(self, msg, arg):
try:
db.create_order(arg) # 本地事务
return COMMIT_MESSAGE
except Exception:
return ROLLBACK_MESSAGE
def check_local_transaction(self, msg):
# Broker 回查(如 Producer 崩溃,状态未知)
if db.has_order(msg.order_id):
return COMMIT_MESSAGE
return ROLLBACK_MESSAGE
producer.send_message_in_transaction(msg, order_data)
# Consumer 端(库存服务)
@listen("order_created")
def handle(msg):
order = parse(msg)
if not is_processed(order.id): # 幂等
deduct_stock(order)
mark_processed(order.id)
整个流程:
- 发 half message
- 本地建订单
- 提交:消息可见,库存服务消费
- 失败:回滚消息
- Producer 崩溃:Broker 回查订单是否存在
十二、避坑清单
- 不要用 MQ 做单服务内异步:用线程池/协程更轻
- Producer 必须带回调或同步等确认,否则发送失败无感知
- Consumer 必须手动 ACK,关闭自动提交
- 必须实现消费幂等,依赖唯一 ID + 数据库唯一索引
- 顺序消息按业务 key 路由,全局顺序几乎不用
- 大消息(> 1MB)拆分或存对象存储,MQ 只发引用
- 死信队列必配,否则毒消息卡住整个分区
- 监控 Lag 是底线,没监控等于裸奔
- Kafka 分区数预估要充足,扩分区影响顺序保证
- RocketMQ 事务消息回查接口必须幂等
- RabbitMQ 别用 fanout 广播大流量,每个队列都拷贝消息会爆
- MQ 集群和应用同机房,跨机房延迟惨不忍睹
十三、面试高频题速记
- Q:MQ 解决什么问题? A:解耦、异步、削峰、数据通道
- Q:Kafka 为什么这么快? A:顺序写、Page Cache、零拷贝、批量压缩、分区并行
- Q:怎么保证消息不丢? A:Producer 同步确认 + Broker 多副本 + Consumer 手动 ACK
- Q:消息重复怎么处理? A:业务幂等(唯一索引、状态机、Redis 去重)
- Q:怎么保证顺序? A:按业务 key 路由到同一分区/队列
- Q:消费端多线程怎么保顺序? A:按 key hash 到内部队列,每个队列单线程
- Q:Kafka 和 RabbitMQ 怎么选? A:Kafka 高吞吐大数据场景,RabbitMQ 路由复杂低延迟业务场景
- Q:Kafka 和 RocketMQ 区别? A:RocketMQ 业务特性更全(顺序/延迟/事务),Kafka 流处理生态更强
- Q:Exactly-Once 真能做到吗? A:Kafka EOS 限定生态内能做到,跨系统靠 At-Least-Once + 业务幂等
- Q:消息积压怎么处理? A:扩 Consumer(受限于分区数)→ 紧急通道(转发到多分区新 topic)
- Q:Kafka Rebalance 怎么避免? A:调大 session.timeout、Static Membership、单次 poll 别处理太多
- Q:RocketMQ 事务消息原理? A:half message + 本地事务 + 提交/回滚 + 超时回查
- Q:什么时候用 MQ,什么时候用 RPC? A:要回结果用 RPC,不需要回结果或解耦用 MQ