一、前文回顾
在上一篇文章中使用Springboot整合了RocketMQ,并实现了最简单的消费者和生产者,今天继续学习RocketMQ中其他类型的消息。
二、延时消息
顾名思义延时消息就是生产者生产消息后,消费者过一段时间才消费对应的消息。使用场景例如,定时任务、订单超时取消等等。
注意:在RocketMQ 4.X的版本是不支持自定义精度的延时,只支持18个级别,如下图:
但是到了RocketMQ5.0版本是可以支持自定义精度的延时。
1、创建一个类型为Delay的Topic
message.type可以使用 TRANSACTION, FIFO, DELAY 或NORMAL,如未指定该属性,默认是NORMAL。
./mqadmin updateTopic -a +message.type=DELAY -b 192.168.111.140:10911 -n 192.168.111.140:9876 -t TestDelay
(这里吐槽一下官方文档,并没有说明这一情况!!!!,最后还是百度到的https://blog.youkuaiyun.com/wwwwfw/article/details/127445987)
2、生产者
/**
* 发送延迟消息
*/
@GetMapping("/delay")
public void sendDelayMessage() throws ClientException, IOException {
//这里不直接使用rocketMQTemplate 是因为最新的rocketmq-spring-boot-starter 是2.2版本,其中Rocket MQ client版本为4.9.3
//而本demo则使用的是Rocket 5.0
String topic = "TestDelay";
//获取客户端服务提供者(不是消息生产者)
ClientServiceProvider provider = ClientServiceProvider.loadService();
//定义延时时间(延时10s)
long deliverTimeStamp = System.currentTimeMillis() + 10 * 1000;
Producer producer = MyRocketMQUtil.getProducer(topic, provider);
Message message = provider.newMessageBuilder().setTopic(topic)
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("delayKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("delayTag")
.setDeliveryTimestamp(deliverTimeStamp)
.setBody("你好我发送的是延时消息".getBytes())
.build();
try {
SendReceipt result = producer.send(message);
log.info("消费发送时间:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
log.info("消息ID:{}", result.getMessageId());
} catch (Exception e) {
e.printStackTrace();
}
}
结果:生产者生产消息的时间为 2022-11-28 13:33:40
3、消费者
public static void main(String[] args) throws ClientException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoints = "192.168.16.128:8081";
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder().setEndpoints(endpoints).build();
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 为消费者指定所属的消费者分组,Group需要提前创建。
String consumerGroup = "MyConsumerGroup";
// 指定需要订阅哪个目标Topic,Topic需要提前创建。
String topic = "TestDelay";
// 初始化PushConsumer,需要绑定消费者分组ConsumerGroup、通信参数以及订阅关系。
PushConsumer pushConsumer = provider.newPushConsumerBuilder().setClientConfiguration(clientConfiguration)
// 设置消费者分组。
.setConsumerGroup(consumerGroup)
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
// 处理消息并返回消费结果。
// LOGGER.info("Consume message={}", messageView);
System.out.println("Consume message!!,Time:"
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return ConsumeResult.SUCCESS;
}).build();
// 如果不需要再使用PushConsumer,可关闭该进程。
// pushConsumer.close();
}
结果:消费者收到消息的时间则是:2022-11-28 13:33:50,刚好延迟了10s
三、顺序消息
所谓顺序消息就和字面意思一样,消息是有顺序的,发送的时候是什么顺序,收到的时候也是什么顺序。
Apache RocketMQ 顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。
基于消息组的顺序判定逻辑,支持按照业务逻辑做细粒度拆分,可以在满足业务局部顺序的前提下提高系统的并行度和吞吐能力。
那么如何保证消息的顺序行呢?这就要分为两个方面,首先是生产者端:
- 单一生产者:消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。
- 串行发送:Apache RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。
这也很好理解,如果多个生产者同时生产消息,无法保证那个生产者先生产了消息;其次如果使用多线程并行的生产消息,也无法保证那个线程先完成。
结论:(1)单一生产者 (2)生产者串行发送 (3)消息的顺序性只体现在同一个消息组内,不同组不存在顺序性。
说完生产者在看消费者端:
1、顺序投递:RocketMQ 通过客户端SDK和服务端通信协议保障消息按照服务端存储顺序投递,但业务方消费消息时需要严格按照接收—处理—应答的语义处理消息,避免因异步处理导致消息乱序。
这里要注意一下:在RocketMQ中消费者分为两类,一类是PushConsumer(被动的接收消息),另一类的是SimpleConsumer(主动拉取消息),如果使用SimpleConsumer拉取消息,一次拉取多条消息则需要在业务方保证顺序性。
2、有限的重试:RocketMQ 顺序消息投递仅在重试次数限定范围内,即一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。、
总结:实现顺序消息需要生产者和消费者共同配合
1、创建FIFO主题
顺序消息仅支持使用MessageType为FIFO的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。所以我们要创建一个类型为FIFO的TOPIC
命令如下:./mqadmin updateTopic -a +message.type=FIFO -b 192.168.111.140:10911 -n 192.168.111.140:9876 -t TestFIFO
2、生产者
ClientServiceProvider provider = ClientServiceProvider.loadService();
String topic = "TestFIFO";
Producer producer = ProducerUtil.getProvider(topic, provider);
// 延时消息
Message message1 = provider.newMessageBuilder().setTopic(topic)
// 设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey1")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("fifoTag")
// 设置消息分组
.setMessageGroup("fifoGroup")
// 消息体。
.setBody("FIFO MESSAGE 1".getBytes()).build();
Message message2 = provider.newMessageBuilder().setTopic(topic)
// 设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey2")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("fifoTag")
// 设置消息分组
.setMessageGroup("fifoGroup")
// 消息体。
.setBody("FIFO MESSAGE 2".getBytes()).build();
try {
producer.send(message1);
producer.send(message2);
System.out.println("发送完成");
} catch (Exception e) {
e.printStackTrace();
}
3、消费者
public static void main(String[] args) throws ClientException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoints = "192.168.16.128:8081";
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder().setEndpoints(endpoints).build();
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 为消费者指定所属的消费者分组,Group需要提前创建。
String consumerGroup = "MyConsumerGroup";
// 指定需要订阅哪个目标Topic,Topic需要提前创建。
String topic = "TestFIFO";
// 初始化PushConsumer,需要绑定消费者分组ConsumerGroup、通信参数以及订阅关系。
PushConsumer pushConsumer = provider.newPushConsumerBuilder().setClientConfiguration(clientConfiguration)
// 设置消费者分组。
.setConsumerGroup(consumerGroup)
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
// 处理消息并返回消费结果。
// LOGGER.info("Consume message={}", messageView);
System.out.println("Consume message!! " + getString(messageView.getBody()));
return ConsumeResult.SUCCESS;
}).build();
// 如果不需要再使用PushConsumer,可关闭该进程。
// pushConsumer.close();
}
结果:
四、小结
本文简单的学习了RocketMQ中的延时消息和顺序消息,目前还差一种事务消息还没有学习。由于事务消息相对来说复杂,所以单独放在下一篇文章中。希望对你有所帮助!