消息队列深度解析:Kafka vs RabbitMQ vs RocketMQ

5 阅读13分钟

三大主流 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 全景对比

维度KafkaRabbitMQRocketMQ
定位流处理平台通用消息队列业务消息中间件
语言Scala/JavaErlangJava
吞吐百万级/秒万级/秒十万级/秒
延迟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 端:发送可靠

方案KafkaRabbitMQRocketMQ
同步发送
异步 + 回调
重试机制retries业务自实现retryTimes
确认机制acks=allconfirm 模式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)

整个流程

  1. 发 half message
  2. 本地建订单
  3. 提交:消息可见,库存服务消费
  4. 失败:回滚消息
  5. Producer 崩溃:Broker 回查订单是否存在

十二、避坑清单

  1. 不要用 MQ 做单服务内异步:用线程池/协程更轻
  2. Producer 必须带回调或同步等确认,否则发送失败无感知
  3. Consumer 必须手动 ACK,关闭自动提交
  4. 必须实现消费幂等,依赖唯一 ID + 数据库唯一索引
  5. 顺序消息按业务 key 路由,全局顺序几乎不用
  6. 大消息(> 1MB)拆分或存对象存储,MQ 只发引用
  7. 死信队列必配,否则毒消息卡住整个分区
  8. 监控 Lag 是底线,没监控等于裸奔
  9. Kafka 分区数预估要充足,扩分区影响顺序保证
  10. RocketMQ 事务消息回查接口必须幂等
  11. RabbitMQ 别用 fanout 广播大流量,每个队列都拷贝消息会爆
  12. 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