本文档主要整理 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.1 生产者环节:确保消息成功发送至 RabbitMQ
方式一:使用 Confirm 模式(推荐)
原理
将信道设置为 Confirm 模式后,所有发布的消息会被分配唯一 ID。
当消息被投递到所有匹配队列,或者已经持久化到磁盘后,RabbitMQ 会向生产者发送 ACK 确认。
如果处理失败,则发送 Nack,生产者可根据 ACK/Nack 结果进行重试。
优势
- 异步确认。
- 吞吐量高于事务模式。
- 适合高并发场景。
方式二:事务模式(不推荐,仅作了解)
原理
发送消息前开启事务:
channel.txSelect();
发送成功后提交事务:
channel.txCommit();
发送失败则回滚事务:
channel.txRollback();
缺点
事务会阻塞信道,大幅降低吞吐量,实际应用中很少使用。
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 本身未直接提供延迟队列功能,但可以通过以下两种核心方案实现:
TTL + 死信交换机(DLX)。延迟消息插件 rabbitmq_delayed_message_exchange。
适用于:
- 订单超时取消。
- 定时任务触发。
- 延迟通知。
2.1 基于 TTL + 死信交换机(DLX)实现
原理
利用消息或队列的 TTL,以及死信交换机 DLX,让消息过期后自动路由到指定队列,从而实现延迟效果。
- TTL:
Time-To-Live,消息存活时间。 - DLX:
Dead Letter Exchange,死信交换机。
实现步骤
步骤一:声明死信交换机和死信队列
创建死信交换机,类型通常为 Direct 或 Topic,并绑定死信队列。
消费者实际消费的是死信队列。
示例代码:
// 声明死信交换机
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 参数指定底层路由类型,例如 Direct 或 Topic。
示例代码:
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 确保消息不丢失,需要从以下三个环节进行保障:
- Producer:生产者。
- Broker:消息队列。
- 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 关键流程
- 生产者发送延迟消息:指定消息的延迟级别,而不是直接设置任意延迟时间。
- Broker 存储延迟消息:消息暂存于内部延迟队列,系统主题为
SCHEDULE_TOPIC_XXXX,并记录目标投递时间。 - 定时任务扫描转发:Broker 定时扫描延迟队列,将到达投递时间的消息转发至目标主题的实际队列。
- 消费者消费消息:消息进入目标队列后,消费者正常消费。
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 配置修改。
| 延迟级别 | 延迟时间 | 延迟级别 | 延迟时间 |
|---|---|---|---|
| 1 | 1s | 10 | 6min |
| 2 | 5s | 11 | 7min |
| 3 | 10s | 12 | 8min |
| 4 | 30s | 13 | 9min |
| 5 | 1min | 14 | 10min |
| 6 | 2min | 15 | 20min |
| 7 | 3min | 16 | 30min |
| 8 | 4min | 17 | 1h |
| 9 | 5min | 18 | 2h |
自定义延迟级别
修改 Broker 配置文件 rocketmq-broker.conf:
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
格式为空格分隔的时间字符串,单位支持
s、m、h、d,重启 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=1 | Leader 副本接收消息后发送确认 | 如果 Leader 宕机且未同步给 Follower,可能丢失消息 |
acks=-1 或 acks=all | 所有 Follower 副本同步消息成功后,Leader 才发送确认 | 可靠性最高 |
acks=-1 或 acks=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_10sdelay_5mdelay_1h
生产者根据业务需求的延迟时间,将消息发送到对应主题。
消费者直接消费这些主题。
实现步骤
步骤一:创建延迟主题
根据业务常见延迟需求创建多个主题。
每个主题对应固定延迟时间。
步骤二:生产者路由
发送消息时,根据目标延迟时间选择对应主题。
例如,需要延迟 5 分钟的消息发送到:
delay_5m
示例代码:
// 根据延迟时间选择主题
String topic = getDelayTopic(delayTime); // 如 delayTime=5000ms → 返回 "delay_5m"
producer.send(new ProducerRecord<>(topic, message));
步骤三:消费者直接消费
消费者订阅延迟主题。
消息写入后,经过固定时间后被消费。
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,无额外组件 | 不支持任意延迟时间 |
| 适合延迟级别固定的场景 | 主题数量随延迟级别增加而增多,维护成本高 |
适用场景:订单超时取消常用的固定延迟,例如 15 分钟、30 分钟。
2.2 方案二:单主题 + 时间轮转发
原理
引入一个独立延迟服务,通过「统一延迟主题 + 时间轮」实现任意延迟时间。
消息流转过程如下:
- 消息先发送到统一延迟主题。
- 延迟服务拉取消息。
- 延迟服务将消息暂存到时间轮。
- 消息到期后转发到业务主题。
- 业务消费者消费业务主题。
实现步骤
步骤一:创建统一延迟主题
所有延迟消息先发送到统一主题:
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. 消息不丢失保障对比
| 中间件 | 生产端保障 | 存储端保障 | 消费端保障 |
|---|---|---|---|
| RabbitMQ | Confirm 模式 | 队列持久化 + 消息持久化 | 手动 ACK |
| RocketMQ | 同步发送 + 重试 | CommitLog + 同步刷盘 + 多副本 | 集群消费 + 消费确认 |
| Kafka | acks=all + retries | 磁盘日志 + 副本机制 | 手动提交 offset |
六、面试回答记忆版
1. 消息不丢失怎么回答?
可以按照三个环节回答:
- 生产者:确认机制、同步发送、失败重试。
- Broker / 队列:持久化、刷盘、副本机制。
- 消费者:手动 ACK、失败重试、幂等处理。
2. 延迟队列怎么回答?
可以按照中间件分别回答:
- Redis:Sorted Set,score 存执行时间戳。
- RabbitMQ:TTL + DLX,或者延迟插件。
- RocketMQ:原生延迟级别。
- Kafka:不原生支持,一般用多主题或时间轮延迟服务。
3. 顺序消费怎么回答?
核心原则:
让同一业务维度的消息进入同一个有序存储单元,并由单线程或单消费者顺序处理。
不同中间件对应:
- RabbitMQ:同一个 Queue。
- RocketMQ:同一个 MessageQueue。
- Kafka:同一个 Partition。
- Redis:同一个 List。