环境配置
RocketMQ版本:5.2.0
RocketMQ SDK版本:5.2.0
引入依赖
implementation 'org.apache.rocketmq:rocketmq-client:5.2.0'
对于RocketMQ的四种消息的消费上一篇文章已经有过介绍,这里主要介绍上一篇文章没有的。
消费者分类
Apache RocketMQ 支持 PushConsumer 、 SimpleConsumer 以及 PullConsumer 这三种类型的消费者。PullConsumer 仅推荐在流处理框架中集成使用,大多数消息收发场景使用 PushConsumer 和 SimpleConsumer 就可以满足需求。生产环境中相同的 ConsumerGroup 下严禁混用 PullConsumer 和其他两种消费者,否则会导致消息消费异常。
PushConsumer
PushConsumers是一种高度封装的消费者类型,消费消息仅通过消费监听器处理业务并返回消费结果。消息的获取、消费状态提交以及消费重试都通过 Apache RocketMQ 的客户端SDK完成。
public class AsyncPushConsumer {
public static void main(String[] args) throws MQClientException {
// 定义一个push消费者,有监听器就是推模式
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 指定nameServer
consumer.setNamesrvAddr("127.0.0.1:9876");
// 指定消费的位置
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET );
// 指定Topic和过滤表达式
consumer.subscribe("OneWayTopic", "*");
// 注册一个消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
// 逐条消费消息
for (MessageExt msg : msgs) {
System.out.println("消费消息:"+ new String(msg.getBody()));
}
//消费完消息响应broker
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//集群消费
//consumer.setMessageModel(MessageModel.CLUSTERING);
//广播消费
//consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.start();
System.out.println("consumer started!");
}
}
SimpleConsumer
SimpleConsumer 是一种接口原子型的消费者类型,消息的获取、消费状态提交以及消费重试都是通过消费者业务逻辑主动发起调用完成。
需要提前引入依赖(注意是 rocketmq-client-java):
implementation 'org.apache.rocketmq:rocketmq-client-java:5.0.5'
代码:
ClientServiceProvider provider = ClientServiceProvider.loadService();
StaticSessionCredentialsProvider staticSessionCredentialsProvider = new StaticSessionCredentialsProvider("accessKey", "secretKey");
//MQ5.0,集群模式这里要连proxy的对外暴露端口
String endpoint = "localhost:9081";
ClientConfiguration configuration = ClientConfiguration.newBuilder()
.setEndpoints(endpoint)// 设置接入点
.setCredentialProvider(staticSessionCredentialsProvider)// 设置权限信息
.build();
// 主题名称
String topic = "topicName";
// 订阅消息的过滤规则,“*”表示订阅多有tag消息
FilterExpression filterExpression = new FilterExpression("*", FilterExpressionType.TAG);
SimpleConsumer simpleConsumer = provider.newSimpleConsumerBuilder()
.setConsumerGroup("consumerGroupName")// 设置消费者分组
.setClientConfiguration(configuration)
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))// 设置预绑定的订阅关系
.setAwaitDuration(Duration.ofSeconds(30))// 设置从服务端接收消息的最大等待时间
.build();
try {
int maxMessageNum = 10;// 每个长轮询获取的最大消息数量
Duration invisibleDuration = Duration.ofSeconds(10);// 消息不可见时间
do{
// SimpleConsumer 需要主动获取消息视图,并处理
List<MessageView> messageViewList = simpleConsumer.receive(maxMessageNum, invisibleDuration);
messageViewList.forEach(messageView -> {
try {
// 业务代码
... ...
// 消费处理完成后,需要主动调用 ACK 提交消费结果
simpleConsumer.ack(messageView);
} catch (ClientException e) {
logger.error("Failed to ack message, messageId={}", messageView.getMessageId(), e);
}
});
}while(true);
} catch (ClientException e) {
// 如果遇到系统流控等原因造成拉取失败,需要重新发起获取消息请求。
logger.error("Failed to receive message", e);
}
SimpleConsumer适用场景 :
(1)消息处理时长不可控
如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。建议使用SimpleConsumer消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改
(2)需要异步化、批量消费等高级定制场景
SimpleConsumer在SDK内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景
(3)需要自定义消费速率
SimpleConsumer是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率
PullConsumer
在RocketMQ中有两种Pull方式:
- 一种是比较原始Pull Consumer,它不提供相关的订阅方法,需要调用pull方法时指定队列进行拉取,并需要自己更新位点。
- 另一种是Lite Pull Consumer,它提供了Subscribe和Assign两种方式,使用起来更加方便。
原始Pull Consumer:
public class PullConsumerTest {
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.start();
try {
MessageQueue mq = new MessageQueue();
mq.setQueueId(0);
mq.setTopic("TopicTest");
mq.setBrokerName("jinrongtong-MacBook-Pro.local");
long offset = 26;
PullResult pullResult = consumer.pull(mq, "*", offset, 32);
if (pullResult.getPullStatus().equals(PullStatus.FOUND)) {
System.out.printf("%s%n", pullResult.getMsgFoundList());
consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
}
} catch (Exception e) {
e.printStackTrace();
}
consumer.shutdown();
}
}
Lite Pull Consumer
Assign模式一开始仍然是初始化DefaultLitePullConsumer,这里我们采用手动提交位点的方式,因此设置AutoCommit为false, 然后启动consumer。与Subscribe模式不同的是,Assign模式下没有自动的负载均衡机制,需要用户自行指定需要拉取的队列,因此在例子中,先用fetchMessageQueues获取了Topic下的队列,再取前面的一半队列进行拉取,示例中还调用了seek方法,将第一个队列拉取的位点设置从10开始。 紧接着进入循环不停地调用poll方法拉取消息,拉取到消息后调用commitSync方法手动提交位点。
Assign模式:
package com.fdw.rocketmq.consumer;
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class LitePullConsumerAssign {
public static volatile boolean running = true;
public static void main(String[] args) throws Exception {
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("please_rename_unique_group_name");
litePullConsumer.setAutoCommit(false);
litePullConsumer.start();
Collection<MessageQueue> mqSet = litePullConsumer.fetchMessageQueues("TopicTest");
List<MessageQueue> list = new ArrayList<>(mqSet);
List<MessageQueue> assignList = new ArrayList<>();
for (int i = 0; i < list.size() / 2; i++) {
assignList.add(list.get(i));
}
litePullConsumer.assign(assignList);
litePullConsumer.seek(assignList.get(0), 10);
try {
while (running) {
List<MessageExt> messageExts = litePullConsumer.poll();
System.out.printf("%s %n", messageExts);
litePullConsumer.commitSync();
}
} finally {
litePullConsumer.shutdown();
}
}
}
subscribe模式首先还是初始化DefaultLitePullConsumer并设置ConsumerGroupName,调用subscribe方法订阅topic并启动。与Push Consumer不同的是,LitePullConsumer拉取消息调用的是轮询poll接口,如果能拉取到消息则返回对应的消息列表,否则返回null。通过setPullBatchSize可以设置每一次拉取的最大消息数量,此外如果不额外设置,LitePullConsumer默认是自动提交位点。在subscribe模式下,同一个消费组下的多个LitePullConsumer会负载均衡消费,与PushConsumer一致。
subscribe模式:
package com.fdw.rocketmq.consumer;
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class LitePullConsumerSubscribe {
public static volatile boolean running = true;
public static void main(String[] args) throws Exception {
DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test");
litePullConsumer.subscribe("TopicTest", "*");
litePullConsumer.setPullBatchSize(20);
litePullConsumer.start();
try {
while (running) {
List<MessageExt> messageExts = litePullConsumer.poll();
System.out.printf("%s%n", messageExts);
}
} finally {
litePullConsumer.shutdown();
}
}
}
消费重试机制
1、无序消息的消费重试
对于无序消息(普通消息、延时消息、事务消息),当Consumer消费消息失败时,可以通过设置返回状态达到消息重试的效果。不过需要注意,无序消息的重试只对集群消费方式生效,广播消费方式不提供失败重试特性。即对于广播消费,消费失败后,失败消息不再重试,继续消费后续消息。
2、顺序消息的消费重试
对于顺序消息,当Consumer消费消息失败后,为了保证消息的顺序性,其会自动不断地进行消息重试,直到消费成功。消费重试默认间隔时间为1000毫秒。重试期间应用会出现消息消费被阻塞的情况。
如何设置?
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 顺序消息消费失败的消费重试时间间隔,单位毫秒,默认为1000,其取值范围为[10, 30000]
consumer.setSuspendCurrentQueueTimeMillis(100);
由于对顺序消息的重试是无休止的,不间断的,直至消费成功,所以,对于顺序消息的消费, 务必要保证应用能够及时监控并处理消费失败的情况,避免消费被永久性阻塞。
注意,顺序消息没有发送失败重试机制,但具有消费失败重试机制!
3、消费重试次数与间隔
对于无序消息集群消费下的重试消费,每条消息默认最多重试16次,但每次重试的间隔时间是不同的,会逐渐变长。每次重试的间隔时间如下表。
若一条消息在一直消费失败的前提下,将会在正常消费后的第4小时46分后进行第16次重试。
若仍然失败,则将消息投递到死信队列
修改最大重试次数:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
// 修改消费重试次数
consumer.setMaxReconsumeTimes(10);
对于修改过的重试次数,将按照以下策略执行:
- 若修改值小于16,则按照指定间隔进行重试
- 若修改值大于16,则超过16次的重试时间间隔均为2小时
- 对于Consumer Group,若仅修改了一个Consumer的消费重试次数,则会应用到该Group中所有 其它Consumer实例。若出现多个Consumer均做了修改的情况,则采用覆盖方式生效。即最后被修改的值会覆盖前面设置的值。
4、重试队列
对于需要重试消费的消息,并不是Consumer在等待了指定时长后再次去拉取原来的消息进行消费,而是将这些需要重试消费的消息放入到了一个特殊Topic的队列中,而后进行再次消费的。这个特殊的队列就是重试队列。
当出现需要进行重试消费的消息时,Broker会为每个消费组都设置一个Topic名称为%RETRY%consumerGroup@consumerGroup 的重试队列。
注意:
- 这个重试队列是针对消息组的,而不是针对每个Topic设置的(一个Topic的消息可以让多个消费者组进行消费,所以会为这些消费者组各创建一个重试队列)
- 只有当出现需要进行重试消费的消息时,才会为该消费者组创建重试队列
-
消费重试的时间间隔与延时消费的延时等级十分相似,除了没有延时等级的前两个时间 外,其它的时间都是相同的,Broker对于重试消息的处理是通过延时消息实现的。先将消息保存到SCHEDULE_TOPIC_XXXX延迟队列中,延迟时间到后,会将消息投递到%RETRY%consumerGroup@consumerGroup重试队列中。
![](https://img-blog.csdnimg.cn/direct/6b0f3191664744e496b4ab4f9c6c4e37.png)
5、消费重试配置
集群消费方式下,消息消费失败后若希望消费重试,则需要在消息监听器接口的实现中明确进行如下三种方式之一的配置:
- 方式1:返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)
- 方式2:返回Null
- 方式3:抛出异常
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try{
for (MessageExt message:list){
System.out.println("消费消息成功,消息用户属性:"+message.getUserProperty("age"));
}
}catch (Exception e){
// 方式一 重发
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
// 方式二 返回null
// return null;
// 方式三 抛出异常
// throw new RuntimeException("消费异常");
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
6、消费不重试配置
集群消费方式下,消息消费失败后若不希望消费重试,则在捕获到异常后同样也返回与消费成功后的相同的结果,即ConsumeConcurrentlyStatus.CONSUME_SUCCESS,则不进行消费重试。
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try{
for (MessageExt message:list){
System.out.println("消费消息成功,消息用户属性:"+message.getUserProperty("age"));
}
}catch (Exception e){
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
重置消费位点
点击重置消费位点,选择消费者
设置重置时间,消费位点就会被重置!
重置后程序自动开始重新消费
死信队列
当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。这个队列就是死信队列(Dead-Letter Queue,DLQ),而其中的消息则称为死信消息(Dead-Letter Message,DLM)。
死信队列是用于处理无法被正常消费的消息的。
死信队列特征
- 死信队列中的消息不会再被消费者正常消费,即DLQ对于消费者是不可见的
- 死信存储有效期与正常消息相同,均为 3 天(commitlog文件的过期时间),3 天后会被自动删除
- 死信队列就是一个特殊的Topic,名称为%DLQ%consumerGroup@consumerGroup ,即每个消费者组都有一个死信队列
- 如果⼀个消费者组未产生死信消息,则不会为其创建相应的死信队列
死信消息处理
实际上,当⼀条消息进入死信队列,就意味着系统中某些地方出现了问题,从而导致消费者无法正常消费该消息,比如代码中原本就存在Bug。因此,对于死信消息,通常需要开发人员进行特殊处理。最关键的步骤是要排查可疑因素,解决代码中可能存在的Bug,然后再将原来的死信消息再次进行投递消费。