本文 的 原文 地址
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的面试题:
如何根据应用场景选择合适的消息中间件?
Rocketmq消息0丢失,如何实现?
Rocketmq如何保证消息可靠?
对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
最近有小伙伴在面试美团,遇到了相关的面试题, 小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
三大MQ指标对比
分布式、微服务、高并发架构中,消息队列(Message Queue,简称MQ)扮演着至关重要的角色。
消息队列用于实现系统间的异步通信、解耦、削峰填谷等功能。
对比指标 | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|
应用场景 | 中小规模应用场景 | 分布式事务、实时日志处理 | 大规模数据处理、实时流处理 |
开发语言 | Erlang | Java | Scala & Java |
消息可靠性 | 最高 (AMQP协议保证) | 较高 (基于事务保证) | 中等 (基于副本机制保证) |
消息吞吐量 | 低 万级到十万级 | 中等 十万级到百万级 | 高 百万级或更高 |
时效性 | 毫秒级 | 毫秒级 | 毫秒级 |
支持的语言和平台 | Java、C++、Python等 | Java、C++、Go等 | Java、Scala、Python等 |
架构模型 | virtual host、broker、exchange、queue | nameserver、controller、broker | broker、topic、partition、zookeeper/Kraft |
社区活跃度和生态建设 | 中等 活跃的开源社区和丰富的插件生态系统 | 较高 阿里巴巴开源,稳定的社区支持 | 最高 活跃的开源社区和广泛的应用 |
github star | 10.8k | 19.4k | 25.2k |
RocketMQ、Kafka、RabbitMQ,如何选型?
RocketMQ、Kafka、RabbitMQ,如何选型?
最为详细的方案,请参考尼恩团队的架构方案: 招行面试:RocketMQ、Kafka、RabbitMQ,如何选型?
对比分析三大MQ常见问题
下面, 对比分析三大MQ常见问题。
消息丢失问题
1、RocketMQ解决消息丢失问题:
生产端: 采用同步发送(等待Broker确认)并启用重试机制,结合事务消息(如预提交half消息+二次确认commit)确保消息可靠投递。
Broker端:配置同步刷盘(消息写入磁盘后返回确认)和多副本同步机制(主从节点数据冗余)防止宕机丢失,同时通过集群容灾保障高可用。
消费端:消费者需手动ACK确认,失败时触发自动重试(默认16次),最终失败消息转入死信队列人工处理,避免异常场景下消息丢失。
最为详细的方案,请参考尼恩团队的架构方案: 滴滴面试:Rocketmq消息0丢失,如何实现?
2、Kafka解决消息丢失问题:
生产端:设置acks=all
确保消息被所有副本持久化后才响应,启用生产者重试(retries
)及幂等性(enable.idempotence=true
)防止网络抖动或Broker异常导致丢失
Broker端:配置多副本同步(min.insync.replicas≥2
)和ISR(In-Sync Replicas)机制,仅同步成功的副本参与选举;避免unclean.leader.election.enable=true
(防止数据不全的副本成为Leader)
消费端:关闭自动提交位移(enable.auto.commit=false
),手动同步提交(commitSync
)确保消息处理完成后再更新位移,结合消费重试及死信队列兜底
最为详细的方案,请参考尼恩团队的架构方案: 得物面试:消息0丢失,Kafka如何实现?
3、RabbitMQ解决消息丢失问题:
生产端:启用Publisher Confirm模式(异步确认消息持久化)并设置mandatory=true
路由失败回退,结合备份交换机处理无法路由的消息;事务消息因性能损耗仅限关键场景使用。
Broker端:消息与队列均需持久化(durable=true
)防止宕机丢失,部署镜像队列集群实现多节点冗余;同步刷盘策略确保数据落盘后响应。
消费端:关闭自动ACK,采用手动ACK并在业务处理成功后提交确认;消费失败时重试(重试次数可配置)并最终转入死信队列人工干预,避免消息因异常未处理而丢失。
消息积压问题
1、RocketMQ解决消息积压问题:
RocketMQ通过横向扩展(增加消费者实例、队列数量)、提升消费能力(线程池调优、批量消费)、动态扩容、消息预取、死信队列隔离无效消息,并支持消费限流及监控告警,快速定位处理积压问题。
RocketMQ还提供了消息拉取和推拉模式,消费者可以根据自身的处理能力主动拉取消息,避免消息积压过多。
最为详细的方案,请参考尼恩团队的架构方案: 阿里面试:如何保证RocketMQ消息有序?如何解决RocketMQ消息积压?
2、Kafka解决消息积压问题
Kafka通过 横向扩展(增加分区及消费者实例)、优化消费者参数(如批量拉取、并发处理)、提升消费逻辑效率(异步化、减少I/O),并动态监控消费滞后指标。
必要时限流生产者或临时扩容消费组,结合分区再平衡策略快速分发积压消息负载。
Kafka还提供了消息清理(compaction)和数据保留策略,可以根据时间或者数据大小来自动删除过期的消息,避免消息积压过多。
3、RabbitMQ解决消息积压问题
RabbitMQ通过调整消费者的消费速率来控制消息积压。
可以使用QoS(Quality of Service)机制设置每个消费者的预取计数,限制每次从队列中获取的消息数量,以控制消费者的处理速度。
RabbitMQ还支持消费者端的流量控制,通过设置basic.qos或basic.consume命令的参数来控制消费者的处理速度,避免消息过多导致积压。
消息重复消费问题
1、RocketMQ解决消息重复消费问题
-
使用消息唯一标识符(Message ID):在消息发送时,为每条消息附加一个唯一标识符。消费者在处理消息时,可以通过判断消息唯一标识符来避免重复消费。可以将消息ID记录在数据库或缓存中,用于去重检查。
-
消费者端去重处理:消费者在消费消息时,可以通过维护一个已消费消息的列表或缓存,来避免重复消费已经处理过的消息。
2、Kafka解决消息重复消费问题
- 幂等性处理:在消费者端实现幂等性逻辑,即多次消费同一条消息所产生的结果与单次消费的结果一致。这可以通过在业务逻辑中引入唯一标识符或记录已处理消息的状态来实现。
- 消息确认机制:消费者在处理完消息后,提交已消费的偏移量(Offset)给Kafka,Kafka会记录已提交的偏移量,以便在消费者重新启动时从正确的位置继续消费。消费者可以定期提交偏移量,确保消息只被消费一次。
3、RabbitMQ解决消息重复消费问题
-
幂等性处理:在消费者端实现幂等性逻辑,即无论消息被消费多少次,最终的结果应该保持一致。这可以通过在消费端进行唯一标识的检查或者记录已经处理过的消息来实现。
-
消息确认机制:消费者在处理完消息后,发送确认消息(ACK)给RabbitMQ,告知消息已经成功处理。RabbitMQ根据接收到的确认消息来判断是否需要重新投递消息给其他消费者。
最为详细的方案,请参考尼恩团队的架构方案: 最系统的幂等性方案:一锁二判三更新
消息有序性
1、Rabbitmq 解决有序性问题
模式一:单队列单消费者模式
-
将需要保证顺序的消息全部发送到同一个队列,且消费者设置为单线程处理。
-
原理:RabbitMQ 队列天然支持 FIFO 顺序存储,单消费者避免并发处理导致乱序。
示例代:
// 生产者发送到同一队列
rabbitTemplate.convertAndSend("order.queue", "message1");
rabbitTemplate.convertAndSend("order.queue", "message2");
// 消费者单线程监听
@RabbitListener(queues = "order.queue")
public void processOrder(String message) {
// 顺序处理逻辑
}
缺点:无法横向扩展消费者,吞吐量受限。
模式二:消息分组策略
按业务标识分区(如订单 ID、用户 ID),相同分组的消息路由到同一队列,每个队列对应一个消费者。
实现方式: 生产者通过哈希算法或自定义路由键将关联的消息分配到特定队列。
- 生产者根据业务标识生成路由键,如
routingKey = orderId.hashCode() % queueCount
。 - 声明多个队列,绑定到同一交换机,并根据路由键规则分发消息。
代码示例:
// 生产者发送消息时指定路由键
String orderId = "ORDER_1001";
String routingKey = "order." + (orderId.hashCode() % 3); // 分配到3个队列之一
rabbitTemplate.convertAndSend("order.exchange", routingKey, message);
优势:在保证同分组顺序性的同时,允许不同分组并行处理。
消费者并发控制 设置
prefetchCount=1
确保每次只处理一个消息,关闭自动应答,手动确认后再获取新消息:
spring:
rabbitmq:
listener:
simple:
prefetch: 1
效果:防止消费者同时处理多个消息导致乱序。
2、RocketMQ解决有序性问题
RocketMQ实现顺序消息的核心是通过生产端和消费端双重保障:
-
全局顺序需单队列(性能受限),分区顺序通过Sharding Key哈希分散到不同队列,兼顾吞吐量与局部有序性。需避免异步消费、消息重试乱序,失败时跳过当前消息防止阻塞
-
生产者使用MessageQueueSelector将同一业务标识(如订单ID)的消息强制路由至同一队列,利用队列FIFO特性保序;
-
消费端对 同一队列启用 单线程拉取 + 分区锁机制(ConsumeOrderlyContext),确保串行处理。
最为详细的方案,请参考尼恩团队的架构方案:
3、Kafka解决有序性问题
Kafka实现顺序消息的核心在于分区顺序性:
- 生产端:相同业务标识(如订单ID)的消息通过固定Key哈希至同一分区(
Partitioner
),利用分区内消息天然有序性保序; - 消费端:每个分区仅由同一消费者组的一个线程消费(单线程串行处理),避免并发消费乱序;
事务消息
1、RabbitMQ的事务消息
-
RabbitMQ支持事务消息的发送和确认。在发送消息之前,可以通过调用"channel.txSelect()"来开启事务,然后将要发送的消息发布到交换机中。如果事务成功提交,消息将被发送到队列,否则事务会回滚,消息不会被发送。
-
在消费端,可以通过"channel.txSelect()"开启事务,然后使用"basicAck"手动确认消息的处理结果。如果事务成功提交,消费端会发送ACK确认消息的处理;否则,事务回滚,消息将被重新投递。
public class RabbitMQTransactionDemo {
private static final String QUEUE_NAME = "transaction_queue";
public static void main(String[] args) {
try {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// 创建连接
Connection connection = factory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
try {
// 开启事务
channel.txSelect();
// 发送消息
String message = "Hello, RabbitMQ!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
// 提交事务
channel.txCommit();
} catch (Exception e) {
// 事务回滚
channel.txRollback();
e.printStackTrace();
}
// 关闭信道和连接
channel.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、RocketMQ的事务消息
RocketMQ提供了事务消息的机制,确保消息的可靠性和一致性。
发送事务消息时,需要将消息发送到半消息队列,然后执行本地事务逻辑。
事务执行成功后,通过调用"TransactionStatus.CommitTransaction"提交事务消息;若事务执行失败,则通过调用"TransactionStatus.RollbackTransaction"回滚事务消息。
事务消息的最终状态由消息生产者根据事务执行结果进行确认。
public class RocketMQTransactionDemo {
public static void main(String[] args) throws Exception {
// 创建事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("group_name");
producer.setNamesrvAddr("localhost:9876");
// 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务逻辑,根据业务逻辑结果返回相应的状态
// 返回 LocalTransactionState.COMMIT_MESSAGE 表示事务提交
// 返回 LocalTransactionState.ROLLBACK_MESSAGE 表示事务回滚
// 返回 LocalTransactionState.UNKNOW 表示事务状态未知
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 根据消息的状态,来判断本地事务的最终状态
// 返回 LocalTransactionState.COMMIT_MESSAGE 表示事务提交
// 返回 LocalTransactionState.ROLLBACK_MESSAGE 表示事务回滚
// 返回 LocalTransactionState.UNKNOW 表示事务状态未知
}
});
// 启动事务消息生产者
producer.start();
// 构造消息
Message msg = new Message("topic_name", "tag_name", "Hello, RocketMQ!".getBytes());
// 发送事务消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.println("Send Result: " + sendResult);
// 关闭事务消息生产者
producer.shutdown();
}
}
3、Kafka的事务消息
Kafka引入了事务功能来确保消息的原子性和一致性。事务消息的发送和确认在生产者端进行。
生产者可以通过初始化事务,将一系列的消息写入事务,然后通过"commitTransaction()"提交事务,或者通过"abortTransaction()"中止事务。
Kafka会保证在事务提交之前,写入的所有消息不会被消费者可见,以保持事务的一致性。
public class KafkaTransactionDemo {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transactional_id");
Producer<String, String> producer = new KafkaProducer<>(props);
// 初始化事务
producer.initTransactions();
try {
// 开启事务
producer.beginTransaction();
// 发送消息
ProducerRecord<String, String> record = new ProducerRecord<>("topic_name", "Hello, Kafka!");
producer.send(record);
// 提交事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
// 处理异常情况
producer.close();
} finally {
producer.close();
}
}
}
消息确认 ACK机制
1、RabbitMQ的ACK机制
RabbitMQ使用ACK(消息确认)机制来确保消息的可靠传递。
消费者收到消息后,需要向RabbitMQ发送ACK来确认消息的处理状态。
只有在收到ACK后,RabbitMQ才会将消息标记为已成功传递,否则会将消息重新投递给其他消费者或者保留在队列中。
以下是RabbitMQ ACK的Java示例:
public class RabbitMQAckDemo {
public static void main(String[] args) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// 创建连接
Connection connection = factory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
// 声明队列
String queueName = "queue_name";
channel.queueDeclare(queueName, false, false, false, null);
// 创建消费者
String consumerTag = "consumer_tag";
boolean autoAck = false; // 关闭自动ACK
// 消费消息
channel.basicConsume(queueName, autoAck, consumerTag, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 消费消息
String message = new String(body, "UTF-8");
System.out.println("Received message: " + message);
try {
// 模拟处理消息的业务逻辑
processMessage(message);
// 手动发送ACK确认消息
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
// 处理消息异常,可以选择重试或者记录日志等操作
System.out.println("Failed to process message: " + message);
e.printStackTrace();
// 手动发送NACK拒绝消息,并可选是否重新投递
long deliveryTag = envelope.getDeliveryTag();
boolean requeue = true; // 重新投递消息
channel.basicNack(deliveryTag, false, requeue);
}
}
});
}
private static void processMessage(String message) {
// 模拟处理消息的业务逻辑
}
}
2、RocketMQ的ACK机制
RocketMQ的ACK机制由消费者控制,消费者从消息队列中消费消息后,可以手动发送ACK确认消息的处理状态。
只有在收到ACK后,RocketMQ才会将消息标记为已成功消费,否则会将消息重新投递给其他消费者。
public class RocketMQAckDemo {
public static void main(String[] args) throws Exception {
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group_name");
consumer.setNamesrvAddr("localhost:9876");
// 订阅消息
consumer.subscribe("topic_name", "*");
// 注册消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt message : msgs) {
try {
// 消费消息
String msgBody = new String(message.getBody(), "UTF-8");
System.out.println("Received message: " + msgBody);
// 模拟处理消息的业务逻辑
processMessage(msgBody);
// 手动发送ACK确认消息
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
// 处理消息异常,可以选择重试或者记录日志等操作
System.out.println("Failed to process message: " + new String(message.getBody()));
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者
consumer.start();
}
private static void processMessage(String message) {
// 模拟处理消息的业务逻辑
}
}
3、Kafka的ACK机制
Kafka的ACK机制用于控制生产者在发送消息后,需要等待多少个副本确认才视为消息发送成功。
这个机制可以通过设置acks参数来进行配置。在Kafka中,acks参数有三个可选值:
acks=0:生产者在发送消息后不需要等待任何确认,直接将消息发送给Kafka集群。这种方式具有最高的吞吐量,但是也存在数据丢失的风险,因为生产者不会知道消息是否成功发送给任何副本。
acks=1:生产者在发送消息后只需要等待首领副本(leader replica)确认。一旦首领副本成功接收到消息,生产者就会收到确认。这种方式提供了一定的可靠性,但是如果首领副本在接收消息后但在确认之前发生故障,仍然可能会导致数据丢失。
acks=all:生产者在发送消息后需要等待所有副本都确认。只有当所有副本都成功接收到消息后,生产者才会收到确认。这是最安全的确认机制,确保了消息不会丢失,但是需要更多的时间和资源。acks=-1与acks=all是等效的。
public classKafkaProducerDemo{
public static void main(String[]args){
// 配置Kafka生产者的参数
Propertiesprops=newProperties();
props.put("bootstrap.servers","localhost:9092");// Kafka集群的地址和端口
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");// 键的序列化器
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");// 值的序列化器
props.put("acks","all");// 设置ACK机制为所有副本都确认
// 创建生产者实例
KafkaProducer<String,String>producer=newKafkaProducer<>(props);
// 构造消息
Stringtopic="my_topic";
Stringkey="my_key";
Stringvalue="Hello, Kafka!";
// 创建消息记录
ProducerRecord<String,String>record=newProducerRecord<>(topic,key,value);
// 发送消息
producer.send(record,newCallback(){
@Override
publicvoidonCompletion(RecordMetadatametadata,Exceptionexception){
if(exception!=null){
System.err.println("发送消息出现异常:"+exception.getMessage());
}else{
System.out.println("消息发送成功!位于分区 "+metadata.partition()+",偏移量 "+metadata.offset());
}
}
});
// 关闭生产者
producer.close();
}
}
延迟消息实现
延迟队列在实际项目中有非常多的应用场景,最常见的比如订单未支付,超时取消订单,在创建订单的时候发送一条延迟消息,达到延迟时间之后消费者收到消息,如果订单没有支付的话,那么就取消订单。
1、RocketMQ实现延迟消息
RocketMQ 默认时间间隔分为 18 个级别,基本上也能满足大部分场景的需要了。
默认延迟级别:
1s、 5s、 10s、 30s、 1m、 2m、 3m、 4m、 5m、 6m、 7m、 8m、 9m、 10m、 20m、 30m、 1h、 2h
使用起来也非常的简单,直接通过setDelayTimeLevel
设置延迟级别即可。
setDelayTimeLevel(level)
实现原理说起来比较简单,Broker 会根据不同的延迟级别创建出多个不同级别的队列,当我们发送延迟消息的时候,根据不同的延迟级别发送到不同的队列中,同时在 Broker 内部通过一个定时器去轮询这些队列(RocketMQ 会为每个延迟级别分别创建一个定时任务),如果消息达到发送时间,那么就直接把消息发送到指 topic 队列中。
RocketMQ 这种实现方式是放在服务端去做的,同时有个好处就是相同延迟时间的消息是可以保证有序性的。
谈到这里就顺便提一下关于消息消费重试的原理,这个本质上来说其实是一样的,对于消费失败需要重试的消息实际上都会被丢到延迟队列的 topic 里,到期后再转发到真正的 topic 中
2、RabbitMQ实现延迟消息
RabbitMQ本身并不存在延迟队列的概念,在 RabbitMQ 中是通过 DLX 死信交换机和 TTL 消息过期来实现延迟队列的。
TTL(Time to Live)过期时间
有两种方式可以设置 TTL。
(1) 通过队列属性设置,这样的话队列中的所有消息都会拥有相同的过期时间
(2) 对消息单独设置过期时间,这样每条消息的过期时间都可以不同
那么如果同时设置呢?这样将会以两个时间中较小的值为准。
针对队列的方式通过参数x-message-ttl
来设置。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 6000);
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
针对消息的方式通过setExpiration
来设置。
AMQP.BasicProperties properties = new AMQP.BasicProperties();
Properties.setDeliveryMode(2);
properties.setExpiration("60000");
channel.basicPublish(exchangeName, routingKey, mandatory, properties, "message".getBytes());
DLX(Dead Letter Exchange)死信交换机
一个消息要成为死信消息有 3 种情况:
(1) 消息被拒绝,比如调用reject
方法,并且需要设置requeue
为false
(2) 消息过期
(3) 队列达到最大长度
可以通过参数dead-letter-exchange
设置死信交换机,也可以通过参数dead-letter- exchange
指定 RoutingKey(未指定则使用原队列的 RoutingKey)。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "exchange.dlx");
args.put("x-dead-letter-routing-key", "routingkey");
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
实现原理
当我们对消息设置了 TTL 和 DLX 之后,当消息正常发送,通过 Exchange 到达 Queue 之后,由于设置了 TTL 过期时间,并且消息没有被消费(订阅的是死信队列),达到过期时间之后,消息就转移到与之绑定的 DLX 死信队列之中。
这样的话,就相当于通过 DLX 和 TTL 间接实现了延迟消息的功能,实际使用中我们可以根据不同的延迟级别绑定设置不同延迟时间的队列来达到实现不同延迟时间的效果。
如果队列通过 dead-letter-exchange 属性指定了一个交换机,那么该队列中的死信就会投递到这个交换机中,这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)
3、Kafka实现延迟消息
对于 Kafka 来说,原生并不支持延迟队列的功能,需要我们手动去实现,这里我根据 RocketMQ 的设计提供一个实现思路。
这个设计,我们也不支持任意时间精度的延迟消息,只支持固定级别的延迟,因为对于大部分延迟消息的场景来说足够使用了。
只创建一个 topic,但是针对该 topic 创建 18 个 partition,每个 partition 对应不同的延迟级别,这样做和 RocketMQ 一样有个好处就是能达到相同延迟时间的消息达到有序性。
应用级 Kafka 延迟消息实现原理
首先创建一个单独针对延迟队列的 topic,同时创建 18 个 partition 针对不同的延迟级别
发送消息的时候根据延迟参数发送到延迟 topic 对应的 partition,对应的key
为延迟时间,同时把原 topic 保存到 header 中
ProducerRecord<Object, Object> producerRecord = new ProducerRecord<>("delay_topic", delayPartition, delayTime, data);
producerRecord.headers().add("origin_topic", topic.getBytes(StandardCharsets.UTF_8));
内嵌的consumer
单独设置一个ConsumerGroup
去消费延迟 topic 消息,消费到消息之后如果没有达到延迟时间那么就进行pause
,然后seek
到当前ConsumerRecord
的offset
位置,同时使用定时器去轮询延迟的TopicPartition
,达到延迟时间之后进行resume
如果达到了延迟时间,那么就获取到header
中的真实 topic ,直接转发
这里为什么要进行pause
和resume
呢?
因为如果不这样的话,如果超时未消费达到max.poll.interval.ms
最大时间(默认300s),那么将会触发 Rebalance。