概述:
在RocketMQ 中一般有两种获取消息的方式:Pull(拉取,消费端主动去broker拉取)和Push(推送,主动推送给消费端)。
区别:
pull:取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。
push:consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。
push的方式是:消息发送到broker后,如果是push,则broker会主动把消息推送给consumer即topic中,而pull的方式是:消息投递到broker后,消费端需要主动去broker上拉消息,即需要手动写代码实现.
优缺点:
pull:
1.主动权在消费端: 在Pull模式中,消费端主动向Broker请求消息,决定拉取的时机和拉取的数量。消费端有更大的控制权,可控性好,可以按照自己的需求灵活地拉取消息。
2.消息拉取频率: 消费端可以根据实际情况决定拉取消息的频率,可以定时拉取,也可以根据业务负载动态调整拉取消息的速度,时间间隔不好设置,间隔太短,则空请求会多,浪费资源,间隔太长,则消息不能及时处理。
3.流量控制: Pull模式下,消费端可以根据自身情况进行流量控制,避免瞬时大量消息涌入导致负载过重。
4.适用场景: Pull模式适用于需要更多消费端控制和自适应的场景,消费端需要根据实时负载情况自主调整拉取消息的速度。
push:
1.被动接收消息: 在Push模式中,消息是由Broker推送给消费端的,消费端不再需要主动去请求消息,而是等待消息到达。
2.实时性高: Push模式下,消息可以实时被推送给消费端,但会增加服务端负载,适用于需要实时响应的场景。
3.消息推送速率: Push模式下,消息的推送速率由Broker控制,可能会受到一些限制。如果消息推送速率过快,消费端会出现很多问题,消费端可能需要自行处理流控。
4.适用场景: Push模式适用于对实时性要求较高、消费端希望被动接收消息的场景。
pushConsumer的方式下,在Rocketmq中,对于每个指定的topic,默认的队列数量是4个,对于producer来说,发送消息到topic的时候,会随机为消息选择一个投递的队列,队列序号是 0~3;
但在实际业务中,有一些比较特殊的需要,比如顺序消费,其基本的原理就是通过指定队列来实现;更为常见的是,在某些情况下,如果我们不对消息指定发送顺序的话,消息会随机投递到队列,那么对于消费端来说,不好做负载均衡的消息分配;
设想,假如我们在某次电商抢购中需要生产两种消息,一个是产生订单的消息,另一个是发送短信的消息,而且我们的服务器数量有限,那么节省资源的方式就是通过指定队列也就是queueId来实现分类消息的发送,对于消费端来说,只需要通过上述的pullConsumer的方式从相同的topic下面获取指定的queueId的消息即可。
public class ProducerQueueSelector {
public static void main(String[] args) throws Exception {
// 声明并初始化一个producer
DefaultMQProducer producer = new DefaultMQProducer("producer1");
// 设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
// NameServer的地址必须有,也可以通过环境变量的方式设置,不一定非得写死在代码里
producer.setNamesrvAddr("192.168.111.132:9876");
producer.setVipChannelEnabled(false);//3.2.6的版本没有该设置,在更新或者最新的版本中务必将其设置为false,否则会有问题
producer.setRetryTimesWhenSendFailed(3);
// 调用start()方法启动一个producer实例
producer.start();
// 发送10条消息到Topic为TopicTest,tag为TagA,消息内容为“Hello RocketMQ”拼接上i的值
for (int i = 0; i < 10; i++) {
try {
Message msg = new Message("TopicTest", // topic
"TagA", // tag
"i" + i, ("Hello RocketMQ " + i).getBytes("utf-8")// body
);
// 调用producer的send()方法发送消息
// 这里调用的是同步的方式,所以会有返回结果
SendResult sendResult = producer.send(msg);
//指定消息投递的队列,同步的方式,会有返回结果
/*SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> queues, Message msg, Object queNum) {
int queueNum = Integer.parseInt(queNum.toString());
return queues.get(queueNum);
}
}, 0);*/
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "," + i);
// System.out.println(sendResult.getSendStatus()); //发送结果状态
// 打印返回结果,可以看到消息发送的状态以及一些相关信息
System.out.println("当前消息投递到的队列是 : " + sendResult.getMessageQueue().getQueueId());
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 发送完消息之后,调用shutdown()方法关闭producer
producer.shutdown();
}
}
运行后观察控制台打印结果,可以看到,通过pullConsumer拉取消息需要从所有的messageQueue中获取消息遍历然后取出所有的消息进行消费,不同的queueId中的消息可能不同。
然后在生产者代码里指定队列发送消息,需要使用到messageQueueSelector这个回调函数;
public class ProducerQueueSelector {
public static void main(String[] args) throws Exception {
// 声明并初始化一个producer
DefaultMQProducer producer = new DefaultMQProducer("producer1");
// 设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
// NameServer的地址必须有,也可以通过环境变量的方式设置,不一定非得写死在代码里
producer.setNamesrvAddr("192.168.111.132:9876");
producer.setVipChannelEnabled(false);//3.2.6的版本没有该设置,在更新或者最新的版本中务必将其设置为false,否则会有问题
producer.setRetryTimesWhenSendFailed(3);
// 调用start()方法启动一个producer实例
producer.start();
// 发送10条消息到Topic为TopicTest,tag为TagA,消息内容为“Hello RocketMQ”拼接上i的值
for (int i = 0; i < 10; i++) {
try {
Message msg = new Message("TopicTest", // topic
"TagA", // tag
"i" + i, ("你好,rocketMq " + i).getBytes("utf-8")// body
);
// 调用producer的send()方法发送消息
// 这里调用的是同步的方式,所以会有返回结果
//SendResult sendResult = producer.send(msg);
//指定消息投递的队列,同步的方式,会有返回结果
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> queues, Message msg, Object queNum) {
int queueNum = Integer.parseInt(queNum.toString());
return queues.get(queueNum);
}
}, 0);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "," + i);
// System.out.println(sendResult.getSendStatus()); //发送结果状态
// 打印返回结果,可以看到消息发送的状态以及一些相关信息
System.out.println("当前消息投递到的队列是 : " + sendResult.getMessageQueue().getQueueId());
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 发送完消息之后,调用shutdown()方法关闭producer
producer.shutdown();
}
}
启动程序后所有的消息都被发送到queueId为0的队列中了,
接下来在consumer端尝试,在对应代码的中做些调整,遍历messageQueue的时候,筛选queueId为0的消息,consumer端代码调整如下:
public class QueueConsumer {
private static final Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws Exception {
offsetTable.clear();
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pullConsumer");
consumer.setNamesrvAddr("192.168.111.132:9876");
consumer.start();
try {
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");
for (MessageQueue mq : mqs) {
//System.out.println("Consume from the queue: " + mq);
//System.out.println("当前获取的消息的归属队列是: " + mq.getQueueId());
if (mq.getQueueId() == 0) {
// long offset = consumer.fetchConsumeOffset(mq, true);
// PullResultExt pullResult
// =(PullResultExt)consumer.pull(mq,
// null, getMessageQueueOffset(mq), 32);
// 消息未到达默认是阻塞10秒,private long consumerPullTimeoutMillis =
// 1000 *
// 10;
PullResultExt pullResult = (PullResultExt) consumer.pullBlockIfNotFound(mq, null,
getMessageQueueOffset(mq), 32);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
System.out.println("我是从第1个队列获取消息的");
System.out.println("收到了消息:" + new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
default:
break;
}
}
}
} catch (MQClientException e) {
e.printStackTrace();
}
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, offset);
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offsetTable.get(mq);
if (offset != null)
return offset;
return 0;
}
}
运行之后看到控制台打印结果,经过筛选之后,我们只从queueId=0的队列中获取消息;
注意:
如果使用pullConsumer,在消费端程序中需要设定offSet,即偏移量的设置。
总结:
消费端驱动: 无论是Pull还是Push,消费端都是主动发起消费的一方。Pull模式中,消费端主动发起拉取消息的请求;Push模式中,虽然消息是被动推送给消费端,但消费端仍然需要主动处理接收到的消息。
消费端订阅: 无论是Pull还是Push,消费端都需要订阅消息的主题(Topic)。
消费端负载均衡: 在Pull模式中,如果有多个消费端订阅了同一个Topic,它们之间可能需要进行负载均衡,以确保每个消费端都能获取到一定比例的消息。在Push模式中,Broker通常会进行消息推送的负载均衡。