RocketMQ之应用
一、简介
RocketMQ
是阿里巴巴开源的分布式消息中间件。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如Group
、Topic
、Queue
等。系统组成则由Producer
、Consumer
、Broker
、NameServer
等。
RocketMQ特点:
- 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式等特点;
Producer
、Consumer
、队列都可以分布式;Producer
向一些队列轮流发送消息,队列集合称为Topic
,Consumer
如果做广播消费,则一个Consumer
实例消费这个Topic
对应的所有队列,如果做集群消费,则多个Consumer
实例平均消费这个Topic
对应的队列集合;- 能够保证严格的消息顺序;
- 支持拉(
pull
)和推(push
)两种消息模式; - 高效的订阅者水平扩展能力;
- 实时的消息订阅机制;
- 亿级消息堆积能力;
- 支持多种消息协议,如
JMS
、OpenMessaging
等; - 较少的依赖。
二、核心概念
2.1 Name Server、Broker
消息队列RocketMQ
在任何一个环境都是可扩展的,生产者必须是一个集群,消息服务器必须是一个集群,消费者也同样。集群级别的高可用,是消息队列RocketMQ
跟其他的消息服务器的主要区别,消息生产者发送一条消息到消息服务器,消息服务器会随机的选择一个消费者,只要这个消费者消费成功就认为是成功了。
注意:文中所提及的消息队列RocketMQ
的服务端或者服务器包含Name Server
、Broker
等。服务端不等同于Broker
。
RocketMQ
主要由Producer
、Broker
、Consumer
三部分组成,其中Producer
负责生产消息,Consumer
负责消费消息,Broker
负责存储消息。Broker
在实际部署过程中对应一台服务器,每个Broker
可以存储多个Topic
的消息,每个Topic
的消息也可以分片存储于不同的Broker
。Message Queue
用于存储消息的物理地址,每个Topic
中的消息地址存储于多个Message Queue
中。ConsumerGroup
由多个Consumer
实例构成。
图中所涉及到的概念如下所述:
- Name Server:名称服务充当路由消息的提供者。是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。在消息队列
RocketMQ
中提供命名服务,更新和发现Broker
服务。
NameServer
即名称服务,两个功能:
- 接收
broker
的请求,注册broker
的路由信息 - 接收
client
(producer/consumer
)的请求,根据某个topic
获取其到broker
的路由信息
NameServer
没有状态,可以横向扩展。每个broker
在启动的时候会到NameServer
注册;
Producer
在发送消息前会根据topic
到NameServer
获取路由(到broker)信息;
Consumer
也会定时获取topic
路由信息。
- Broker:消息中转角色,负责存储消息,转发消息。可以理解为消息队列服务器,提供了消息的接收、存储、拉取和转发服务。
broker
是RocketMQ
的核心,所以需要保证broker
的高可用。
broker
分为Master Broker
和Slave Broker
,一个Master Broker
可以对应多个Slave Broker
,但是一个Slave Broker
只能对应一个Master Broker
。
Master
与Slave
的对应关系通过指定相同的BrokerName
,不同的BrokerId
来定义,BrokerId
为0
表示Master
,非0
表示Slave
。Master
也可以部署多个。
每个Broker
与Name Server
集群中的所有节点建立长连接,定时注册Topic
信息到所有Name Server
。Broker
启动后需要完成一次将自己注册至Name Server
的操作;随后每隔30s
定期向Name Server
上报Topic
路由信息。 - Producer:负责生产消息,一般由业务系统负责生产消息。与
Name Server
集群中的其中一个节点(随机)建立长链接(Keep-alive
),定期从Name Server
读取Topic
路由信息,并向提供Topic
服务的Master Broker
建立长链接,且定时向Master Broker
发送心跳。 - Consumer:负责消费消息,一般是后台系统负责异步消费。与
Name Server
集群中的其中一个节点(随机)建立长连接,定期从Name Server
拉取Topic
路由信息,并向提供Topic
服务的Master Broker
、Slave Broker
建立长连接,且定时向Master Broker
、Slave Broker
发送心跳。Consumer
既可以从Master Broker
订阅消息,也可以从Slave Broker
订阅消息,订阅规则由Broker
配置决定。
另外,Broker
中还存在一些非常重要的名词需要说明:
2.2 Topic、Queue、tags
RocketMQ
的Topic/Queue
和JMS
中的Topic/Queue
概念有一定的差异,JMS
中所有消费者都会消费一个Topic
消息的副本,而Queue
中消息只会被一个消费者消费;但到了RocketMQ
中Topic
只代表普通的消息队列,而Queue
是组成Topic
的更小单元。
Topic:表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。
Queue:主题被划分为一个或多个子主题,称为“message queues”。一个topic
下,我们可以设置多个queue(消息队列)。当我们发送消息时,需要要指定该消息的topic。RocketMQ
会轮询该topic
下的所有队列,将消息发送出去。
定义:Queue
是Topic
在一个Broker
上的分片,在分片基础上再等分为若干份(可指定份数)后的其中一份,是负载均衡过程中资源分配的基本单元。集群消费模式下一个消费者只消费该Topic
中部分Queue
中的消息,当一个消费者开启广播模式时则会消费该Topic
下所有Queue
中的消息。
先看一张有关Topic
和Queue
的关系图:
Tags:Tags
是Topic
下的次级消息类型/二级类型(注:Tags也支持TagA || TagB这样的表达式),可以在同一个Topic
下基于Tags
进行消息过滤。Tags
的过滤需要经过两次比对,首先会在Broker
端通过Tag hashcode
进行一次比对过滤,匹配成功传到consumer
端后再对具体Tags
进行比对,以防止Tag hashcode
重复的情况。比如交易消息又可以分为:交易创建消息,交易完成消息等,一条消息可以没有Tag
。RocketMQ
提供2
级消息分类,方便大家灵活控制。标签,换句话说,为用户提供了额外的灵活性。有了标签,来自同一个业务模块的不同目的的消息可能具有相同的主题和不同的标签。标签将有助于保持您的代码干净和连贯,并且标签还可以为RocketMQ
提供的查询系统提供帮助。
Queue
中具体的存储单元结构如下图,最后面的8
个Byte
存储Tag
信息。
2.3 Producer与Producer Group
Producer:表示消息队列的生产者。消息队列的本质就是实现了publish-subscribe
模式,生产者生产消息,消费者消费消息。所以这里的Producer
就是用来生产和发送消息的,一般指业务系统。RocketMQ
提供了发送:普通消息(同步、异步和单向)、定时消息、延时消息、事务消息。
Producer Group:是一类Producer
的集合名称,这类Producer
通常发送一类消息,且发送逻辑一致。相同角色的生产者被分组在一起。同一生产者组的另一个生产者实例可能被broker
联系,以提交或回滚事务,以防原始生产者在交易后崩溃。
警告:考虑提供的生产者在发送消息时足够强大,每个生产者组只允许一个实例,以避免对生产者实例进行不必要的初始化。
2.4 Consumer与Consumer Group
Consumer:消息消费者,一般由业务后台系统异步的消费消息。
- Push Consumer:
Consumer
的一种,应用通常向Consumer
对象注册一个Listener
接口,一旦收到消息,Consumer
对象立刻回调Listener
接口方法。 - Pull Consumer:
Consumer
的一种,应用通常主动调用Consumer
的拉消息方法从Broker
拉消息,主动权由应用控制。
Consumer Group:Consumer Group
是一类Consumer
的集合名称,这类Consumer
通常消费一类消息,且消费逻辑一致(使用相同Group ID
的订阅者属于同一个集群。同一个集群下的订阅者消费逻辑必须完全一致(包括Tag
的使用),这些订阅者在逻辑上可以认为是一个消费节点)。消费者群体是一个伟大的概念,它实现了负载平衡和容错的目标,在信息消费方面,是非常容易的。
警告:消费者群体的消费者实例必须订阅完全相同的主题。
三、组件的关系
3.1 Broker、Producer和Consumer
如果不考虑负载均衡和高可用,最简单的Broker
,Producer
和Consumer
之间的关系如下图所示:
3.2 Topic,Topic分片和Queue
Queue
是RocketMQ
中的另一个重要概念。在对该概念进行分析介绍前,我们先来看上面的这张图:
从本质上来说,RocketMQ
中的Queue是
数据分片的产物。为了更好地理解Queue
的定义,我们还需要引入一个新的概念:Topic
分片。在分布式数据库和分布式缓存领域,分片概念已经有了清晰的定义。同理,对于RocketMQ
,一个Topic
可以分布在各个Broker
上,我们可以把一个Topic
分布在一个Broker
上的子集定义为一个Topic
分片。对应上图,TopicA
有3
个Topic
分片,分布在Broker1
,Broker2
和Broker3
上,TopicB
有2
个Topic
分片,分布在Broker1
和Broker2
上,TopicC
有2
个Topic
分片,分布在Broker2
和Broker3
上。
将Topic
分片再切分为若干等分,其中的一份就是一个Queue
。每个Topic
分片等分的Queue
的数量可以不同,由用户在创建Topic
时指定。
queue
数量指定方式:
- 代码指定:
producer.setDefaultTopicQueueNums(8);
- 配置文件指定,同时设置
broker
服务器的配置文件broker.properties:defaultTopicQueueNums=16
rocket-console
控制台指定
我们知道,数据分片的主要目的是突破单点的资源(网络带宽,CPU
,内存或文件存储)限制从而实现水平扩展。RocketMQ
在进行Topic
分片以后,已经达到水平扩展的目的了,为什么还需要进一步切分为Queue
呢?
解答这个问题还需要从负载均衡说起。以消息消费为例,借用Rocket MQ
官方文档中的Consumer
负载均衡示意图来说明:
如图所示,TOPIC_A
在一个Broker
上的Topic
分片有5
个Queue
,一个Consumer Group
内有2
个Consumer
按照集群消费的方式消费消息,按照平均分配策略进行负载均衡得到的结果是:第一个Consumer
消费3
个Queue
,第二个Consumer
消费2
个Queue
。如果增加Consumer
,每个Consumer
分配到的Queue
会相应减少。Rocket MQ
的负载均衡策略规定:Consumer
数量应该小于等于Queue
数量,如果Consumer
超过Queue
数量,那么多余的Consumer
将不能消费消息。
在一个Consumer Group
内,Queue
和Consumer
之间的对应关系是一对多的关系:一个Queue
最多只能分配给一个Consumer
,一个Cosumer
可以分配得到多个Queue
。这样的分配规则,每个Queue
只有一个消费者,可以避免消费过程中的多线程处理和资源锁定,有效提高各Consumer
消费的并行度和处理效率。
由此,我们可以给出Queue
的定义:Queue
是Topic
在一个Broker
上的分片等分为指定份数后的其中一份,是负载均衡过程中资源分配的基本单元。
四、RocketMQ生产模式
- 消息发送方式:RocketMQ提供三种方式可以发送普通消息:同步、异步、和单向发送。
- 消息类型:消息客户端提供多种SDK:普通、顺序、事务、延时消息。
4.1 发送方式
RocketMQ
发送普通消息的三种方式:同步消息(默认)、异步消息和单向消息。其中前两种消息是可靠的,因为会有发送是否成功的应答。
4.1.1 同步发送(Sync)
原理:同步发送是指消息发送方发出一条消息后,会在收到服务端返回响应之后才发下一条消息的通讯方式。
应用场景:可靠的同步传输应用于广泛的场景,例如重要通知邮件、报名短信通知、营销短信系统等。
实例代码:
SendResult sendResult = producer.send(msg);
4.1.2 异步发送(Async)
原理:异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。消息队列RocketMQ
版的异步发送,需要您实现异步发送回调接口(SendCallback
)。消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息。发送方通过回调接口接收服务端响应,并处理响应结果。
应用场景:异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,例如,您视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
实例代码:
// 异步发送消息, 发送结果通过callback返回给客户端。
producer.sendAsync(msg, new SendCallback() {
@Override
public void onSuccess(final SendResult sendResult) {
// 消息发送成功。
System.out.println("send message success. topic=" + sendResult.getTopic()
+ ", msgId=" + sendResult.getMessageId());
}
@Override
public void onException(OnExceptionContext context) {
// 消息发送失败,需要进行重试处理,可重新发送这条消息或持久化这条数据进行补偿处理。
System.out.println("send message failed. topic=" + context.getTopic()
+ ", msgId=" + context.getMessageId());
}
});
4.1.3 单向发送(Oneway)
由于在oneway
方式发送消息时没有请求应答处理,一旦出现消息发送失败,则会因为没有重试而导致数据丢失。若数据不可丢,建议选用可靠同步或可靠异步发送方式。
原理:发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。
应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
实例代码:
//发送单向消息,没有任何返回结果
producer.sendOneway(msg);
4.2 消息类型
消息客户端提供多种SDK:普通、顺序、事务、延时消息
4.2.1 收发顺序消息
顺序消息(FIFO
消息)是消息队列RocketMQ
版提供的一种严格按照顺序来发布和消费的消息类型。
顺序消息分为两类:
- 全局顺序:对于指定的一个
Topic
,所有消息按照严格的先入先出FIFO
(First In First Out)的顺序进行发布和消费。 - 分区顺序:对于指定的一个
Topic
,所有消息根据Sharding Key
进行区块分区。同一个分区内的消息按照严格的FIFO
顺序进行发布和消费。Sharding Key
是顺序消息中用来区分不同分区的关键字段,和普通消息的Key
是完全不同的概念。
实例代码:
发送消息
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
订阅消息
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeOrderlyStatus.SUCCESS;
}
});
4.2.2 发送延迟消息
延时消息用于指定消息发送到消息队列RocketMQ
版的服务端后,延时一段时间才被投递到客户端进行消费(例如3
秒后才被消费),适用于解决一些消息生产和消费有时间窗口要求的场景,或者通过消息触发延迟任务的场景,类似于延迟队列。
实例代码:
// 延时消息,单位毫秒(ms),在指定延迟时间(当前时间之后)进行投递,例如消息在3秒后投递。
long delayTime = System.currentTimeMillis() + 3000;
// 设置消息需要被投递的时间。
msg.setStartDeliverTime(delayTime);
4.2.3 发送定时消息
定时消息可以做到在指定时间戳之后才可被消费者消费,适用于对消息生产和消费有时间窗口要求,或者利用消息触发定时任务的场景。
实例代码:
// 定时消息,单位毫秒(ms),在指定时间戳(当前时间之后)进行投递,例如2021-06-26 16:20:00投递。
//如果被设置成当前时间戳之前的某个时刻,消息将立即被投递给消费者。
long timeStamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-06-26 16:20:00").getTime();
msg.setStartDeliverTime(timeStamp);
4.2.4 发送延时消息
使用场景:
比如电商里,提交了一个订单就可以发送一个延时消息,1h
后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
延时消息的使用限制:
现在RocketMQ
并不支持任意时间的延时,需要设置几个固定的延时等级,从1s
到2h
分别对应着等级1
到18
消息消费失败会进入延时消息队列,消息发送时间与设置的延时等级和重试次数有关
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
Message message = new Message("delay", msg.getBytes());
//在创建好message对象之后
message.setDelayTimeLevel(3); //这里是对应延时等级3对应是10s
4.2.5 发送事务消息
消息队列RocketMQ
版提供类似XA
或Open XA
的分布式事务功能,通过消息队列RocketMQ
版事务消息,能达到分布式事务的最终一致。
事务消息交互流程如下图所示。
实例代码
发送事务消息包含以下两个步骤:
- 发送半事务消息(
Half Message
)及执行本地事务,
public static void main(String[] args) throws MQClientException {
/**
* 创建事务消息Producer
*/
TransactionMQProducer transactionMQProducer = new TransactionMQProducer(MqConfig.GROUP_ID, getAclRPCHook());
transactionMQProducer.setNamesrvAddr(MqConfig.NAMESRV_ADDR);
transactionMQProducer.setTransactionCheckListener(new LocalTransactionCheckerImpl());
transactionMQProducer.start();
for (int i = 0; i < 10; i++) {
try {
Message message = new Message(MqConfig.TOPIC,
MqConfig.TAG,
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = transactionMQProducer.sendMessageInTransaction(message,
new LocalTransactionExecuter() {
@Override
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
System.out.println("开始执行本地事务: " + msg);
return LocalTransactionState.UNKNOW;
}
}, null);
assert sendResult != null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 提交事务消息状态。
当本地事务执行完成(执行成功或执行失败),需要通知服务器当前消息的事务状态。通知方式有以下两种:
- 执行本地事务完成后提交。
- 执行本地事务一直没提交状态,等待服务器回查消息的事务状态。
事务状态有以下三种:
TransactionStatus.CommitTransaction
:提交事务,允许订阅方消费该消息。TransactionStatus.RollbackTransaction
:回滚事务,消息将被丢弃不允许消费。TransactionStatus.Unknow
:无法判断状态,期待消息队列RocketMQ
版的Broker
向发送方再次询问该消息对应的本地事务的状态。
/**
* MQ发送事务消息本地Check接口实现类
*/
public class LocalTransactionCheckerImpl implements TransactionCheckListener {
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
System.out.println("收到事务消息的回查请求, MsgId: " + msg.getMsgId());
return LocalTransactionState.COMMIT_MESSAGE;
}
}
事务回查机制说明
-
发送事务消息为什么必须要实现回查
Check
机制?当步骤1中半事务消息发送完成,但本地事务返回状态为TransactionStatus.Unknow
,或者应用退出导致本地事务未提交任何状态时,从Broker
的角度看,这条Half
状态的消息的状态是未知的。因此Broker
会定期要求发送方Check
该Half
状态消息,并上报其最终状态。 -
Check
被回调时,业务逻辑都需要做些什么?事务消息的Check
方法里面,应该写一些检查事务一致性的逻辑。消息队列RocketMQ版
发送事务消息时需要实现LocalTransactionChecker
接口,用来处理Broker主动发起
的本地事务状态回查请求,因此在事务消息的Check
方法中,需要完成两件事情:- 检查该半事务消息对应的本地事务的状态(committed or rollback)。
- 向
Broker
提交该半事务消息本地事务的状态。
4.2.6 发送批量消息
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic
,相同的waitStoreMsgOK
,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB
。
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
producer.send(messages);
} catch (Exception e) {
e.printStackTrace();
//处理error
}
批量发送消息
//large batch
String topic = "BatchTest";
List<Message> messages = new ArrayList<>(100 * 1000);
for (int i = 0; i < 100 * 1000; i++) {
messages.add(new Message(topic, "Tag", "OrderID" + i, ("Hello world " + i).getBytes()));
}
//split the large batch into small ones:
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
List<Message> listItem = splitter.next();
producer.send(listItem);
}
class ListSplitter implements Iterator<List<Message>> {
private int sizeLimit = 1000 * 1000;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
tmpSize = tmpSize + 20; //for log overhead
if (tmpSize > sizeLimit) {
//it is unexpected that single message exceeds the sizeLimit
//here just let it go, otherwise it will block the splitting process
if (nextIndex - currIndex == 0) {
//if the next sublist has no element, add this one and then break, otherwise just break
nextIndex++;
}
break;
}
if (tmpSize + totalSize > sizeLimit) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Not allowed to remove");
}
}
4.3 分布式消息OpenMessaging
除了以上发送方式外,RocketMQ
还支持面向云的分布式消息,请参考OpenMessaging 示例
五、RocketMQ消费模式
5.1 订阅方式
消息队列RocketMQ
版支持以下两种订阅方式:
1.集群订阅:同一个Group ID
所标识的所有Consumer
平均分摊消费消息。例如某个Topic
有9
条消息,一个Group ID
有3
个Consumer
实例,那么在集群消费模式下每个实例平均分摊,只消费其中的3
条消息。设置方式如下所示。
// 集群订阅方式设置(不设置的情况下,默认为集群订阅方式)。
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
2.广播订阅:同一个Group ID
所标识的所有Consumer
都会各自消费某条消息一次。例如某个Topic
有9
条消息,一个Group ID
有3
个Consumer
实例,那么在广播消费模式下每个实例都会各自消费9
条消息。设置方式如下所示。
// 广播订阅方式设置。
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);
说明:请确保同一个
Group ID
下所有Consumer
实例的订阅关系保持一致。两种不同的订阅方式有着不同的功能限制,例如,广播模式不支持顺序消息、不维护消费进度、不支持重置消费位点等。
5.2 消费方式
消息队列RocketMQ
版支持以下两种消息获取方式:
- Push(默认):消息由消息队列
RocketMQ
版推送至Consumer
。Push
方式下,消息队列RocketMQ
版还支持批量消费功能,可以将批量消息统一推送至Consumer
进行消费。 - Pull:消息由
Consumer
主动从消息队列RocketMQ
版拉取。
5.2.1 push方式实现
// 程序第一次启动从消息队列头获取数据
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 广播消费
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
// 注册回调实现类来处理从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);
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
5.2.2 pull方式实现
//消费者监听处理消息的方法,这里是抽取的方式
PullResult pullResult = consumer.pull(new MessageQueue("pull-push", "*");
for (MessageExt messageExt : pullResult.getMsgFoundList()) {
System.out.println("消费线程:" + Thread.currentThread().getName() +
", 消息ID:" + messageExt.getMsgId() +
", 消息内容:" + new String(messageExt.getBody()));
}
消费端的Push
模式是通过长轮询的模式来实现的,就如同下图:
Consumer
端每隔一段时间主动向broker
发送拉消息请求,broker
在收到Pull
请求后,如果有消息就立即返回数据,Consumer
端收到返回的消息后,再回调消费者设置的Listener
方法。如果broker
在收到Pull
请求时,消息队列里没有数据,broker
端会阻塞请求直到有数据传递或超时才返回。
当然,Consumer
端是通过一个线程将阻塞队列LinkedBlockingQueue<PullRequest>
中的PullRequest
发送到broker
拉取消息,以防止Consumer
一致被阻塞。而Broker
端,在接收到Consumer
的PullRequest
时,如果发现没有消息,就会把PullRequest
扔到ConcurrentHashMap
中缓存起来。
broker
在启动时,会启动一个线程不停的从ConcurrentHashMap
取出PullRequest
检查,直到有数据返回。
区别:
推送方式:必须消费者在线,当生产者生成了信息之后,消费者自动获取
抽取方式:当生成者生成了消息之后,这时刚刚启动消费者,自动抽取生成者没有消费的信息
注意:如果是
springboot
继承的话,消费者不会停止启动,都是使用推送方式
5.3 消息过滤
生产者创建消息
// 创建消息,并指定Topic,Tag和消息体生成者
Message msg = new Message("Topic",// Topic
"Tag", // Tag
("hello world").getBytes("utf-8") //Message body
);
5.3.1 Tag标签过滤
发送消息时我们会为每一条消息设置Tag
标签,同一大类中的消息放在一个主题Topic
下,但是如果进行分类我们则可以根据Tag
进行分类,每一类消费者可能不是关系某个主题下的所有消息,我们就可以通过Tag
进行过滤,订阅关注的某一类数据。
// 设置标签 消费者
// 订阅主题Topic和Tag
consumer.subscribe("Topic", "TagA || TagB");
消费者组订阅相同的主题不同的
Tag时
,如果订阅是多个Tag
则通过“||”分割
同一个消费者组订阅的主题,Tag
必须相同
5.3.2 SQL过滤
SQL92
表达式消息过滤,是通过消息的属性运行SQL
过滤表达式进行条件匹配,消息发送时需要设置用户的属性putUserProperty
方法设置属性。
支持的语法:
- 数值比较, 如
>,>=,<,<=,BETWEEN,=
; - 字符比较, 如
=,<>,IN
; IS NULL
或者IS NOT NULL
;- 逻辑连接符,
AND,OR,NOT
;
支持的常量类型:
- 数值型,如
123
,3.1415
; - 字符型,如‘abc’(必须用单引号);
- NULL,特殊常数;
- 布尔值,
TRUE
或FALSE
;
//订阅主题Topic和Tag
consumer.subscribe("filterSqlTopic", MessageSelector.bySql("i>5"));
注意:
- 只有使用
push
模式的消费者才能用使用SQL92
标准的sql
语句 SQL92
过滤默认关闭,需要在代理开始前设置conf
文件夹下broker.conf
的参数:
#开启对filter的支持
enablePropertyFilter=true
优化
拉取消息时进行过滤的性能很差:
- 堆外到堆,一旦每个消费者订阅相同的主题拉消息。
- 解码消息属性,一旦每个消费者订阅相同的主题拉消息。
可使用BloomFilter
预计算优化情况,具体请参考:RocketMQ通过SQL92过滤消息
注意:此优化默认关闭,需要在代理开始开启时设置一些配置:
- enableCalcFilterBitMap = true,表示在构建消费队列时计算位图。
- expectConsumerNumUseFilter = XX(Integer, default is 32), 表示估计订阅相同主题的消费者数量。
- maxErrorRateOfBloomFilter = XX(1~100,默认为20),表示布隆过滤器的错误率。
- enableConsumeQueueExt = true,表示构造消费队列扩展文件。
5.3.3 类过滤
通过定义消息过滤类的接口实现消息过滤
//使用Java代码,在服务器做消息过滤
String filterCode = MixAll.file2String("opt\\classfilter\\MessageFilterImpl.java");
consumer.subscribe("TopicFilter1", "cn.gumx.rocketmq.filter.MessageFilterImpl",filterCode);
自定义消息的过滤类
public class MessageFilterImpl implements MessageFilter {
public boolean match(MessageExt msg, FilterContext arg1) {
String property = msg.getUserProperty("age");
if (property != null) {
int age = Integer.parseInt(property);
if ((age % 3) == 0 && (age > 10)) {
return true;
}
}
return false;
}
}
使用类消息过滤模式,需要额外需要启动
filter
组件mqfiltersrv
服务,否则消费不了,每个broker
都需要启动一个,相当于加了一层过滤层。启动命令如下:./mqfiltersrv -n 10.10.12.203:9876;10.10.12.204:9876 &
filtersrv
出现了。减少了Broker
的负担,又减少了Consumer
接收无用的消息。当然缺点也是有的,多了一层filtersrv
网络开销
MessageFilterImpl
消息过滤实现类中的代码最好不要带有中文防止错误
注意:RocketMQ4.3.1
开始删除与mqfilter
服务器相关的脚本,4.3.2
删除客户端关于mqfilter
客户端代码,后面版本不支持该功能。