文章目录
消费者概述
消费者类型
在RocketMQ中根据使用者对读取操作的控制情况,消费者可分为两种类型。
- 一个是 DefaultMQPushConsumer ,由系统控制读取操作,收到消息后自动调用传入的处理方法来处理;
- 另一个是 DefaultMQPullConsumer ,读取操作中的大部分功能由使用者自主控制。
消费者组与订阅模式
- ConsumerGroup: Consumer 的 GroupName 用于把多个Consumer 组织到一起,提高并发处理能力,GroupName 需要和消息模式( MessageModel )配合使用。
- MessageModel:RocketMQ 支持两种消息模式: Clustering(默认) 和 Broadcasting
- ConsumerGroup 与 MessageModel 配合:
- 在Clustering 模式下,同一个 ConsumerGroup ( GroupName 相同) 里的每个Consumer 只消费所订阅消息的一部分内容, 同一个 ConsumerGroup 里所有的Consumer 消费的内容合起来才是所订阅Topic 内容的整体,从而达到负载均衡的目的
- 在Broadcasting 模式下,同一个ConsumerGroup 里的每个Consumer 都能消费到所订阅Topic 的全部消息,也就是一个消息会被多次分发,被多个Consumer 消费
MessageListener
- MessageListenerConcurrently
- 使用者将并发地使用消息。为了获得良好的性能,建议使用这种方法。
- 不建议抛出异常,可以返回
ConsumeConcurrentlyStatus.RECONSUME_LATER
状态
- MessageListenerOrderly
- 使用者将锁定每个MessageQueue,以确保按顺序逐个使用它。这将导致性能损失,但是当关心消息的顺序时,这是非常有用的。
- 不建议抛出异常,可以返回
ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
代替
消费状态 Consume Status
- MessageListenerConcurrently
ConsumeConcurrentlyStatus.CONSUME_SUCCESS
:成功消费ConsumeConcurrentlyStatus.RECONSUME_LATER
:告诉使用者现在不使用它,以后需要重新使用它。然后可以继续使用其他消息。
- MessageListenerOrderly 顺序消息
ConsumeOrderlyStatus.SUCCESS
消费成功ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT
:因为关心顺序,所以不能跳过消息,但是可以返回SUSPEND_CURRENT_QUEUE_A_MOMENT
来告诉使用者等待一会儿
线程池大小 Thread Number
消费端使用 ThreadPoolExecutor
在内部处理消费,因此可以通过设置 setConsumeThreadMin
或 setConsumeThreadMax
来更改它
ConsumeFromWhere
当一个新的Consumer Group建立时,它将需要决定是否需要使用Broker中已经存在的历史消息。
CONSUME_FROM_LAST_OFFSET
:将忽略历史消息,并使用此后生成的任何消息。CONSUME_FROM_FIRST_OFFSET
:将消费 Broker 中存在的所有消息。CONSUME_FROM_TIMESTAMP
:来消费指定时间戳之后生成的消息
消息重复 Duplication
- 消息重复的原因
- Producer 重新发送,如
FLUSH_SLAVE_TIMEOUT
情况 - Consumer 消费了一些消息,这些消息消费的 offsets 没有来得及更新到Broker,这时 Consumer 挂了
- 因此,如果应用程序不能容忍重复,那么可能需要进行一些外部工作来处理这个问题。例如,可以检查数据库的主键、使用幂等操作
PushConsumer
DefaultMQPushConsumer 的简单使用
- 使用DefaultMQPushConsumer 主要是设置好各种参数和传入处理消息的函数。系统收到消息后自动调用处理函数来处理消息,自动保存Offset ,而且加入新的DefaultMQPushConsumer 后会自动做负载均衡。
- DefaultMQPushConsumer 需要设置三个参数:
- 一是这个Consumer 的 GroupName
- 二是NameServer 的地址和端口号
- 三是Topic 的名称
-
设置 MessageModel。 RocketMQ 支持两种消息模式: Clustering 和 Broadcasting
-
NameServer 的地址和端口号,可以填写多个,用分号隔开,达到消除单点故障的目的
-
Topic 名称用来标识消息类型,需要提前创建。
- 如果不需要消费某个 Topic 下的所有消息,可以通过指定消息的 Tag 进行消息过滤,比如:
Consumer.subscribe ("Topic Test", "tagl||tag2||tag3")
,( Tag 是在发送消息时设置的标签) 。 - 在填写Tag 参数的位置,用
null
或者*
表示要消费这个Topic的所有消息
// Consumer 的 GroupName 用于把多个 Consumer 组织到一起,提高并发处理能力。
// GroupName 需要和消息模式(MessageModel)配合使用
DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("base_consumer_group");
// NameServer 的地址与端口号。多个用分号隔开,达到消除单点故障的目的
pushConsumer.setNamesrvAddr("10.0.64.106:9876;10.0.64.107:9876");
// 设置消费位
pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// CLUSTERING 模式,同一个 ConsumerGroup(GroupName相同)里的每个 Consumer 只消费所订阅消息的一部分。同一个 ConsumerGroup 里所有的 Consumer 消费的内容合起来才是所订阅Topic内容的整体,从而达到负载
// BROADCASTING 模式,同一个 ConsumerGroup 里面的每个 Consumer 都能消费到所订阅 Topic 的全部消息。也就是一个消息会被多次分发,被多个 Consumer 消费
pushConsumer.setMessageModel(MessageModel.CLUSTERING);
// 设置 Topic 的名称用来标识消息类型。
// 如果不需要消费某个topic 下的所有消息,可以通过指定消息的 Tag 进行消息过滤。null 或者 * 表示要消费全部消息
pushConsumer.subscribe("base_topic", "*");
pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.printf(Thread.currentThread().getName() + "收到新消息:" + msgs + "%n");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
pushConsumer.start();
DefaultMQPushConsumer 的处理流程
- DefaultMQPushConsumer 主要功能是 DefaultMQPushConsumerImpl 类实现的,消息的处理在于方法
pullMessage
中的PullCallback
中。在 PullCallback 的 onSuccess 函数中有个 switch 语句,根据从 Broker 返回的消息类型做相应的处理。
public void pullMessage(final PullRequest pullRequest) {
... ...
PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
switch (pullResult.getPullStatus()) {
case FOUND:
case NO_NEW_MSG:
case NO_MATCHED_MSG:
case OFFSET_ILLEGAL:
}
}
};
}
- DefaultMQPushConsumer 的源码中有很多
PullRequest
的语句,原因在于Push
是通过长轮询的方式达到的,长轮询方法既有 pull 的优点,又兼具Push方式的实时性。
- Push 方式是 Server 端接收到消息后,主动把消息推送给Client端,实时性高。但是有很多弊端:
- 加大了 Server 的工作量影响 Server 的性能;
- 其次 Client的 处理能力各不相同,Client的状态不受Server控制,如果Client不能及时处理 Server 推送过来的消息,会造成各种潜在的问题。
- Pull 方式是 Client 端循环地从 Server 端拉取消息,主动权在 Client 手中,自己拉取到一定数量后,处理妥当了在接着拉取。Pull 方式的问题时循环拉取的间隔不好设定,间隔太短就出在一个"盲等"的状态,浪费资源;每个pull的时间间隔太长,Server端有消息到来时,有可能没有被及时处理。
- 长轮询方式通过 Client 端和 Server 端的配合,达到既拥有 Pull 的优点,又能达到保证实时性的目的。
- Pull 源码
requestHeader.setSuspendTimeoutMillis (brokerSuspendMaxTimeMillis)
设置 Broker 最长阻塞时间,默认是15秒,是Broker在没有新消息的时候才阻塞,有消息会立即返回 - 长轮询服务端接收到消息请求后,如果队列里面没有新消息,并不急于返回,通过一个循环不断查看状态,每次 waitForRunning 一段时间(默认是5秒),然后再Check。
- 默认情况下当 broker 一直没有新消息,第三次check的时候,等待时间超过 request 里面的 SuspendMaxTimeMills ,就返回空结果。在等待的过程中,Broker 收到新信息,会直接返回。
- 长轮询的核心是 Broker 端 Hold 住客户单过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给客户端。
- 长轮询的主动权还是掌握在客户端手中,broker 即使有大量消息堆积,也不会主动推送给客户端
- 长轮询方式的局限性,是在 hold 住 Consumer 请求时候需要占用资源,它适合在消息队列客户端连接数可控的场景
- 关于长轮询的具体实现,见源码分析的博文
DefaultMQPushConsumer 流量控制
- 原理
- 线程池。PushConsumer 的核心还是Pull方式,有个线程池,消息处理逻辑在各个线程里同时执行。配置这个线程池可以控制消费的流量。
- ProcessQueue。
- RocketMQ 定义了一个快照类ProcessQueue。,在PushConsumer 运行的时候, 每个Message Queue 都会有个对应的ProcessQueue对象,保存了这个Message Queue 消息处理状态的快照
- ProcessQueue 对象里主要的内容是一个TreeMap 和一个读写锁。TreeMap里以Message Queue 的Offset 作为Key ,以消息内容的引用为Value ,保存了所有从MessageQueue 获取到,但是还未被处理的消息; 读写锁控制着多个线程对TreeMap 对象的并发访:
- PushConsumer 会判断获取但还未处理的消息个数、消息总大小、Offset 的跨度,任何一个值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的。此外Process Queue 还可以辅助实现顺序消费的逻辑
- Consumer 可以设置四个参数进行流量控制:
setConsumeThreadMin
设置Consumer 的线程数setConsumeThreadMax
设置Consumer 的线程数setPullBatchSize
指的是一次从Broker 的一个Message Queue 获取消息的最大数量,默认值是32setConsumeMessageBatchMaxSize
指的是这个Consumer的Executor (也就是调用MessageListener 处理的地方)一次传人的消息数( List msgs 这个链表的最大长度),默认值是 1
PullConsumer 简单使用
使用 DefaultMQPullConsumer 像使用DefaultMQPushConsumer 一样需要设置各种参数,写处理消息的函数,同时还需要做额外的事情。
- 获取 Message Queue 并遍历。
- 维护 Offsetstore。
- 根据不同的消息状态做不同的处理。
public class PullConsumer {
private static final Map<MessageQueue, Long> OFFSET_TABLE = new HashMap<>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
# 一个Topic 包括多个Message Queue ,如果这个Consumer 需要获取Topic下所有的消息,就要遍历多有的Message Queue 。
# 如果有特殊情况,也可以选择某些特定的Message Queue 来读取消息
for (MessageQueue mq : mqs) {
System.out.printf("Consume from the queue: %s%n", mq);
SINGLE_MQ:
while (true) {
try {
PullResult pullResult =
consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.printf("%s%n", pullResult);
# 维护 offset。从一个Message Queue 里拉取消息的时候,要传人Offset 参数( long 类型的值),随着不断读取消息, Offset 会不断增长。这个时候由用户负责把Offset存储下来,根据具体情况可以存到内存里、写到磁盘或者数据库里等
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
# 根据不同的消息状态做不同的处理
# 拉取消息的请求发出后,会返回: FOUND 、NO_MATCHED_MSG 、NO_NEW_MSG 、OFFSET_ILLEGAL 四种状态,需要根据每个状态做不同的处理。比较重要的两个状态是FOUNT 和NO_NEW_MSG ,分别表示获取到消息和没有新的消息
switch (pullResult.getPullStatus()) {
case FOUND:
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
consumer.shutdown();
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSET_TABLE.get(mq);
if (offset != null) {
return offset;
}
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
OFFSET_TABLE.put(mq, offset);
}
}
Consumer 的启动、关闭流程
-
对于PullConsumer 来说,使用者主动权很高,可以根据实际需要暂停、停止、启动消费过程。需要注意的是 Offset 的保存,要在程序的异常处理部分增加把 Offset 写人磁盘方面的处理,记准了每个Message Queue 的Offset ,才能保证消息消费的准确性。
-
DefaultMQPushConsumer 的退出, 要调用shutdown()函数, 以便释放资源、保存Offset 等。这个调用要加到Consumer 所在应用的退出逻辑中。
-
PushConsumer 在启动的时候,会做各种配置检查,然后连接NameServer获取Topic 信息,启动时如果遇到异常,比如无法连接NameServer,程序仍然可以正常启动不报错(日志里有WARN 信息) 。在单机环境下可以测试这种情况,启动DefaultMQPushConsumer 时故意把NameServer 地址填错,程序仍然可以正常启动,但是不会收到消息。
- 这和分布式系统的设计有关, RocketMQ 集群可以有多个NameServer 、Broker,某个机器出异常后整体服务依然可用。所以 DefaultMQPushConsumer 被设计成当发现某个连接异常时不立刻退出,而是不断尝试重新连接。可以进行这样一个测试,在 DefaultMQPushConsumer 正常运行的时候,手动 kill 掉 Broker 或NameServer ,过一会儿再启动。会发现 DefaultMQPushConsumer 不会出错退出,在服务恢复后正常运行,在服务不可用的这段时间,仅仅会在日志里报异常信息。
- 如果需要在 DefaultMQPushConsumer 启动的时候,及时暴露配置问题,该如何操作呢? 可以在
Consumer.start()
语句后调用:Consumer.fetchSubscribeMessageQueues ("TopicName")
,这时如果配置信息写得不准确,或者当前服务不可用,这个语句会报 MQC!ientException 异常
存储队列位置信息 offset
Offset 概述
- RocketMQ 中, 一种类型的消息会放到一个Topic 里,为了能够并行, 一般一个Topic 会有多个Message Queue (也可以设置成一个), Offset 是指某个Topic 下的一条消息在某个Message Queue 里的位置,通过 Offset的值可以定位到这条消息,或者指示Consumer 从这条消息开始向后继续处理。
- Offset 的类结构,主要分为本地文件类型和Broker 代存的类型两种。
- 对于 DefaultMQPushConsurner 来说,
- 默认是 CLUSTERING 模式,由Broker 端存储和控制Offset 的值,使用
RemoteBrokerOffsetStore
结构。 - BROADCASTING 模式下,每个Consumer都收到这个Topic 的全部消息,各个Consumer 间相互没有干扰, RocketMQ 使用
LocalfileOffsetStore
,把Offset 存到本地。
- 默认是 CLUSTERING 模式,由Broker 端存储和控制Offset 的值,使用
- OffsetStore 使用Json 格式存储
- 在使用 ·
DefaultMQPushConsumer
的时候,我们不用关心OffsetStore
的事,但是如果PullConsumer
,我们就要自己处理OffsetStore
。上面的例子PullConsumer.java
,使用内存存储。
Offset 的使用
- 设置Consumer 读取消息的初始位置,DefaultMQPushConsumer 类里函数
setConsumeFromWhere(ConsumeFromWhere.CONSUME FROM_FIRST_OFFSET)
实现这个功能。 - 具体设置的类型,见上面
ConsumeFromWhere
的类型说明。如设置从某个时间开始消费消息,Consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP), Consumer.setConsumeTimestamp("20131223171201")
,时间戳格式是精确到秒的。 - 注意设置读取位置不是每次都有效,它的优先级默认在Offset Store 后面。
- 比如在DefaultMQPushConsumer 的 BROADCASTING 方式下,默认是从Broker 里读取某个Topic 对应ConsumerGroup 的Offset , 当读取不到Offset 的时候, ConsumeFromWhere 的设置才生效。
- 大部分情况下这个设置在ConsumerGroup 初次启动时有效。如果Consumer 正常运行后被停止, 然后再启动, 会接着上次的Offset 开始消费, ConsumeFrom Where 的设置元效