RocketMQ 的两种消息消费模式:Pull 和 Push

概述:

        在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通常会进行消息推送的负载均衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值