消息队列常见问题整理

4 阅读24分钟

本文档主要整理 Redis、RabbitMQ、RocketMQ、Kafka 在消息队列场景中的常见实现方案,包括:消息不丢失、延迟队列、顺序消费等核心问题。


目录


一、Redis 实现消息队列

Redis 可通过多种数据结构实现消息队列,核心方案如下。

1. 基于 List 类型实现普通消息队列

实现方式

生产消息

使用 RPUSH 命令向列表尾部添加消息:

RPUSH queue_key "message1"
消费消息

消费方式主要有两种:

消费方式命令说明
轮询模式LPOP从列表头部获取消息;如果队列为空,则 sleep 一段时间后重试,避免空轮询消耗资源。
阻塞模式BLPOP阻塞式弹出;当队列无消息时一直阻塞等待,直到有消息到来或超时。

示例:

BLPOP queue_key 0

0 表示无限阻塞。

优势

  • 实现简单。
  • 支持 FIFO 顺序。
  • 适合基本的异步通信场景。

适用场景

  • 任务通知。
  • 日志收集。
  • 简单异步任务处理。

2. 基于 Pub/Sub 模式实现多消费者队列

实现方式

生产者发布消息
PUBLISH channel message
消费者订阅频道
SUBSCRIBE channel

特点

Redis Pub/Sub 支持 一个生产者、多个消费者 的广播模式。

局限性

  • 消费者下线期间生产的消息会丢失。
  • Redis 不会持久化 Pub/Sub 消息。
  • 不适合可靠性要求高的业务场景。

适用场景

  • 实时通知。
  • 在线广播。
  • 对消息可靠性要求不高的场景。

3. 基于 Sorted Set 实现延时消息队列

实现方式

生产消息

使用 ZADD 命令:

  • 消息内容作为 member
  • 消息执行时间戳作为 score

示例:

ZADD delay_queue 1620000000 "task1"

表示 task1 在时间戳 1620000000 执行。

消费消息

消费者通过 ZRANGEBYSCORE 获取已经到执行时间的消息:

ZRANGEBYSCORE delay_queue 0 当前时间戳

处理完成后,使用 ZREM 删除消息:

ZREM delay_queue "task1"

优势

  • 支持按时间顺序延迟处理消息。
  • 适合定时任务、订单超时取消等场景。

二、RabbitMQ

1. RabbitMQ 如何确保消息不丢失

RabbitMQ 确保消息不丢失,需要从以下三个环节进行保障:

  1. 生产者发送。
  2. 消息队列存储。
  3. 消费者接收。

1.1 生产者环节:确保消息成功发送至 RabbitMQ

方式一:使用 Confirm 模式(推荐)

原理

将信道设置为 Confirm 模式后,所有发布的消息会被分配唯一 ID。

当消息被投递到所有匹配队列,或者已经持久化到磁盘后,RabbitMQ 会向生产者发送 ACK 确认。

如果处理失败,则发送 Nack,生产者可根据 ACK/Nack 结果进行重试。

优势

  • 异步确认。
  • 吞吐量高于事务模式。
  • 适合高并发场景。

方式二:事务模式(不推荐,仅作了解)

原理

发送消息前开启事务:

channel.txSelect();

发送成功后提交事务:

channel.txCommit();

发送失败则回滚事务:

channel.txRollback();

缺点

事务会阻塞信道,大幅降低吞吐量,实际应用中很少使用。


1.2 消息队列环节:确保消息持久化存储

需要同时配置:

  1. 队列持久化。
  2. 消息持久化。

这样可以确保 RabbitMQ 重启后消息不丢失。

队列持久化

声明队列时,将 durable 参数设置为 true

作用:保证队列元数据持久化到磁盘,包括:

  • 队列名称。
  • 绑定关系。
  • 队列基础配置。
消息持久化

发送消息时设置:

deliveryMode = 2

作用:将消息内容持久化到磁盘日志文件。

持久化配置可以和 Confirm 模式配合使用。RabbitMQ 会在消息持久化到磁盘后再发送 ACK。如果持久化前服务宕机,生产者因未收到 ACK 会自动重发。


1.3 消费者环节:确保消息被成功消费

启用 接收方确认机制

消费者处理消息后,必须显式发送 ACK 确认,RabbitMQ 只有收到 ACK 后才会删除消息。

正常处理
channel.basicAck(deliveryTag, false);
处理失败
channel.basicNack(deliveryTag, false, true);

basicNack 后,RabbitMQ 可以将消息重新分发。

特殊情况
  • 如果消费者在确认前断开连接或取消订阅,RabbitMQ 会将消息重新分发给其他消费者。
  • 如果消费者未确认且连接未断开,RabbitMQ 会认为消费者繁忙,不再向其分发新消息。

2. RabbitMQ 如何实现延迟队列

RabbitMQ 本身未直接提供延迟队列功能,但可以通过以下两种核心方案实现:

  1. TTL + 死信交换机(DLX)
  2. 延迟消息插件 rabbitmq_delayed_message_exchange

适用于:

  • 订单超时取消。
  • 定时任务触发。
  • 延迟通知。

2.1 基于 TTL + 死信交换机(DLX)实现

原理

利用消息或队列的 TTL,以及死信交换机 DLX,让消息过期后自动路由到指定队列,从而实现延迟效果。

  • TTL:Time-To-Live,消息存活时间。
  • DLX:Dead Letter Exchange,死信交换机。

实现步骤

步骤一:声明死信交换机和死信队列

创建死信交换机,类型通常为 DirectTopic,并绑定死信队列。

消费者实际消费的是死信队列。

示例代码:

// 声明死信交换机
channel.exchangeDeclare("dlx_exchange", BuiltinExchangeType.DIRECT, true);

// 声明死信队列
channel.queueDeclare("dlx_queue", true, false, false, null);

// 绑定死信交换机和队列(routing key 为 "dlx_key")
channel.queueBind("dlx_queue", "dlx_exchange", "dlx_key");

步骤二:声明普通队列并关联死信交换机

普通队列设置以下参数:

参数说明
x-dead-letter-exchange死信交换机名称
x-dead-letter-routing-key死信路由键
x-message-ttl队列内所有消息的统一过期时间,单位毫秒,可选

示例代码:

Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx_exchange"); // 绑定死信交换机
args.put("x-dead-letter-routing-key", "dlx_key");   // 死信路由键
args.put("x-message-ttl", 5000);                     // 队列内所有消息 5 秒后过期

channel.queueDeclare("normal_queue", true, false, false, args);

步骤三:发送消息并设置 TTL

如果队列已经设置 x-message-ttl,消息会自动过期。

如果需要为单条消息设置独立 TTL,可以通过 expiration 属性指定。

单条消息 TTL 优先级高于队列 TTL。

示例代码:

AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .expiration("10000") // 单条消息 10 秒后过期,覆盖队列 TTL
    .build();

channel.basicPublish("", "normal_queue", props, "延迟消息内容".getBytes());

步骤四:消费死信队列消息

消息在普通队列中过期后,会被 RabbitMQ 自动转发到死信队列。

消费者从死信队列消费,即可实现延迟处理。


2.2 基于延迟消息插件实现

原理

通过官方插件 rabbitmq_delayed_message_exchange 提供的 x-delayed-message 类型交换机,直接支持按消息级别设置延迟时间。

这种方式可以避免 TTL + DLX 方式中的消息挤压问题。


实现步骤

步骤一:安装插件

下载对应 RabbitMQ 版本的插件,放置到 RabbitMQ 插件目录,然后执行命令启用:

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

步骤二:声明延迟交换机

交换机类型必须为:

x-delayed-message

并通过 x-delayed-type 参数指定底层路由类型,例如 DirectTopic

示例代码:

Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", BuiltinExchangeType.DIRECT.getType()); // 底层路由类型

channel.exchangeDeclare("delayed_exchange", "x-delayed-message", true, false, args);

步骤三:绑定队列到延迟交换机

与普通交换机绑定队列方式相同,指定 routing key。

示例代码:

channel.queueDeclare("delayed_queue", true, false, false, null);
channel.queueBind("delayed_queue", "delayed_exchange", "delayed_key");

步骤四:发送延迟消息

通过消息属性 x-delay 指定延迟时间,单位为毫秒。

消息会在延迟时间到达后,才被路由到队列。

示例代码:

AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .headers(Map.of("x-delay", 8000)) // 延迟 8 秒
    .build();

channel.basicPublish("delayed_exchange", "delayed_key", props, "延迟消息内容".getBytes());

2.3 两种方案对比

方案优点缺点适用场景
TTL + DLX原生支持,无需插件队列 TTL 不灵活;消息 TTL 可能导致消息挤压延迟时间固定的场景,例如固定 30 分钟后取消订单
延迟插件支持单条消息独立延迟时间;无消息挤压问题;可靠性更高需要安装插件,部分环境可能受限延迟时间灵活、消息量较大、对延迟准确性要求较高的场景

3. RabbitMQ 如何确保顺序消费

RabbitMQ 确保消息顺序消费的核心思路是:

保证消息在生产、存储、消费三个环节的顺序性一致。


3.1 核心实现原则

原则一:单队列存储

消息必须发送到同一个队列。

因为 RabbitMQ 中单个队列内的消息是按照 FIFO 顺序存储的。

如果消息分散到多个队列,不同队列的消息无法保证全局顺序。


原则二:单线程生产与消费
生产者

使用单线程发送消息,避免多线程并发发送导致消息顺序错乱。

消费者

同一队列仅由单个消费者实例消费,或者同一消费组内仅一个消费者处理该队列。

消费者内部也应采用单线程处理消息,避免多线程并行消费打乱顺序。


3.2 关键配置与实践

队列绑定策略

确保生产者将相关顺序消息路由到同一个队列。

可通过以下方式实现:

  • 固定 routing key。
  • Direct 交换机。
  • 精准路由策略。
关闭自动确认,采用手动确认

消费者开启手动 ACK 机制,确保消息处理完成后再确认。

如果消费失败,消息会重新入队,避免未处理完成的消息被后续消息覆盖。


三、RocketMQ

1. RocketMQ 如何确保消息不丢失

RocketMQ 确保消息不丢失,需要从以下三个环节进行保障:

  1. Producer:生产者。
  2. Broker:消息队列。
  3. Consumer:消费者。

1.1 Producer 环节:确保消息成功发送至 Broker

同步发送与重试机制

采用同步发送模式,即发送一条消息后等待 Broker 返回响应。

只有收到“发送成功”响应,状态为 OK,才继续发送下一条。

如果出现超时或失败,会触发重试,确保消息被 Broker 接收。

分布式事务消息投递

通过半消息确认和消息回查机制实现分布式事务消息,保证跨服务场景下消息的可靠投递。

发送状态查询

如果消息发送超时,可以通过查询日志 API 检查消息是否在 Broker 中存储成功,从而进一步确认发送状态。


1.2 Broker 环节:确保消息持久化与高可用

消息持久化至 CommitLog

消息到达 Broker 后,会被持久化到 CommitLog 日志文件中。

即使 Broker 宕机,未消费的消息也可以从 CommitLog 恢复,避免丢失。

同步刷盘策略

RocketMQ 提供两种刷盘策略:

刷盘策略说明适用场景
同步刷盘消息进入 Broker 内存后,必须刷写到 CommitLog 文件才算发送成功,然后 Broker 再向 Producer 返回 ACK。可靠性要求高的核心业务场景
异步刷盘消息进入内存即返回成功,由后台线程异步刷盘。吞吐量要求高、可接受少量丢失的场景

核心消息场景不建议使用异步刷盘,因为 Broker 断电可能丢失内存中未刷盘的消息。

高可用架构:多副本机制

Broker 支持多 Master 多 Slave 的同步双写模式。

消息会同时写入 Master 和 Slave 的 CommitLog。

如果 Master 宕机,Slave 可以切换为新 Master,避免消息丢失。


1.3 Consumer 环节:确保消息被成功消费

集群消费模式

RocketMQ 默认采用集群消费模式。

同一消费组内的多个消费者共同消费队列消息。

如果某个消费者挂掉,其他消费者会接替其消费任务,避免消息因单个消费者故障而未被处理。


2. RocketMQ 如何实现延迟队列

RocketMQ 原生支持延迟队列,通过定时消息机制实现,无需额外插件。

适用于:

  • 订单超时取消。
  • 定时任务触发。
  • 消息重试机制。

2.1 核心实现原理

RocketMQ 的延迟队列基于定时消息机制。

消息发送后不会立即投递到目标队列,而是先存储在延迟消息专用队列中。

到达指定延迟时间后,由内部定时任务转发至目标队列,供消费者消费。


2.2 关键流程

  1. 生产者发送延迟消息:指定消息的延迟级别,而不是直接设置任意延迟时间。
  2. Broker 存储延迟消息:消息暂存于内部延迟队列,系统主题为 SCHEDULE_TOPIC_XXXX,并记录目标投递时间。
  3. 定时任务扫描转发:Broker 定时扫描延迟队列,将到达投递时间的消息转发至目标主题的实际队列。
  4. 消费者消费消息:消息进入目标队列后,消费者正常消费。

2.3 使用步骤

步骤一:生产者发送延迟消息

通过 Message 对象的 setDelayTimeLevel(int level) 方法设置延迟级别。

示例代码:

// 创建消息(目标主题为 "order_topic")
Message message = new Message(
    "order_topic",
    "order_tag",
    "order_id_123",
    "订单超时取消".getBytes()
);

// 设置延迟级别为 3(对应延迟 10 秒)
message.setDelayTimeLevel(3);

// 发送消息(同步发送确保可靠性)
SendResult sendResult = producer.send(message);

步骤二:消费者消费目标队列消息

消费者正常订阅目标主题,无需额外配置延迟逻辑。

示例代码:

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");
consumer.subscribe("order_topic", "order_tag");

consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
    for (MessageExt msg : msgs) {
        System.out.println("消费延迟消息:" + new String(msg.getBody()));
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});

consumer.start();

2.4 延迟级别定义

RocketMQ 不支持任意延迟时间,而是通过预定义延迟级别实现。

默认提供 18 个级别,可通过 Broker 配置修改。

延迟级别延迟时间延迟级别延迟时间
11s106min
25s117min
310s128min
430s139min
51min1410min
62min1520min
73min1630min
84min171h
95min182h
自定义延迟级别

修改 Broker 配置文件 rocketmq-broker.conf

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

格式为空格分隔的时间字符串,单位支持 smhd,重启 Broker 后生效。


2.5 底层存储与转发机制

延迟消息暂存

延迟消息发送时,会被重定向到系统延迟主题:

SCHEDULE_TOPIC_XXXX

其中 XXXX 为延迟级别对应的队列 ID。

消息内容中包含原始目标主题和队列信息。

定时任务扫描

Broker 内部启动定时调度线程 ScheduleMessageService

默认每 100ms 扫描一次延迟队列,检查消息是否到达投递时间。

投递时间计算方式:

消息存储时间 + 延迟时间
消息转发

对到达投递时间的消息,Broker 会将其从 SCHEDULE_TOPIC_XXXX 转发到原始目标主题的队列中。

此时消息变为普通消息,消费者可以正常消费。


2.6 优缺点分析

优点缺点
原生支持,无需额外开发不支持任意延迟时间,仅支持预定义级别
基于 Broker 存储,可靠性高延迟精度受扫描间隔影响,默认约 100ms 误差
集成简单,API 友好大量延迟消息可能导致调度线程压力增大

3. RocketMQ 如何确保顺序消费

RocketMQ 确保顺序消费的核心是:

保证消息在生产、存储、消费三个环节的顺序一致性。


3.1 核心实现原则

单个队列的 FIFO 存储特性

RocketMQ 中,单个消息队列 Queue 内的消息严格遵循 FIFO 顺序。

也就是说,先发送的消息会先被存储和投递。

多个队列同时消费时,不同队列的消息无法保证全局顺序。

因此,需要确保相关顺序消息进入同一个队列。


确保消息发送到同一队列

通过 MessageQueueSelector 接口自定义队列选择算法,将需要保持顺序的消息路由到同一个队列。

例如,可以根据业务 ID,如订单 ID,进行哈希取模,确保同一 ID 的消息始终进入固定队列。

示例代码:

// 自定义队列选择器,确保同一业务 ID 的消息进入同一队列
MessageQueueSelector selector = (mqs, msg, arg) -> {
    String orderId = (String) arg; // 业务 ID,如订单号
    int hashCode = orderId.hashCode();
    int index = hashCode % mqs.size();
    return mqs.get(index);
};

// 发送消息时指定选择器和业务 ID 参数
producer.send(msg, selector, "ORDER_123");

单线程生产与消费
生产者

使用单线程发送消息,避免多线程并发发送导致消息顺序错乱。

消费者

同一队列仅由单个消费者线程处理,确保消费顺序与队列存储顺序一致。


四、Kafka

1. Kafka 如何确保消息不丢失

Kafka 通过以下机制确保消息不丢失:

  • 消息持久化。
  • 副本机制。
  • ACK 确认机制。
  • 生产者重试机制。

1.1 设置合适的 ACK 确认机制

通过 request.required.acks 参数控制消息发送的确认级别。

ACK 配置说明风险
acks=0生产者不等待 Broker 确认延迟最低,但可能丢失消息
acks=1Leader 副本接收消息后发送确认如果 Leader 宕机且未同步给 Follower,可能丢失消息
acks=-1acks=all所有 Follower 副本同步消息成功后,Leader 才发送确认可靠性最高

acks=-1acks=all 可以确保即使 Leader 宕机,新 Leader 也已经同步消息。


1.2 消息持久化存储

Kafka 将消息顺序写入磁盘日志文件。

Kafka 依赖磁盘持久化,而不是单纯依赖内存。

因此,即使 Broker 重启,消息也可以从磁盘恢复。


1.3 副本机制与集群复制

Kafka 通过 Leader-Follower 副本机制在集群内复制消息。

每个 Partition 有:

  • 1 个 Leader。
  • 多个 Follower。

消息需要同步到 Follower 副本,结合 acks=-1 可以防止单点故障导致数据丢失。


1.4 生产者重试机制

配置生产者重试参数,例如:

retries=3

当网络超时或 Broker 暂时不可用时,生产者可以自动重试发送消息,避免因瞬时故障导致消息丢失。


2. Kafka 如何实现延迟队列

Kafka 本身不原生支持延迟队列,需要通过以下方式间接实现:

  • 消息标记。
  • 主题设计。
  • 客户端逻辑。
  • 独立延迟服务。

核心思路是控制消息从生产到被消费的时间间隔。


2.1 方案一:多主题 + 固定延迟级别

原理

按照延迟时间创建多个固定延迟级别的主题,例如:

  • delay_10s
  • delay_5m
  • delay_1h

生产者根据业务需求的延迟时间,将消息发送到对应主题。

消费者直接消费这些主题。


实现步骤

步骤一:创建延迟主题

根据业务常见延迟需求创建多个主题。

每个主题对应固定延迟时间。

步骤二:生产者路由

发送消息时,根据目标延迟时间选择对应主题。

例如,需要延迟 5 分钟的消息发送到:

delay_5m

示例代码:

// 根据延迟时间选择主题
String topic = getDelayTopic(delayTime); // 如 delayTime=5000ms → 返回 "delay_5m"
producer.send(new ProducerRecord<>(topic, message));
步骤三:消费者直接消费

消费者订阅延迟主题。

消息写入后,经过固定时间后被消费。

优缺点

优点缺点
实现简单,无额外组件不支持任意延迟时间
适合延迟级别固定的场景主题数量随延迟级别增加而增多,维护成本高

适用场景:订单超时取消常用的固定延迟,例如 15 分钟、30 分钟。


2.2 方案二:单主题 + 时间轮转发

原理

引入一个独立延迟服务,通过「统一延迟主题 + 时间轮」实现任意延迟时间。

消息流转过程如下:

  1. 消息先发送到统一延迟主题。
  2. 延迟服务拉取消息。
  3. 延迟服务将消息暂存到时间轮。
  4. 消息到期后转发到业务主题。
  5. 业务消费者消费业务主题。

实现步骤

步骤一:创建统一延迟主题

所有延迟消息先发送到统一主题:

delay_topic
步骤二:延迟服务处理

延迟服务消费 delay_topic,消息中包含:

字段说明
target_topic目标业务主题
delay_timestamp延迟到期时间戳
步骤三:时间轮暂存

将消息按照 delay_timestamp 放入时间轮。

例如可以使用 Netty 的 HashedWheelTimer

时间轮定期检查到期消息。

步骤四:转发消息

消息到期后,延迟服务将消息发送到 target_topic,业务消费者消费 target_topic


时间轮工作原理

时间轮是一种高效的定时任务调度数据结构。

它通过「环形数组 + 槽位」管理延迟任务,支持 O(1) 插入和删除,适合大量延迟消息场景。

示例代码:

// 延迟服务核心逻辑
HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 3600); // 1 秒精度,3600 个槽位,覆盖 1 小时

consumer.subscribe(Collections.singletonList("delay_topic"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        long delayTimestamp = parseDelayTimestamp(record.value()); // 从消息中解析到期时间戳
        String targetTopic = parseTargetTopic(record.value());     // 解析目标业务主题

        // 计算剩余延迟时间:当前时间到到期时间的差值
        long delay = delayTimestamp - System.currentTimeMillis();

        if (delay <= 0) {
            // 已到期,直接转发
            producer.send(new ProducerRecord<>(targetTopic, record.value()));
        } else {
            // 放入时间轮,到期后转发
            timer.newTimeout(timeout -> {
                producer.send(new ProducerRecord<>(targetTopic, record.value()));
            }, delay, TimeUnit.MILLISECONDS);
        }
    }
}

优缺点

优点缺点
支持任意延迟时间需要额外维护延迟服务
主题数量少存在单点风险,需要集群化部署
灵活性高转发过程可能引入消息重复,需要业务端幂等处理

2.3 方案三:消息标记 + 消费者过滤(不推荐)

原理

所有消息发送到同一业务主题,消息中携带 delay_timestamp

消费者拉取消息后检查是否到达延迟时间。

  • 如果已到期,则消费。
  • 如果未到期,则暂存本地,例如内存或 Redis,之后定期重试。

缺点

  • 消费者需要频繁拉取未到期消息,浪费资源。
  • 消费者重启后暂存消息可能丢失,需要额外持久化。
  • 多消费者实例之间难以协调,可能重复处理。

2.4 关键注意事项

消息可靠性

延迟服务需要开启 Kafka 消费者手动提交偏移量:

enable.auto.commit=false

确保消息处理完成后再提交,避免消息丢失。

同时,时间轮需要持久化,例如结合 Redis 或 RocksDB,防止服务重启后未到期消息丢失。

延迟精度

延迟精度受以下因素影响:

  • 时间轮精度,例如 1 秒或 100ms。
  • Kafka 拉取间隔。

实际延迟时间可能存在约 ±1 个时间单位误差。

消息去重

延迟服务转发消息时,可能因为网络重试导致重复。

业务端需要通过消息唯一 ID,例如 messageId,实现幂等处理。


3. Kafka 如何确保顺序消费

Kafka 确保顺序消费的核心是:

基于 Partition 的 FIFO 特性,以及消息路由策略。


3.1 单个 Partition 的 FIFO 存储

Kafka 分布式存储的基本单位是 Partition。

同一个 Partition 内的消息通过 Write Ahead Log 组织,严格遵循 FIFO 顺序。

也就是说,先发送的消息会先被存储和投递。

因此,Kafka 可以保证单个 Partition 内的消息顺序。


3.2 确保相关消息进入同一 Partition

可以通过以下方式将需要保持顺序的消息路由到同一个 Partition:

方式说明
指定 Partition发送消息时直接指定 partition 参数,所有消息发往同一个 Partition,天然保证顺序。
指定 Key如果未指定 Partition,Kafka 会根据消息的 key 进行哈希计算,相同 Key 的消息会被路由到同一个 Partition。

常见 Key 示例:

  • 业务 ID。
  • 订单号。
  • 用户 ID。

3.3 消费端单 Partition 单 Consumer 处理

Kafka 保证一个 Partition 只能被同一个 Consumer Group 中的一个 Consumer 实例消费。

这样可以避免多个 Consumer 并发处理同一个 Partition,从而导致顺序错乱。


五、对比总结

1. 延迟队列实现方式对比

中间件是否原生支持延迟队列常见实现方式适合场景
Redis不直接原生支持Sorted Set + 时间戳轮询简单延时任务、轻量订单超时处理
RabbitMQ不直接原生支持TTL + DLX、延迟消息插件订单超时取消、定时通知
RocketMQ原生支持延迟级别、定时消息机制电商订单、事务消息、重试机制
Kafka不原生支持多主题、延迟服务、时间轮大吞吐场景下的延迟消息转发

2. 顺序消费实现方式对比

中间件顺序保证单位核心做法
Redis List单个 List使用 List FIFO 特性
RabbitMQ单个 Queue单队列、单消费者、手动 ACK
RocketMQ单个 Queue相同业务 ID 路由到同一队列,单线程消费
Kafka单个 Partition相同 Key 路由到同一 Partition,单 Partition 单 Consumer

3. 消息不丢失保障对比

中间件生产端保障存储端保障消费端保障
RabbitMQConfirm 模式队列持久化 + 消息持久化手动 ACK
RocketMQ同步发送 + 重试CommitLog + 同步刷盘 + 多副本集群消费 + 消费确认
Kafkaacks=all + retries磁盘日志 + 副本机制手动提交 offset

六、面试回答记忆版

1. 消息不丢失怎么回答?

可以按照三个环节回答:

  1. 生产者:确认机制、同步发送、失败重试。
  2. Broker / 队列:持久化、刷盘、副本机制。
  3. 消费者:手动 ACK、失败重试、幂等处理。

2. 延迟队列怎么回答?

可以按照中间件分别回答:

  • Redis:Sorted Set,score 存执行时间戳。
  • RabbitMQ:TTL + DLX,或者延迟插件。
  • RocketMQ:原生延迟级别。
  • Kafka:不原生支持,一般用多主题或时间轮延迟服务。

3. 顺序消费怎么回答?

核心原则:

让同一业务维度的消息进入同一个有序存储单元,并由单线程或单消费者顺序处理。

不同中间件对应:

  • RabbitMQ:同一个 Queue。
  • RocketMQ:同一个 MessageQueue。
  • Kafka:同一个 Partition。
  • Redis:同一个 List。