rocketmq-各类型消息实战(三)

系列文章目录

rocketmq-安装篇(一)
rocketmq-手把手搭建集群模式(二)
rocketmq-各类型消息实战(三)
rocketmq-名词解释(四)
rocketmq-消息存储(五)



前言

通过实战代码展示发送各种类型的消息。废话不多说,直接上干货。


一消费模式

两种模式:集群模式、广播模式

1.1集群模式

默认模式-负载均衡

  • 同组消费者同一条信息只被消费一遍;
  • 集群模式的消费进度是保存在Broker端的,所以即使应用崩溃,消费进度也不会出错
  • 场景: 异步通信、削峰等对消息没有顺序要求的场景都适合集群消费

1.2广播模式

  • 所有消费者组(即使同组)中的全部消费者实例将消费整个 Topic 的全部消息;组名失效
  • 消费进度:广播消费的消费进度保存在客户端机器的文件中。如果文件弄丢了,那么消费进度就丢失了,可能会导致部分消息没有消费
  • 场景:广播消费比较适合各个消费者实例都需要通知的场景,比如刷新应用服务器中的缓存

1.3指定模式

/**
 * 设置消费模型,集群还是广播,默认为集群
 * MessageModel.CLUSTERING、MessageModel.BROADCASTING、
 */
consumer.setMessageModel(MessageModel.CLUSTERING);

二项目代码

此处只展示部分核心代码,源代码见:这里 查看
spring-boot-rocketmq、spring-boot-rocketmq-consumer两个模块即可。

2.1生产者

spring-boot-rocketmq,步骤如下 :

  • 在application.properties 配置参数
  • 定义配置类读取参数,MQProducerConfigure
  • 编写controller,注入defaultMQProducer,在接口中直接发送消息即可
    config:

@ConfigurationProperties(prefix = "rocketmq.producer")
public class MQProducerConfigure {
	public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerConfigure.class);

	private String groupName;
	private String namesrvAddr;
	// 消息最大值
	private Integer maxMessageSize;
	// 消息发送超时时间
	private Integer sendMsgTimeOut;
	// 失败重试次数
	private Integer retryTimesWhenSendFailed;

	/**
	 * mq 生成者配置
	 * @return
	 * @throws MQClientException
	 */
	@Bean
	@ConditionalOnProperty(prefix = "rocketmq.producer", value = "isOnOff", havingValue = "on")
	public DefaultMQProducer defaultProducer() throws MQClientException {
		LOGGER.info("defaultProducer 正在创建---------------------------------------");
		DefaultMQProducer producer = new DefaultMQProducer(groupName);
		producer.setNamesrvAddr(namesrvAddr);
		producer.setVipChannelEnabled(false);
		producer.setMaxMessageSize(maxMessageSize);
		producer.setSendMsgTimeout(sendMsgTimeOut);
		producer.setRetryTimesWhenSendAsyncFailed(retryTimesWhenSendFailed);
		producer.start();
		LOGGER.info("rocketmq producer server 开启成功----------------------------------");
		return producer;
	}
}

controller:


/**
 * @author: 
 * @Date: 2020/4/21 11:17
 * @Description:
 */
@RestController
@RequestMapping("/mqProducer")
public class MQProducerController {
	public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerController.class);

	@Autowired
	DefaultMQProducer defaultMQProducer;
	@Autowired
	RocketmqMessageLogMapper rocketmqMessageLogMapper;
	@Autowired
	private UidGenService uidGenService;
	/**
	 * 同步发送简单的MQ消息
	 * @param msg
	 * @return
	 */
	@RequestMapping("/sendSync")
	public BaseResponse sendSync(String msg) throws Exception {
		if (StringUtils.isEmpty(msg)) {
			return new BaseResponse().succeed();
		}
		LOGGER.info("发送MQ消息内容:" + msg);
		Message sendMsg = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());
		// 默认3秒超时 ,同步发送消息
		SendResult sendResult = defaultMQProducer.send(sendMsg);
		LOGGER.info("消息发送响应:" + sendResult.toString());
		BaseResponse response = new BaseResponse().succeed();
		response.setData(sendResult);
		return response;
	}

	/**
	 * 异步发送简单的MQ消息
	 * @param
	 * @return
	 */
	@RequestMapping("/sendAsync")
	public BaseResponse sendAsync(@RequestParam  Map<String,String> map) throws Exception {
		/*if (StringUtils.isEmpty(timestamp)) {
			return new BaseResponse().succeed();
		}*/

		String topic  = "HelloTopic";
		String tags = "TestTag";

		map.put(PublicConstants.TRACE_ID,MDC.get(PublicConstants.TRACE_ID));
		String msg = JSONObject.toJSONString(map);
		LOGGER.info("发送MQ消息内容:" + msg);
		Message message = new Message(topic, tags, UUID.randomUUID().toString(), msg.getBytes());

		// 默认3秒超时 ,异步发送消息
		defaultMQProducer.send(message, new SendCallback() {
			@Override
			public void onSuccess(SendResult sendResult) {
				LOGGER.info("发送MQ消息成功:{}" ,sendResult);
				System.out.println(sendResult);
				//保存消费消息
				RocketmqMessageLog rocketmqMessageLog = new RocketmqMessageLog();
				rocketmqMessageLog.setId(uidGenService.getUid());
				rocketmqMessageLog.setMsgBody(msg);
				rocketmqMessageLog.setMsgId(sendResult.getMsgId());
				rocketmqMessageLog.setQueueId(sendResult.getMessageQueue().getQueueId());
				rocketmqMessageLog.setTopic(topic);
				rocketmqMessageLog.setTags(tags);
				rocketmqMessageLogMapper.insert(rocketmqMessageLog);
			}

			@Override
			public void onException(Throwable e) {
				LOGGER.info("发送MQ消息异常:",e);
				System.out.println(e.getMessage());
			}
		});
		LOGGER.info("消息发送完成" );
		BaseResponse response = new BaseResponse().succeed();
		return response;
	}
}


application.properties:


spring.application.name=spring-boot-rocketmq
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
######################rocketmq 生产者配置
# 是否开启自动配置
rocketmq.producer.isOnOff=on
# 发送同一类消息设置为同一个group,保证唯一默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标识
rocketmq.producer.groupName= springbootRocketmqProducer
# mq的nameserver地址
rocketmq.producer.namesrvAddr=192.168.149.128:9876
# 消息最大长度 默认 1024 * 4 (4M)
rocketmq.producer.maxMessageSize = 4096
# 发送消息超时时间,默认 3000ms
rocketmq.producer.sendMsgTimeOut=3000
# 发送消息失败重试次数,默认2
rocketmq.producer.retryTimesWhenSendFailed=2

2.2消费者

spring-boot-rocketmq-consumer,步骤如下 :

  • 在application.properties 配置参数
  • 定义配置类读取参数,MQConsumerConfigure
  • 编写监听器,MQConsumeConcurrentListener,针对业务逻辑处理接收到的信息即可。

MQConsumerConfigure:


/**
 * @author 
 * @date 2020年10月19日 14:55
 * @Description: mq消费者配置
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq.consumer")
public class MQConsumerConfigure {
	public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumerConfigure.class);

	private String groupName;
	private String namesrvAddr;
	private String topics;
	// 消费者线程数据量
	private Integer consumeThreadMin;
	private Integer consumeThreadMax;
	private Integer consumeMessageBatchMaxSize;

	@Autowired
	private MQConsumeConcurrentListener mqConsumeSequenceListener;
	/**
	 * mq 消费者配置
	 * @return
	 * @throws MQClientException
	 */
	@Bean
	@ConditionalOnProperty(prefix = "rocketmq.consumer", value = "isOnOff", havingValue = "on")
	public DefaultMQPushConsumer defaultConsumer() throws MQClientException {
		LOGGER.info("defaultConsumer 正在创建---------------------------------------");
		DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
		consumer.setNamesrvAddr(namesrvAddr);
		consumer.setConsumeThreadMin(consumeThreadMin);
		consumer.setConsumeThreadMax(consumeThreadMax);
		consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
		// 设置监听
		consumer.registerMessageListener(mqConsumeSequenceListener);

		/**
		 * 设置consumer第一次启动是从队列头部开始还是队列尾部开始
		 * 如果不是第一次启动,那么按照上次消费的位置继续消费
		 */
		consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
		/**
		 * 设置消费模型,集群还是广播,默认为集群
		 */
		//consumer.setMessageModel(MessageModel.CLUSTERING);

		try {
			// 设置该消费者订阅的主题和tag,如果订阅该主题下的所有tag,则使用*,
			String[] topicArr = topics.split(";");
			for (String tag : topicArr) {
				String[] tagArr = tag.split("~");
				consumer.subscribe(tagArr[0], tagArr[1]);
			}
			consumer.start();
			LOGGER.info("consumer 创建成功 groupName={}, topics={}, namesrvAddr={}",groupName,topics,namesrvAddr);
		} catch (MQClientException e) {
			LOGGER.error("consumer 创建失败!");
		}
		return consumer;
	}
}

MQConsumeMsgListener:


/**
 * @author: 
 * @Date: 2020/4/21 11:05
 * @Description: 消费者监听
 */
@Component
public class MQConsumeConcurrentListener implements MessageListenerConcurrently {
	public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumeConcurrentListener.class);

	/**
	 * 默认msg里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
	 * 不要抛异常,如果没有return CONSUME_SUCCESS ,consumer会重新消费该消息,直到return CONSUME_SUCCESS
	 * @param msgList
	 * @param consumeConcurrentlyContext
	 * @return
	 */
	@Override
	public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
		if (CollectionUtils.isEmpty(msgList)) {
			LOGGER.info("MQ接收消息为空,直接返回成功");
			return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
		}
		MessageExt messageExt = msgList.get(0);
		LOGGER.info("MQ接收到的消息为:" + messageExt.toString());
		try {
			String topic = messageExt.getTopic();
			String tags = messageExt.getTags();
			String body = new String(messageExt.getBody(), "utf-8");
			//JSONObject jsonObject = JSONObject.parseObject(body);
			//LOGGER.info("MQ消息traceId={},topic={}, tags={}, 消息内容={}",jsonObject.getString("traceId"), topic,tags,body);
			LOGGER.info("MQ消息,topic={}, tags={}, 消息内容={}", topic,tags,body);
		} catch (Exception e) {
			LOGGER.error("获取MQ消息内容异常{}",e);
		}
		// TODO 处理业务逻辑
		return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
	}
}

application.properties:


spring.application.name=spring-boot-rocketmq
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
######################rocketmq 消费者配置
# 是否开启自动配置
rocketmq.consumer.isOnOff=on
# 发送同一类消息设置为同一个group,保证唯一默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标识
rocketmq.consumer.groupName=springbootRocketmqConsumer
# mq的nameserver地址
rocketmq.consumer.namesrvAddr=192.168.149.128:9876
# 消费者订阅的主题topic和tags(*标识订阅该主题下所有的tags),格式: topic~tag1||tag2||tags3;
# rocketmq.consumer.topics=TestTopic~TestTag;TestTopic~HelloTag;HelloTopic~HelloTag;MyTopic~*
rocketmq.consumer.topics=TestTopic~*
# 消费者线程数据量
rocketmq.consumer.consumeThreadMin=5
rocketmq.consumer.consumeThreadMax=32
# 设置一次消费信息的条数,默认1
rocketmq.consumer.consumeMessageBatchMaxSize=1

2.3搞一下

自己把代码下载下来,修改下:application.properties对应地址

rocketmq.consumer.namesrvAddr=192.168.149.128:9876

不知道怎么搞nameserver的兄弟,可以看下之前文章,手把手的教你搭建rocketmq服务。

发送端:

//spring-boot-rocketmq 项目跑一下启动类SpringBootRocketmqApplication
//直接访问 MQProducerController 里面的rest接口就搞定了
//此处是:
localhost:8004/mqProducer/sendSync?msg=小兄弟你好呀

消费端:

//spring-boot-rocketmq-Consumer 项目跑一下启动类SpringBootRocketmqConsumerApplication
//注意看MQConsumeConcurrentListener这个类,会打印接收到的消息日志:
MQ消息,topic=TestTopic, tags=TestTag, 消息内容=小兄弟你好呀

三基本消息类型

3.1同步消息

同步等待:broker返回了响应结果,再往后执行。

//发送消息到一个Broker
SendResult sendResult=producer.send(msg);

可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

3.2异步消息

给broker发送了消息就执行往后执行,等broker返回了执行结果,会回调相应的预定义的方法。

		// 默认3秒超时 ,异步发送消息
		defaultMQProducer.send(message, new SendCallback() {
			@Override
			public void onSuccess(SendResult sendResult) {
				LOGGER.info("发送MQ消息成功:{}" ,sendResult);
				System.out.println(sendResult);
				//保存消费消息
				RocketmqMessageLog rocketmqMessageLog = new RocketmqMessageLog();
				rocketmqMessageLog.setId(uidGenService.getUid());
				rocketmqMessageLog.setMsgBody(msg);
				rocketmqMessageLog.setMsgId(sendResult.getMsgId());
				rocketmqMessageLog.setQueueId(sendResult.getMessageQueue().getQueueId());
				rocketmqMessageLog.setTopic(topic);
				rocketmqMessageLog.setTags(tags);
				rocketmqMessageLogMapper.insert(rocketmqMessageLog);
			}

			@Override
			public void onException(Throwable e) {
				LOGGER.info("发送MQ消息异常:",e);
				System.out.println(e.getMessage());
			}
		});

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。

3.3单向消息

这种方式主要用在不特别关心发送结果的场景,例如日志发送。

	/**
	 * 发送单向的MQ消息
	 * @param msg
	 * @return
	 */
	@RequestMapping("/sendOneWay")
	public BaseResponse sendOneWay(String msg) throws Exception {
		if (StringUtils.isEmpty(msg)) {
			return new BaseResponse().succeed();
		}
		LOGGER.info("发送MQ消息内容:" + msg);
		Message sendMsg = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());
		// 发送消息
		defaultMQProducer.sendOneway(sendMsg);
		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

四场景化消息类型

4.1顺序消息

  • 消息有序指的是可以按照消息的发送顺序来消费。
  • 顺序消费的原理解析: 在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列),而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上单线程依次拉取,则就保证了顺序。
  • RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

多线程拉取消息?
如果采用 实现MessageListenerConcurrently的方式,此时是多线程同时从一个队列中获取消息,无法保证有序;采用MessageListenerOrderly 是通过单线程从一个队列中获取消息,保证有序。

下面用订单进行分区有序的的示例,一个订单顺序流程:创建订单、锁定库存、推送订单通知,订单号相同的消息会被先后发送到同一个队列中,消费时,同一个order对应的信息肯定在同一个队列。

发送端:

/**
	 * 发送顺序消息
	 * @param
	 * @return
	 */
	@RequestMapping("/sendSequence")
	public BaseResponse sendSequence() throws Exception {
		/**
		 * 造数据
		 */
		List<OrderStep> orders = getOrders();
		orders.stream().forEach(order -> {
			String jsonString = JSONObject.toJSONString(order);
			LOGGER.info("sendSequence-发送MQ消息内容:" + jsonString);
			Message message = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), jsonString.getBytes());
					// 发送消息
					try {
						/**
						 * 参数1:消息对象
						 * 参数2:队列选择器
						 * 参数3:选择队列的业务标识-订单id
						 */
						defaultMQProducer.send(message, new MessageQueueSelector() {
                            @Override
                            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                                Long id = (Long) arg;
                                //根据订单id选择发送哪个queue
                                long index = id % mqs.size();
                                LOGGER.info("sendSequence-订单id={},队列={}", id, index);
                                return mqs.get((int) index);
                            }
                        }, order.getOrderId());
					} catch (Exception e) {
						LOGGER.info("sendSequence-发送消息异常:{}",e);
					}
				}
		);
		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

消费端:

/**
 * @author:
 * @Date: 2020/4/21 11:05
 * @Description: 消费者监听--多线程获取消息,
 */
@Component
public class MQConsumeSequenceListener implements MessageListenerOrderly {
	public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumeSequenceListener.class);

	/**
	 * 默认msg里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
	 * 不要抛异常,如果没有return SUCCESS ,consumer会重新消费该消息,直到return SUCCESS
	 * @param msgList
	 * @param consumeOrderlyContext
	 * @return
	 */
	public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgList, ConsumeOrderlyContext consumeOrderlyContext) {
		if (CollectionUtils.isEmpty(msgList)) {
			LOGGER.info("MQ接收消息为空,直接返回成功");
			return ConsumeOrderlyStatus.SUCCESS;
		}

		try {
			LOGGER.info("MQ接收到的消息msgSize=[{}]", msgList.size());
			for (MessageExt messageExt : msgList) {
				if (null == messageExt) {
					LOGGER.info("mq消息msg为空,直接处理下一条消息");
					continue;
				}
				String topic = messageExt.getTopic();
				String tags = messageExt.getTags();
				String body = new String(messageExt.getBody(), "utf-8");
				//JSONObject jsonObject = JSONObject.parseObject(body);
				//LOGGER.info("MQ消息traceId={},topic={}, tags={}, 消息内容={}",jsonObject.getString("traceId"), topic,tags,body);
				LOGGER.info("MQ消息,topic={}, tags={}, 消息内容={}", topic,tags,body);

			}
		} catch (Exception e) {
			LOGGER.error("获取MQ消息内容异常{}",e);
		}

		// TODO 处理业务逻辑
		return ConsumeOrderlyStatus.SUCCESS;
	}

}

4.2延时消息

  • 消费者延时消费消息。比如电商里,提交了一个订单,当时就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

它的实现与普通消息的发送和消费没多大区别,只多了一句话:

message.setDelayTimeLevel()

使用限制
现在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";

发送端:

	/**
	 * 发送延时消息
	 * @param msg
	 * @return
	 */
	@RequestMapping("/sendDelay")
	public BaseResponse sendDelay(String msg) throws Exception {
		if (StringUtils.isEmpty(msg)) {
			return new BaseResponse().succeed();
		}
		LOGGER.info("发送MQ消息内容:" + msg);

		Message message = new Message("delayTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());

		// 设置延时等级2,这个消息将在5s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
		message.setDelayTimeLevel(2);
		// 发送消息
		SendResult sendResult = defaultMQProducer.send(message);
		// 打印结果
		System.out.println(String.format("SendResult status:%s, queueId:%d",
				sendResult.getSendStatus(),
				sendResult.getMessageQueue().getQueueId()));
		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

消费端: 与普通消费一致

4.3批量消息

  • 批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic相同的waitStoreMsgOK,而且不能是延时消息。这一批消息的总大小不应超过4MB。

发送端:

/**
	 * 发送批量消息
	 */
	@RequestMapping("/sendBatch")
	public BaseResponse sendBatch() throws Exception {

		// 创建批量消息
		List<Message> messageList = new ArrayList<>();
		Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
		Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
		Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
		messageList.add(message1);
		messageList.add(message2);
		messageList.add(message3);

		LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));

		// 发送消息
		SendResult sendResult = defaultMQProducer.send(messageList);
		// 打印结果
		System.out.println(String.format("SendResult status:%s, queueId:%d",
				sendResult.getSendStatus(),
				sendResult.getMessageQueue().getQueueId()));
		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

大于4MB的批量消息发送
要发送大于4MB的消息,我们只需要把大的消息分裂成若干个小的消息。首先创建一个拆分消息的工具类

public class ListSplitter implements Iterator<List<Message>> {
    private final int SIZE_LIMIT = 1024 * 1024 * 4;
    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();
            }
            // 增加日志的开销20字节
            tmpSize = tmpSize + 20;

            if (tmpSize > SIZE_LIMIT) {
                //单个消息超过了最大的限制
                //忽略,否则会阻塞分裂的进程
                if (nextIndex - currIndex == 0) {
                    //假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
                    nextIndex++;
                }
                break;
            }
            if (tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }

        }
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }
}

然后在实现发送消息,和上面的区别在于在发送消息前,使用了工具类分隔消息

/**
	 * 发送批量消息
	 */
	@RequestMapping("/sendBatch")
	public BaseResponse sendBatch() throws Exception {

		// 创建批量消息
		List<Message> messageList = new ArrayList<>();
		Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
		Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
		Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
		messageList.add(message1);
		messageList.add(message2);
		messageList.add(message3);

		LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));

		//发送批量消息:把大的消息分裂成若干个小的消息
		ListSplitter splitter = new ListSplitter(messageList);
		while (splitter.hasNext()) {
			try {
				List<Message> listItem = splitter.next();
				SendResult result = defaultMQProducer.send(listItem);
				System.out.println(String.format("SendResult status:%s, queueId:%d",
						result.getSendStatus(),
						result.getMessageQueue().getQueueId()));
			} catch (Exception e) {
				e.printStackTrace();
				//处理error
			}
		}

		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

消费端: 与普通消费一致

4.4过滤消息

  • 方式1: tag
    在消费端订阅感兴趣的tag即可,
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("defultGroup");
//消费者将接收包含 TagA 或 TagB 或 TagC 的消息,但是限制是一个消息只能有一个标签。
consumer.subscribe("Topic", "TagA || TagB || TagC");
  • 方式2: 根据msg自定义属性值 通过SQL92表达式进行筛选,
    例如:
 messages.putUserProperty("index", String.valueOf(i));

SQL92基本语法

RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。

  • 数值比较,比如:>,>=,<,<=,BETWEEN,=;
  • 字符比较,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 逻辑符号 AND,OR,NOT;

常量支持类型为:

  • 数值,比如:123,3.1415;
  • 字符,比如:‘abc’,必须用单引号包裹起来;
  • NULL,特殊的常量
  • 布尔值,TRUE 或 FALSE

使用限制
只有push模式的消费者支持使用 SQL92 进行消息过滤:

public void subscribe(finalString topic, final MessageSelector messageSelector)

发送端:


	/**
	 * 发送过滤消息
	 */
	@RequestMapping("/sendFilter")
	public BaseResponse sendFilter() throws Exception {

		// 创建批量消息
		List<Message> messageList = new ArrayList<>();
		Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
		Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
		Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
		messageList.add(message1);
		messageList.add(message2);
		messageList.add(message3);

		LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));

		for (int i = 0; i < 10; i++) {
			Message message = new Message("filterTopic",  "TagA", ("Hello filter message-" + i).getBytes());

			// 发送消息时,通过`putUserProperty`来设置消息的属性,这个属性在消费的时候会拿来写SQL语句
			message.putUserProperty("num", String.valueOf(i));

			SendResult sendResult = defaultMQProducer.send(message);

			System.out.println(String.format("SendResult status:%s, queueId:%d",
					sendResult.getSendStatus(),
					sendResult.getMessageQueue().getQueueId()));
		}

		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

消费端:
MQConsumerConfigure 需要调整订阅topic 部分

try {
			// 设置该消费者订阅的主题和tag,如果订阅该主题下的所有tag,则使用*,
			String[] topicArr = topics.split(";");
			for (String tag : topicArr) {
				String[] tagArr = tag.split("~");
				consumer.subscribe(tagArr[0], tagArr[1]);
			}
			//测试过滤消息
			consumer.subscribe("filterTopic", MessageSelector.bySql("TAGS = 'TagA' and num between 2 and 5"));

			consumer.start();
			LOGGER.info("consumer 创建成功 groupName={}, topics={}, namesrvAddr={}",groupName,topics,namesrvAddr);
		} catch (MQClientException e) {
			LOGGER.error("consumer 创建失败!");
		}

消息订阅的时候使用了 MessageSelector.bySql(); 表明使用 SQL92 的过滤模式,然后传入的字符串是符合 SQL92 语法的语句。
“TAGS”是关键字,为了兼容tag过滤,在SQL92过滤中,使用TAGS替代tag,然后“num”是消息生产时 message.putUserProperty(“num”, String.valueOf(i));设置的属性。

如果我的生产者有多个tag:

String[] tags = new String[] {"TagA", "TagB", "TagC"};
// 依次选择 tag
Message message = new Message("filterTopic",  tags[i % tags.length], ("Hello filter message-" + i).getBytes());
// 发送消息时,通过`putUserProperty`来设置消息的属性
message.putUserProperty("num", String.valueOf(i));

那么消费者可以这样过滤:

// 订阅 Topic
consumer.subscribe("filterTopic", MessageSelector.bySql("TAGS in ('TagA', 'TagB') and num between 2 and 5"));

需要注意的是,使用 MessageSelector.bySql 时,需要在broker.conf中配置enablePropertyFilter=true,否则会报如下错误:

Exception in thread "main" org.apache.rocketmq.client.exception.MQClientException: CODE: 1  DESC: The broker does not support consumer to filter message by SQL92
For more information, please visit the url, http://rocketmq.apache.org/docs/faq/

最后再重启 broker 就行了。

4.5事物消息

事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

  • 正常事务消息的发送及提交 (黑线)
  • 事务消息的补偿流程 (红线)。

在这里插入图片描述

正常事物流程

  1. 发送消息(half 消息)
  2. MQ服务端响应消息发送结果
  3. 根据发送结果执行本地事务(如果发送失败,此时half消息对业务不可见,本地事务逻辑不执行)。
  4. 根据本地事务状态执行Commit(Commit操作生成消息索引,消息对消费者可见)或者Rollback(直接删掉消息)

事务消息的补偿流程

  1. 对没有Commit/Rollback的事务消息(pending状态的消息),启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息)
  2. Producer收到回查消息,检查回查消息对应的本地事务的状态。
  3. 根据本地事务状态,重新Commit或者Rollback

补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况.

事务消息的状态
事务消息共有三种状态:提交状态、回滚状态、中间状态。

  • LocalTransactionState.COMMIT_MESSAGE:: 提交事务,它允许消费者消费此消息。
  • LocalTransactionState.ROLLBACK_MESSAGE: 回滚事务,它代表该消息将被删除,不允许被消费。
  • LocalTransactionState.UNKNOW: 中间状态,它代表需要检查消息队列来确定状态。

发生端: 这个单独写的测试代码


	/**
	 * 发送事务消息1
	 */
	@RequestMapping("/sendTransaction2")
	public BaseResponse sendTransaction2() throws Exception {

		// 创建 TransactionMQProducer 实例,并设置生产者组名
		TransactionMQProducer producer = new TransactionMQProducer("transactionGroup");
		// 设置 NameServer 地址
		producer.setNamesrvAddr("192.168.149.128:9876");
		// 设置 Tags
		String[] tags = {"TagA", "TagB", "TagC"};

		// 添加事务监听器
		producer.setTransactionListener(new TransactionListener() {
			/**
			 * 执行本地事务的方法
			 */
			@Override
			public LocalTransactionState executeLocalTransaction(Message message, Object o) {
				if (StringUtils.equals("TagA", message.getTags())) { // 如果是 TagA 的消息,就提交事务
					return LocalTransactionState.COMMIT_MESSAGE;
				} else if (StringUtils.equals("TagB", message.getTags())) { // 如果是 TagB 的消息,就回滚事务
					return LocalTransactionState.ROLLBACK_MESSAGE;
				} else if (StringUtils.equals("TagC", message.getTags())) { // 如果是 TagC 的消息,就把消息设置为 中间状态 ,使其待会儿进行回查本地事物
					return LocalTransactionState.UNKNOW;
				}
				return LocalTransactionState.UNKNOW;
			}

			/**
			 * 消息回查执行的方法
			 */
			@Override
			public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
				System.out.println("消息的Tag:" + messageExt.getTags());
				// 消息来回查的时候,进行提交事务
				return LocalTransactionState.COMMIT_MESSAGE;
			}
		});

		// 启动producer
		producer.start();

		// 分别给每一个 Tag 绑定一条消息
		for (int i = 0; i < tags.length; i++) {
			// 创建消息对象,指定主题Topic、Tag和消息体
			Message msg = new Message("TransactionTopic", tags[i], ("Hello Transaction" + i).getBytes());
			// 发送消息
			SendResult result = producer.sendMessageInTransaction(msg, null);
			// 打印发送结果
			System.out.println("发送结果:" + result.getSendStatus());
		}

		BaseResponse response = new BaseResponse().succeed();
		response.setData("successed");
		return response;
	}

注意: 在创建 Producer 时,是带事务的Producer: TransactionMQProducer
我们使用了 producer.setTransactionListener() 方法注册了一个事务监听器,使用的是 TransactionListener 接口,里面有两个方法:

  • LocalTransactionState executeLocalTransaction(Message var1, Object var2): 该方法是用作执行本地事务。在上面代码中,分别对TagA、TagB和TagC做了不同的事务操作。
  • LocalTransactionState checkLocalTransaction(MessageExt var1): 该方法用于检查本地事务状态,并回应消息队列的检查请求,在上面代码中,处于中间状态的 TagC 将会进行回查,就会执行该方法。

消费端: 与普通消费一致

最后执行的结果应该是只有 TagA 和 TagC 绑定的消息能够被正常消费,TagB 绑定的消息在事务中被 Rollback ,所以被 MQServer 删除了。

使用限制

  1. 事务消息不支持 延时消息批量消息
  2. 为了避免单个消息被check多次从而引起大部分队列消息的堆积,我们限制单个消息的check次数默认为15次,用户可以通过配置transactionCheckMax参数更改。如果一个消息checked超过了“transactionCheckMax”,这个消息就会被丢弃,同时默认打印一条error log。用户可以通过重写AbstractTransactionCheckListener来自定义处理方式。
  3. 根据配置文件中的transactionTimeout设置,事务性消息经过“transactionTimeout”时间后会被checked。用户可以在发送事务性消息时设置CHECK_IMMUNITY_TIME_IN_SECONDS属性,该属性的优先级高于transactionMsgTimeout。
  4. 一个事务性消息可能会checked 检查或consumed消费多次
  5. 已经commited的消息再次发送到用户的目标topic可能会失败。目前依靠日志记录(log record)。RocketMQ本身的高可用机制确保了高可用性。如果想确保事务性消息没有丢失并且保证了事务的完整性,可以使用同步双写机制。
  6. 事务性消息的Producer IDs不能与其他类型消息的Producer IDs共享。与其他类型的消息不同,事务性消息允许反向查询,通过Producer ID查询client。

五问题

5.1 使用sql92的的方式订阅消息时,及时在broker.conf 增加了配置,还是无法接收到
答:检查一下是否修改的是配置文件是启动时指定的问题。
5.2发送事务消失时,发送tagc的消息时发送的是UNKOWN,但是producer没有接收到事务确认回调,怎么回事?
答:添加事务监听器 此处需要注意rocketmq 从 4.3.0开始才支持事务,所以按照rocketmq的时候需要安装高版本的。–例子中的版本为4.2.0 不支持事务。为了测试事务消息,rocketmq服务及代码升级为了4.6.1
5.3 MessageListenerOrderly vs MessageListenerConcurrently(只设置一个线程) 有啥区别?
答:todo

5.4 两个消费组,一个设置为集群模式,一个设置为广播是什么效果?
消费记录哪里存储,消息消费次数?

答:todo


源代码见:这里


我向你奔赴而来,你就是星辰大海

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值