目录
一、简介
RocketMQ 提供了一种发送顺序消息的方法,顺序消息是指消息发送和消费的顺序是有序的,即消息按照特定的顺序发送到 Broker,然后按照相同的顺序消费。这个方法就是RocketMQTemplate的syncSendOrderly。
1.1、特点
使用syncSendOrderly方法具有以下特点:
- 有序性 该方法可以保证消息被顺序消费,即同一个队列的消息按照发送顺序被消费,不会产生消息乱序的情况。
- 同步发送 该方法是同步发送方式,发送线程会被阻塞直到收到Broker的发送响应结果。
- 返回值 返回值是一个SendResult对象,包含消息发送的状态、消息ID等信息。
- 参数 除需指定消息和Topic外,还需额外指定一个hashKey参数,用于选择发送的队列。
- 吞吐量 由于同步发送和有序保证的存在,吞吐量相对较低,不适合对吞吐量要求较高的场景。
1.2、场景
一般来说,syncSendOrderly模式适用于以下场景:
- 对消息顺序性有严格要求的业务,如账户资金交易等
- 对消息可靠性传输有较高要求,可以容忍较低吞吐量的场景
二、顺序消息的消费者
2.1、Maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rocketmq</artifactId>
<groupId>com.alian</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>09-consume-ordered</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.alian</groupId>
<artifactId>common-rocketmq-dto</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
父工程已经在我上一篇文章里,通用公共包也在我上一篇文章里有说明,包括消费者。具体参考:RocketMQ笔记(一)SpringBoot整合RocketMQ发送同步消息
2.2、application配置
application.properties
server.port=8009
# rocketmq地址
rocketmq.name-server=192.168.0.234:9876
# 默认的消费者组
rocketmq.consumer.group=ORDERED_CONSUMER_GROUP
# 批量拉取消息的数量
rocketmq.consumer.pull-batch-size=10
# 集群消费模式
rocketmq.consumer.message-model=CLUSTERING
实际上对于本文来说,下面两个配置不用配置,也不会生效。
# 默认的消费者组
rocketmq.consumer.group=ORDERED_CONSUMER_GROUP
# 集群消费模式
rocketmq.consumer.message-model=CLUSTERING
2.3、application配置
package com.alian.ordered;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RocketMQMessageListener(topic = "ordered_string_message_topic",
consumerGroup = "ORDERED_GROUP_STRING",
consumeMode = ConsumeMode.ORDERLY)
public class StringMessageConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
log.info("字符串消费者接收到的消息: {}", message);
// 处理消息的业务逻辑
}
}
相比我们之前的消费者,最大的区别就是配置了属性consumeMode = ConsumeMode.ORDERLY,这也是顺序消息消费的核心配置,和把消费者线程数设置为 consumeThreadNumber = 1的作用差不多。RocketMQ 提供了两种消费模式:顺序消费模式(ConsumeMode.ORDERLY)和并发消费模式(ConsumeMode.CONCURRENTLY)。
- 顺序消费模式(ConsumeMode.ORDERLY):
在顺序消费模式下,消息会按照严格的顺序被消费,确保同一个消息队列中的消息是按照顺序被消费的。这意味着同一个消息队列中的消息会被同一个消费者实例顺序地消费,消费者之间不会同时消费同一个消息队列中的消息。也就是一个队列,一个线程。 - 并发消费模式(ConsumeMode.CONCURRENTLY):
在并发消费模式下,消息会被多个消费者实例并发地消费,同一个消息队列中的消息可以同时被多个消费者实例处理。这意味着同一个消息队列中的消息可以被多个消费者实例并发地处理,消费者之间可以同时消费同一个消息队列中的消息。
三、顺序消息的生产者
3.1、Maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rocketmq</artifactId>
<groupId>com.alian</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>03-send-one-way-message</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.alian</groupId>
<artifactId>common-rocketmq-dto</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
父工程已经在我上一篇文章里,通用公共包也在我上一篇文章里有说明,包括消费者。具体参考:RocketMQ笔记(一)SpringBoot整合RocketMQ发送同步消息
3.2、application配置
application.properties
server.port=8004
# rocketmq地址
rocketmq.name-server=192.168.0.234:9876
# 默认的生产者组
rocketmq.producer.group=ordered_group
# 发送同步消息超时时间
rocketmq.producer.send-message-timeout=3000
# 用于设置在消息发送失败后,生产者是否尝试切换到下一个服务器。设置为 true 表示启用,在发送失败时尝试切换到下一个服务器
rocketmq.producer.retry-next-server=true
# 用于指定消息发送失败时的重试次数
rocketmq.producer.retry-times-when-send-failed=3
# 设置消息压缩的阈值,为0表示禁用消息体的压缩
rocketmq.producer.compress-message-body-threshold=0
在 RocketMQ 中,RocketMQTemplate的syncSendOrderly方法,它允许你发送同步顺序消息,主要是三个参数
- topic
- payload
- hashkey
hashKey是该方法的一个重要参数,它决定了消息被发送到哪个消息队列,从而保证消息的有序性。具体来说:
- RocketMQ会对同一个Topic内的消息队列进行分区(sharding)
- 具有相同hashKey的消息会被发送到同一个队列分区
- 消费者会消费同一个队列分区中的所有消息,从而实现消息的顺序消费
3.3、同步发送顺序消息
SendSyncOrderlyMessageTest.java
@Slf4j
@SpringBootTest
public class SendSyncOrderlyMessageTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void sendOrderedStringMessage() {
String topic = "ordered_string_message_topic";
String message = "我是一条同步顺序消息:";
for (int i = 0; i < 5; i++) {
// hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
rocketMQTemplate.syncSendOrderly(topic, message + i, "alian_sync_ordered");
}
}
@Test
public void sendOrderedStringMessageWithBuilder() {
String topic = "ordered_string_message_topic";
String message = "我是一条同步顺序消息:";
for (int i = 0; i < 5; i++) {
Message<String> msg = MessageBuilder.withPayload(message + i).build();
// hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
rocketMQTemplate.syncSendOrderly(topic, msg, "alian_sync_ordered");
}
}
@AfterEach
public void waiting() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条同步顺序消息:0
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条同步顺序消息:1
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条同步顺序消息:2
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条同步顺序消息:3
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条同步顺序消息:4
3.4、错误的使用异步发送顺序消息
可能会有人看到有个方法asyncSendOrderly,然后顺手就写了如下的异步发送:
SendAsyncOrderlyMessageTest.java
@Slf4j
@SpringBootTest
public class SendAsyncOrderlyMessageTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
public void sendOrderedStringMessage() {
String topic = "ordered_string_message_topic";
String message = "我是一条异步顺序消息:";
for (int i = 0; i < 5; i++) {
// hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
int finalI = i;
rocketMQTemplate.asyncSendOrderly(topic, message + i, "alian_async_ordered", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 异步发送成功的回调逻辑
log.info("异步顺序消息【{}】发送成功: {}" , finalI, sendResult);
}
@Override
public void onException(Throwable e) {
// 异步发送失败的回调逻辑
log.info("异步顺序消息【{}】发送失败: {}",finalI, e.getMessage());
}
});
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Test
public void sendOrderedStringMessageWithBuilder() {
String topic = "ordered_string_message_topic";
String message = "我是一条异步顺序消息:";
for (int i = 0; i < 5; i++) {
int finalI = i;
Message<String> msg = MessageBuilder.withPayload(message + i).build();
// hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
rocketMQTemplate.asyncSendOrderly(topic, msg, "alian_async_ordered", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 异步发送成功的回调逻辑
log.info("异步顺序消息【{}】发送成功: {}" , finalI, sendResult);
}
@Override
public void onException(Throwable e) {
// 异步发送失败的回调逻辑
log.info("异步顺序消息【{}】发送失败: {}",finalI, e.getMessage());
}
});
}
}
@AfterEach
public void waiting() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
[GROUP_STRING_16] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:4
[GROUP_STRING_16] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:2
[GROUP_STRING_17] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:1
[GROUP_STRING_17] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:0
[GROUP_STRING_17] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:3
发现运行的结果并不是顺序消费的。这是因为:asyncSendOrderly的异步特性,asyncSendOrderly 方法是异步发送,生产者在发送消息后不会等待Broker响应直接返回。虽然消息仍然会根据hashKey被分发到同一队列分区,但由于异步发送的无序性,最终写入该队列分区的消息顺序可能与发送顺序不一致。
因此,哪怕把消费者线程数设置为 consumeThreadNumber = 1,也没有用,实际单线程消费时也是无序的。发送都无序了,顺序消费就没有意义了。syncSendOrderly方法能够保证消息发送和消费的顺序一致性,而asyncSendOrderly则无法做到这一点,主要是由于异步发送的无序性所导致。
结果就是异步发送的顺序并不是,你代码看到的顺序,为了也能达到顺序的结果,我们每发一次就休眠一段时间,如下:
@Test
public void sendOrderedStringMessage() {
String topic = "ordered_string_message_topic";
String message = "我是一条异步顺序消息:";
for (int i = 0; i < 5; i++) {
// hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
int finalI = i;
rocketMQTemplate.asyncSendOrderly(topic, message + i, "alian_async_ordered", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 异步发送成功的回调逻辑
log.info("异步顺序消息【{}】发送成功: {}" , finalI, sendResult);
}
@Override
public void onException(Throwable e) {
// 异步发送失败的回调逻辑
log.info("异步顺序消息【{}】发送失败: {}",finalI, e.getMessage());
}
});
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
[GROUP_STRING_18] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:0
[GROUP_STRING_19] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:1
[GROUP_STRING_20] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:2
[_GROUP_STRING_1] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:3
[_GROUP_STRING_2] com.alian.ordered.StringMessageConsumer : 字符串消费者接收到的消息: 我是一条异步顺序消息:4
看到结果是顺序的,那是因为,大部分情况下这些消息都发送到了同一个队列,一定是同一个队列,但是也存在消息超时发失败,或者网络不好的情况,发送慢,也不一定是顺序的,况且这种方式也牺牲了性能,降低了并发性,得不偿失。非常不建议这样操作。
当然异步单批次发送消息是可以顺序的,我们批量发送消息时讲解。