Loading

【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 核心模型

Kafka 模型

  • 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 在三个环节提供可靠性保障:

  1. 生产者 → Broker(Confirm 机制):开启 publisher-confirm,Broker 收到消息后回 ACK;若路由不到队列可返回 return( mandatory=true 时)。用于保证「发出去了」。
  2. Broker 持久化:队列 durable=true + 消息 deliveryMode=2(持久化),保证 Broker 重启不丢。
  3. 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 用半事务消息 + 回查 实现最终一致:

  1. 生产者发送 Half Message(半消息):Broker 先存,但对消费者不可见;
  2. 生产者执行本地事务(写订单库);
  3. 本地事务成功 → 提交(Commit),半消息变为可消费;失败 → 回滚(Rollback),半消息丢弃;
  4. 若第 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、其他实践要点

  1. 重试与死信:消费失败先本地/重试队列退避重试(1s/5s/30s),多次失败进死信并告警,人工兜底;
  2. 积压监控:Kafka 看 consumer lag,RabbitMQ 看队列深度,RocketMQ 看堆积量,超阈值告警;
  3. 消费耗时长:设置合适 max.poll.interval(Kafka),避免消费者被踢出组导致疯狂重复消费;
  4. 消息 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。
posted @ 2026-09-19 05:41  Stellan7  阅读(21)  评论(0)    收藏  举报