RocketMQ——快速入门
RocketMQ 架构设计
消息队列实现了消息投放和消息消费间的解耦,实现了异步处理消息的功能。RocketMQ 作为消息中间件,在其存储消息的结构上实现了消息均衡投放、消息容灾、高可用(Dledger
主从切换)、自动故障转移特点。
先引入以下几个概念:
Broker
:实际存储消息的节点,接收来自生产者的消息,提供给消费者消费;一般以一个集群的形式提供服务,防止单点故障;Topic
:一类或一大类消息的集合,比如订单相关的消息包括(订单已下单、订单已支付等等....);MessageQueue
:消息队列对 Topic 进一步划分,将同一个 Topic 的消息均匀分布在多个消息队列中,其中每个消息队列存在于一个 Broker 上,这样保证了出现故障时不会丢失整个 Topic 消息,并且均衡分布防止了消息数据倾斜的问题。NameServer
:存储了整个系统的元数据,就想 Kafka 会采用Zookeeper
来存储管理集群元数据一样。NameServer 主要存储了 (1)集群中有哪些Topic;(2)这些Topic的 MessageQueue 存放在哪些 Broker上;(3)集群中有哪些活跃的 Broker;NameServer 为了应对单点故障,也是以集群的形式对外提供服务。
为了保证数据的安全完整性以及服务高可用,需要对数据做多备份,因此提供了 Master Broker
、Slave Broker
主从节点,主从之间会定期进行数据同步。但是如果 Master 出现宕机,如何重新选主成为一个问题,RocketMQ 4.5
之后采用 Dledger
实现高可用集群,类似于 Redis 集群的 Sentinel 能够进行自动的故障转移操作。Dledger
底层使用 Raft 协议算法重新选举新的 Master 对外提供服务。
RocketMQ 特点
1、顺序消费
1.1 分区有序
一个 Topic 对应多个 MessageQueue,那么在一个 MessageQueue 中的消息就是保持有序的,也叫做分区有序。
例如上图一个 Topic 包括了 3个 MessageQueue,一共四条消息投递到了不同的消息队列中,那么 MessageQueue-0 内部的 Message1、Message4 就可以认为是分区有序的。但是需要注意,消费者从消息队列中取数据消费是可以并行消费多个 MessageQueue 中消息的。例如这个例子,第一个消费到的消息不一定是 Message-1。
1.2 全局有序
那么如果现在有这样一组消息:创建订单、更新订单、完成订单。各个消息需要保证按照顺序消费,此时就需要全局有序的概念了。要做到全局有序,只能一个 Topic 分配一个 MessageQueue,这样就变成单分区下的有序消费,全局有序模式下不支持并行消费。
2、消息过滤
一个消息队列中存储了多个 Topic 相关的消息,现在有需求需要消费者只消费某一个 Topic 的消息,如何实现?
- 业务代码实现,消费者获取 MessageQueue 中所有消息,然后消费时判断 Topic;
- RocketMQ 给每个消息打上一个标签,消费者在消费时只需要订阅标签为特定 Topic 的消息,消费者就只会拉取到相关 Topic 的消息。
3、事务消息
这里的事务类似于我们熟知的 Mysql 事务,即一组操作要么全部成功,要么全部失败。
RocketMQ 所定义的事务是指把消息发送到那个服务的本地事务和将消息投递到 MQ 这个操作组成一个事务,它涉及到两个操作:(1)本地事务运行且成功;(2)消息投递到MQ成功。
例如上图我们将订单数据持久化到数据库 和 消息投递到MQ 两个操作作为一个事务。
4、延迟队列
RocketMQ 支持的延时队列只有 18个选项,分别是 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
。
实际上可以组合这些时间实现任意的等待时间,可以利用时间轮算法,参考 Dubbo 的时间轮算法实现。
5、死信队列
死信队列用于处理那些无法被正常消费的消息,这些消息超过了最大重试消费次数(16),会被存放到死信队列中,实际上也是存储在一个 Topic 中,所有死信队列都会带上 %DLQ%
的前缀,然后跟上消费者组的名称,并且这个 Topic 对消费者来说是不可见的。
死信队列中的消息最多只能存放 3 天,并且可以通过 Dashboard
实现对这些消息的重放。
RocketMQ——Message
Message 作为消息的载体,其主要包括以下属性:
Topic
:消息主题,消息类型;Flag
:Properties
:消息属性,内部包含消息的标签 Tag;body
:消息体内容;TransactionId
:事务ID;
消息——生产者
1、普通消息发送
1.1 同步消息发送
DefaultMQProducer producer = new DefaultMQProducer("producer_name");
producer.setNamesrvAddr("localhost:9876");
producer.start();
for (int i = 0; i < 100; i++) {
Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ"+i).getBytes(StandardCharsets.UTF_8));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
消息通信步骤如下:
- 创建生产者对象,默认使用
DefaultProducer
,指定生产者组的名称(一类生产者的集合),这类生产者发送同一类消息且发送逻辑一致; - 配置 NameServer 地址,多个 NameServer 中间通过分号隔开,比如
127.0.0.2:9876
、127.0.0.1:9876
; - 构建消息,指定消息主题、消息标签、消息体,消息主题用来区分不同类型的消息(比如订单相关、商品相关),标签可以理解为对消息更细致的划分,RocketMQ 可以在消费端对 Tag 进行过滤;
- 调用同步请求
send()
接口发送消息。同步发送后阻塞等待响应SendResult
,它包含实际发送状态以及SEND_OK(成功发送)
、FLUSH_DISK_TIMEOUT(刷盘超时)
、FLUSH_SLAVE_TIMEOUT(备份超时)
、SLAVE_NOT_AVAILABLE(备份不可用)
;
2、顺序消息发送
针对同一主题的消息,严格按照 FIFO 的原则进行消息发布和消费,同时消息按照分片键 ShardingKey
划分到不同队列中,同一个队列中的消息按照到达顺序消费。
生产顺序性,RocketMQ
通过生产者和服务端协议保证了,如果是单一生产者且生产者生产消息的逻辑是串行发送,那么消息将顺序发送到服务端,并且同一分区键的消息会按照发送顺序存储在同一队列中。
存在一些场景需要多个消息顺序消费,比如订单下单、付款和发货三个操作需要顺序执行。如果是普通消息,订单A 的消息可能被轮询发送到不同队列中,不同队列消息无序;而顺序消息发送根据分片键将相同类别的消息发送到同一个队列中。
public class Producer {
public static void main(String[] args) throws UnsupportedEncodingException {
try {
// 默认生产者
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
// 消息标签
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 队列选择器 MessageQueueSelector 根据 orderId 确定消息的目的队列
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
// orderId % QueueLen
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
e.printStackTrace();
}
}
}
如果出现 Broker 掉线,队列长度会发生变化,如果不及时更新本地存储的队列信息,可能会发送消息到掉线队列上,因此 MQ 提供了可用性和强顺序性两种策略,通过指定 --order
参数来设置。
3、延迟消息发送
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// Instantiate a producer to send scheduled messages
DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
// Launch producer
producer.start();
int totalMessagesToSend = 100;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// This message will be delivered to consumer 10 seconds later.
message.setDelayTimeLevel(3);
// Send the message
producer.send(message);
}
// Shutdown producer after use.
producer.shutdown();
}
}
批量消息发送
为了减少 API 和网络调用次数,RocketMQ
将一些消息汇聚成一批后发送,可以增加吞吐率:
public class SimpleBatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
producer.start();
//If you just send messages of no more than 1MiB at a time, it is easy to use batch
//Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
producer.send(messages);
}
}
事务消息发送
在一些特殊场景下,需要对数据上下游的一致性有较高要求,以电商交易场景为例,用户支付订单这一操作会影响到下游物流分发、积分变更、购物车清空等多个子系统的变更,业务处理包括如下:
- 主分支订单系统状态更新,由支付状态变为支付成功;
- 物流系统状态新增物流发货信息,创建物流表单记录;
- 积分系统积分变更;
- 购物车系统状态变更,更新购物车记录。
使用普通消息和订单事务无法保证消息一致,因为普通消息无法像数据库事务一样具有回滚、提交等功能。而 RocketMQ 分布式事务消息功能在普通消息的基础上支持二阶段的提交功能。将二阶段和本地事务绑定,实现全局提交结果的一致性。
二阶段简单来说分为以下几个阶段:
- 生产者发送半事务消息到 MQ,半事务消息虽然已经发送到 MQ,但是暂时不能投递;
- 执行本地事务,根据执行结果的成功与否,向 Broker 发送半事务消息状态(Commit/Rollback),半事务消息只有在
Commit
状态下才会真正向下游投递; - 这个过程中可能出现网络中断等问题,导致某事务的二次确认丢失,Broker 在扫描某条消息时发现长期处于半事务消息,需要主向消息生产者询问消息的最终状态。这样保证了事务执行成功,下游一定能收到消息并投递。
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
// 事务回查使用的线程池
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg =
new Message("TopicTest", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
// 事务回查具体逻辑
static class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
/**
* 半事务消息发送成功后,执行本地事务的方法
* 具体执行完本地事务后,可以返回:
* 1.LocalTransactionState.COMMIT_MESSAGE:提交事务;
* 2.LocalTransactionState.ROLLBACK_MESSAGE:回滚事务;
* 3.LocalTransactionState.UNKNOW:暂时无法判断状态;
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
return LocalTransactionState.UNKNOW;
}
/**
* 二次确认消息未收到,Broker 端回查事务状态的方法
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Integer status = localTrans.get(msg.getTransactionId());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
}
消息——消费者
RocketMQ
有两种消费模式:
- 集群消费模式:消息只需要被消费者组的任意一个成员消费一次就行;
- 广播消费模式:消息需要被消费者组的任意一个成员均消费一次。
多个消费者如何保证消息消费的负载均衡呢,常见的有以下几种分配策略:
- 平均分配;
- 机房优先分配策略;
- 一致性 hash 分配策略等。
RocketMQ
支持通过 consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragely());
来指定消息分配策略。
试想如果消费者宕机后重启,如何知道上一次的消费状态,这就需要消费位点来记录,每个队列都会记录自己的最小位点、最大位点、对于消费组还有消费位点的概念。集群模式下,消费位点由客户端交给服务端保存;广播模式下,消费位点由客户端自己保存,一般情况下消费位点更新不会出现消息重复的问题。
但是如果消费者发生变动触发了重平衡,重平衡完成后每个消费者可能分配到新的队列,但客户端交给服务端的消费位点不是实时的,重平衡就可能造成消息少量重复问题。
如何解决?
1、Push消费模式
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
// 初始化consumer,并设置consumer group name
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// 设置NameServer地址
consumer.setNamesrvAddr("localhost:9876");
//订阅一个或多个topic,并指定tag过滤条件,这里指定*表示接收所有tag的消息
consumer.subscribe("TopicTest", "*");
//注册回调接口来处理从Broker中收到的消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
// 返回消息消费状态,ConsumeConcurrentlyStatus.CONSUME_SUCCESS为消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动Consumer
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
消费者 Pull 方式消费消息分为以下几步:
- 初始化
DefaultMQPushConsumer
,给消费者设置消费者组名、NameServer
地址等等信息; - 订阅一个或多个
Topic
,并根据Tag
过滤条件过滤部分消息; - 初始化消费者注册获取消息后执行的回调函数
registerMessageListener()
,可以是并发消费也可以是顺序消费; - 启动消费者;
具体的消息回调逻辑如下:
// 消息消费顶层接口
public interface MessageListener {
}
// 消息并发消费接口
// msgs 是从 Broker 端获取的被消费消息列表,用户实现该接口,并把自己对消息的消费逻辑写在 consumeMessage 方法中
public interface MessageListenerConcurrently extends MessageListener {
ConsumeConcurrentlyStatus consumeMessage(final List<MessageExt> msgs,
final ConsumeConcurrentlyContext context);
}
- msgs 是从 Broker 端获取的需要被消费的消息列表,该接口返回最终的消费状态,
ConsumeConcurrentlyStatus.CONSUME_SUCCESS
表示消费成功,RECONSUME_LATER
表示消费失败,一段时间后再重新消费。 - 消费者API却非常简单,用户并不需要关注重平衡或者拉取的逻辑,只需要写好自己的消费逻辑即可。
消费组可以设置消费方式具体是集群模式还是广播模式,集群模式默认一个消费组内的消费者分担消费;广播模式下消费组内每一个消费者都会消费全量消息。
// 集群模式消费
consumer.setMessageModel(MessageModel.CLUSTERING);
// 广播模式消费
consumer.setMessageModel(MessageModel.BROADCASTING);
1.1 并发消费和顺序消费配置
虽然发送端通过发送顺序消息保证消息在同一个队列中按照 FIFO 的顺序消费,但是也无法保证消息实际被顺序消费,因为消息可能同时被多个线程消费。
要保证消息顺序消费,需要实现的是 MessageListenerOrderly()
接口,顺序消费返回 ConsumeOrderlyStatus.SUCCESS
(消费成功)、ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
(消费失败)。
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
1.2 消息过滤
前面讲过消息根据 Topic 进行种类分类,根据 Tag 再进行分类,RocketMQ
支持的消息过滤方式有两种:
Tag过滤
:消费者订阅的 Tag 和发送者设置的 Tag相互匹配则将消息投递给消费端消费。Tag 过滤适用于简单的过滤场景,仅需要对 Topic 中的消息进行一级分类并过滤。SQL92过滤
:发送者设置 Tag 或者消息属性,消费者订阅满足 SQL92 过滤表达式的消息被投递给消费端进行消费。SQL92 消费适用于一条消息支持多个属性,并且根据 SQL 语法自定义组合多种类型表达式对消息进行多级分类并实现多维度的过滤。
1.2.1 Tag 消息过滤
以电商交易场景为例,客户下单到收到商品会生产一系列消息包括 订单消息、物流消息、支付消息。
这些消息统一被发往 Topic=Trade_Topic
的 Topic 队列中被各个不同的系统所订阅。现各个系统需要订阅不同的消息部分,通过 Tag 过滤如下:
// 物流系统、支付系统,只用订阅单个 Tag
consumer.subscribe("TagFilterTest", "TagA");
// 实时计算系统需要订阅主题下所有消息
consumer.subscribe("TagFilterTest", "*");
// 交易成功率分析系统,订阅订单和支付两个 Tag 消息
consumer.subscribe("TagFilterTest", "TagA||TagB");
// 同一个消费者多次订阅某个 Topic 下的 Tag,以最后一次订阅为准
consumer.subscribe("TagFilterTest", "TagA");
consumer.subscribe("TagFilterTest", "TagB");
1.2.2 SQL92 消息过滤
SQL92 过滤是在消息发送时设置消息的 Tag 或者自定义属性,消费者订阅时使用 SQL 语法来设置过滤表达式,根据自定义属性或者 Tag 过滤消息。Tag 在 SQL 语法中表示为 TAGS
,如果要开启属性过滤,首要的得在 Broker 端设置 enablePropertyFilter=true
,该值默认为 false。
各系统对消息的过滤需求如上图:
- 物流系统1:只需订阅物流消息且消息地域为杭州;
- 物流系统2:只需订阅物流消息且消息地域为杭州或上海;
- 订单跟踪系统:只需订阅订单消息;
根据需求,我们还需要自定义属性消息地域并设置在消息中:
- 消息发送端设置消息自定义属性:
Message msg = new Message("topic", "tag", "Hello MQ".getBytes());
// 设置自定义属性A,属性值为1。
msg.putUserProperties("region", "");
- 消息消费端:通过 SQL 语法设置过滤表达式,根据自定义属性过滤消息:
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"and (region in ('HZ', 'SH'))"));
1.3 消息重试和死信队列
1.3.1 消息重试
若Consumer消费某条消息失败,则RocketMQ会在重试间隔时间后,将消息重新投递给Consumer消费,若达到最大重试次数后消息还没有成功被消费,则消息将被投递至死信队列。
- 设置消息最大重试消费次数:
consumer.setMaxReconsumeTimes(10);
- 设置消息失败后重新投递的重试间隔:
consumer.setSuspendCurrentQueueTimeMillis(5000);
顺序消费和并发消费的重试机制并不相同,顺序消费消费失败后会先在 客户端本地 重试直到最大重试次数,这样可以避免消费失败的消息被跳过,消费下一条消息而打乱顺序消费的顺序,而并发消费消费失败后会将 消费失败的消息重新投递回服务端 ,再等待服务端重新投递回来,在这期间会正常消费队列后面的消息。
并发消费失败后并不是投递回原 Topic,而是投递到一个特殊的 Topic,名称为 %RETRY%ConsumerGroupName
。集群模式下每一个消费者组会对应一个特殊 Topic,并且会订阅该 Topic。
1.3.2 死信队列
当一条消息初次消费失败,RocketMQ会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息。此时,该消息不会立刻被丢弃,而是将其发送到该消费者对应的特殊队列中,这类消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue),死信队列是死信Topic下分区数唯一的单独队列。如果产生了死信消息,那对应的ConsumerGroup的死信Topic名称为%DLQ%ConsumerGroupName,死信队列的消息将不会再被消费。可以利用RocketMQ Admin工具或者RocketMQ Dashboard上查询到对应死信消息的信息。
2、Pull消费模式
2.1 普通拉取模式
Pull 是消费消息的另一种方式,即主动拉取,分为原始拉取 Pull Consumer
、Lite Pull Consumer
。
public class PullConsumerTest {
public static void main(String[] args) throws MQClientException {
// 初始化 DefaultMQPullConsumer
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 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();
}
}
PullResult
有以下几种状态:
- FOUND:成功拉取到消息;
- NO_NEW_MSG:没有发现新消息;
- NO_MATCHED_MSG:没有匹配到消息;
- OFFSET_ILLEGAL:传入的拉取位点是非法的。
2.2 Lite Pull 拉取模式
Lite Pull Consumer
是新推出的拉取消费模式,比普通拉取更加简单易用,提供了 Subscribe
、Assign
两种模式。
Subscribe
模式如下:
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();
}
}
}
Subscribe
模式下消费消息步骤:
- 初始化
DefaultLitePullConsumer
并设置消费者组名; - 订阅对应主题的消息,设置单次拉取消息的数量;
- 轮询 poll 接口拉取消息。
Subscribe 模式下 MQ 会自动提交消费位点并且同一个消费组下多个 LitePullConsumer
会负载均衡地进行消费。
Assign
模式如下:
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();
// 获取 Topic=TopicTest 的所有队列信息
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);
// 设置第一个队列拉取消息的位点为10
litePullConsumer.seek(assignList.get(0), 10);
try {
while (running) {
// 循环调用 poll 拉取
List<MessageExt> messageExts = litePullConsumer.poll();
System.out.printf("%s %n", messageExts);
litePullConsumer.commitSync();
}
} finally {
litePullConsumer.shutdown();
}
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· 一文掌握DeepSeek本地部署+Page Assist浏览器插件+C#接口调用+局域网访问!全攻略