RocketMq : 消费消息的两种方式 pull 和 push

403 篇文章 639 订阅 ¥99.90 ¥299.90
RocketMQ提供Pull和Push两种消费消息方式。Push模式实时性高但增加服务端负载,消费者感觉消息被推送;Pull模式主动权在消费者,可控性好但需设置好时间间隔。本文通过代码示例展示了如何指定队列发送和拉取消息。
摘要由CSDN通过智能技术生成


在这里插入图片描述

1.概述

原创在:https://blog.csdn.net/zhangcongyi420/article/details/90548393

在RocketMQ中一般有两种获取消息的方式,一个是拉(pull,消费者主动去broker拉取),一个是推(push,主动推送给消费者)

区别是:

push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。

pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

从下面这张简单的示意图也可以大致看出其中的差别,相当于是说,push的方式是:消息发送到broker后,如果是push,则broker会主动把消息推送给consumer即topic中,而pull的方式是:消息投递到broker后,消费端需要主动去broker上拉消息,即需要手动写代码实现,

在这里插入图片描述

两种方式的优缺点对比:

push

实时性高,但增加服务端负载,消费端能力不同,如果push的速度过快,消费端会出现很多问题

pull

消费者从server端拉消息,主动权在消费端,可控性好,但是时间间隔不好设置,间隔太短,则空请求会多,浪费资源,间隔太长,则消息不能及时处理

在前面的几篇中均使用的是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();

	}

}

第一种方式,直接发送,启动程序,我们观察一下控制台的打印结果,
在这里插入图片描述
可以看到,消息是随机发送到当前topic下面的不同的队列中的,我们再看消费端代码,

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) {

				//System.out.println("我是从第1个队列获取消息的");
				// 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("收到了消息:" + 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;
	}

}

运行程序,我们观察一下控制台打印结果,可以看到,通过pullConsumer拉取消息需要从所有的messageQueue中获取消息遍历然后取出所有的消息进行消费,不同的queueId中的消息可能不同,
在这里插入图片描述

这时,我们在生产者代码里面,指定队列发送消息,需要使用到messageQueueSelector这个回调函数,具体用法比较简单,大家可以在API中看到,修改后的代码如下,

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,即偏移量的设置,关于偏移量的问题,大家可以先自行查找一下相关的资料,会在以后继续探讨,本篇的讲解到此结束,最后感谢观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值