RocketMQ 提供了两种顺序级别:
- 普通顺序消息 :Producer 将相关联的消息发送到相同的消息队列。
- 完全严格顺序 :在【普通顺序消息】的基础上,Consumer 严格顺序消费。
目前已知的应用只有数据库 binlog 同步强依赖严格顺序消息,其他应用绝大部分都可以容忍短暂乱序,推荐使用普通的顺序消息。
如下是 RocketMQ 官方文档对这两种顺序级别的定义:
- 普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。比如:用
orderId
作为hashKey
,那么这个订单的所有消息都会放在一个队列。 - 严格顺序消息模式下,消费者收到的所有消息均是有顺序的。
下面,我们开始示例。
6.1 Demo06Message
创建 [Demo06Message]消息类,提供给当前示例使用。代码如下:
package com.ebadagang.springboot.rocketmq.message;
/**
* 示例 06 的 Message 消息
*/
public class Demo06Message {
public static final String TOPIC = "DEMO_06";
/**
* 编号
*/
private Integer id;
public Demo06Message setId(Integer id) {
this.id = id;
return this;
}
public Integer getId() {
return id;
}
@Override
public String toString() {
return "Demo06Message{" +
"id=" + id +
'}';
}
}
6.2 Demo06Producer
创建 [Demo06Producer]类,它会使用 RocketMQ-Spring 封装提供的 RocketMQTemplate ,实现三种发送顺序消息的方式。代码如下:
package com.ebadagang.springboot.rocketmq.producer;
import com.ebadagang.springboot.rocketmq.message.Demo06Message;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Demo06Producer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public SendResult syncSendOrderly(Integer id) {
// 创建 Demo06Message 消息
Demo06Message message = new Demo06Message();
message.setId(id);
// 同步发送消息
return rocketMQTemplate.syncSendOrderly(Demo06Message.TOPIC, message, String.valueOf(id));
}
public void asyncSendOrderly(Integer id, SendCallback callback) {
// 创建 Demo06Message 消息
Demo06Message message = new Demo06Message();
message.setId(id);
// 异步发送消息
rocketMQTemplate.asyncSendOrderly(Demo06Message.TOPIC, message, String.valueOf(id), callback);
}
public void onewaySendOrderly(Integer id) {
// 创建 Demo06Message 消息
Demo06Message message = new Demo06Message();
message.setId(id);
// 异步发送消息
rocketMQTemplate.sendOneWayOrderly(Demo06Message.TOPIC, message, String.valueOf(id));
}
}
- 调用了对应的 Orderly 方法,从而实现发送顺序消息。
- 同时,需要传入方法参数
hashKey
,作为选择消息队列的键。@param hashKey use this key to select queue. for example: orderId, productId ...
- 一般情况下,可以使用订单号、商品号、用户编号。
在 RocketMQ 中,Producer 可以根据定义 MessageQueueSelector 消息队列选择策略,选择 Topic 下的队列。目前提供三种策略:
- SelectMessageQueueByHash ,基于
hashKey
的哈希值取余,选择对应的队列。 - SelectMessageQueueByRandom ,基于随机的策略,选择队列。
- SelectMessageQueueByMachineRoom ,机器空间,有点看不懂,目前是空的实现
public MessageQueue select(List
,暂时不管。mqs, Message msg, Object arg) { return null; } - 未使用 MessageQueueSelector 时,采用轮询的策略,选择队列。
源代码private MessageQueueSelector messageQueueSelector = new SelectMessageQueueByHash();
可以看出RocketMQTemplate 在发送顺序消息时,默认采用 SelectMessageQueueByHash 策略。如此,相同的 hashKey
的消息,就可以发送到相同的 Topic 的对应队列中。这种形式,就是我们上文提到的普通顺序消息的方式。其他MessageQueueSelector策略可以在producer
中,这样指定rocketMQTemplate.setMessageQueueSelector(new SelectMessageQueueByRandom());
。
6.3 Demo06Consumer
创建 [Demo06Consumer]类,实现 Rocket-Spring 定义的 RocketMQListener 接口,消费消息。代码如下:
package com.ebadagang.springboot.rocketmq.consumer;
import com.ebadagang.springboot.rocketmq.message.Demo06Message;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = Demo06Message.TOPIC,
consumerGroup = "demo06-consumer-group-" + Demo06Message.TOPIC,
consumeMode = ConsumeMode.ORDERLY // 设置为顺序消费
)
public class Demo06Consumer implements RocketMQListener {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onMessage(Demo06Message message) {
logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
// sleep 2 秒,用于查看顺序消费的效果
try {
Thread.sleep(2 * 1000L);
} catch (InterruptedException ignore) {
}
}
}
差异点,主要是 @RocketMQMessageListener 注解,通过设置了
consumeMode = ConsumeMode.ORDERLY
,表示使用顺序消费。
6.4 简单测试
创建 [Demo06ProducerTest]测试类,编写三个单元测试方法,调用 Demo06Producer 三种发送顺序消息的方式。代码如下:
package com.ebadagang.springboot.rocketmq.producer;
import com.ebadagang.springboot.rocketmq.Application;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CountDownLatch;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class Demo06ProducerTest {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private Demo06Producer producer;
@Test
public void testSyncSendOrderly() throws InterruptedException {
// 发送多条消息
for (int i = 0; i < 3; i++) {
int id = 1024; // 固定成 1024 ,方便我们测试是否发送到相同消息队列
SendResult result = producer.syncSendOrderly(id);
logger.info("[testSyncSendOrderly][发送编号:[{}] 发送结果:[{}]]", id, result);
}
// 阻塞等待,保证消费
new CountDownLatch(1).await();
}
@Test
public void testASyncSendOrderly() throws InterruptedException {
for (int i = 0; i < 3; i++) {
int id = 1024; // 固定成 1024 ,方便我们测试是否发送到相同消息队列
producer.asyncSendOrderly(id, new SendCallback() {
@Override
public void onSuccess(SendResult result) {
logger.info("[testASyncSendOrderly][发送编号:[{}] 发送成功,结果为:[{}]]", id, result);
}
@Override
public void onException(Throwable e) {
logger.info("[testASyncSendOrderly][发送编号:[{}] 发送异常]]", id, e);
}
});
}
// 阻塞等待,保证消费
new CountDownLatch(1).await();
}
@Test
public void testOnewaySendOrderly() throws InterruptedException {
for (int i = 0; i < 3; i++) {
int id = 1024; // 固定成 1024 ,方便我们测试是否发送到相同消息队列
producer.onewaySendOrderly(id);
logger.info("[testOnewaySendOrderly][发送编号:[{}] 发送完成]", id);
}
// 阻塞等待,保证消费
new CountDownLatch(1).await();
}
}
我们来执行#testSyncSendOrderly()
方法,测试同步发送顺序消息。控制台输出如下:
# Producer 同步发送 3 条顺序消息成功,都发送到了 Topic 为 DEMO_06 ,队列编号 queueId为 1 的消息队列上
2020-08-04 22:20:12.407 INFO 6816 --- [ main] c.e.s.r.producer.Demo06ProducerTest : [testSyncSendOrderly][发送编号:[1024] 发送结果:[SendResult [sendStatus=SEND_OK, msgId=240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E126F0000, offsetMsgId=6585E30D00002A9F000000000003A9A2, messageQueue=MessageQueue [topic=DEMO_06, brokerName=broker-a, queueId=1], queueOffset=0]]]
2020-08-04 22:20:12.485 INFO 6816 --- [ main] c.e.s.r.producer.Demo06ProducerTest : [testSyncSendOrderly][发送编号:[1024] 发送结果:[SendResult [sendStatus=SEND_OK, msgId=240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E12F70002, offsetMsgId=6585E30D00002A9F000000000003AAD0, messageQueue=MessageQueue [topic=DEMO_06, brokerName=broker-a, queueId=1], queueOffset=1]]]
2020-08-04 22:20:12.532 INFO 6816 --- [ main] c.e.s.r.producer.Demo06ProducerTest : [testSyncSendOrderly][发送编号:[1024] 发送结果:[SendResult [sendStatus=SEND_OK, msgId=240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E13450004, offsetMsgId=6585E30D00002A9F000000000003ABFE, messageQueue=MessageQueue [topic=DEMO_06, brokerName=broker-a, queueId=1], queueOffset=2]]]
# 第一条消息的消费,messageId=240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E126F0000
2020-08-04 22:20:30.451 INFO 6816 --- [MessageThread_1] c.e.s.rocketmq.consumer.Demo06Consumer : [onMessage][线程编号:183 消息内容:Demo06Message{id=1024}]
2020-08-04 22:20:32.454 INFO 6816 --- [MessageThread_1] a.r.s.s.DefaultRocketMQListenerContainer : consume 240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E126F0000 cost: 2034 ms
# 第二条消息的消费
2020-08-04 22:20:32.454 INFO 6816 --- [MessageThread_1] c.e.s.rocketmq.consumer.Demo06Consumer : [onMessage][线程编号:183 消息内容:Demo06Message{id=1024}]
2020-08-04 22:20:34.464 INFO 6816 --- [MessageThread_1] a.r.s.s.DefaultRocketMQListenerContainer : consume 240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E12F70002 cost: 2010 ms
# 第三条消息的消费,
2020-08-04 22:20:34.464 INFO 6816 --- [MessageThread_1] c.e.s.rocketmq.consumer.Demo06Consumer : [onMessage][线程编号:183 消息内容:Demo06Message{id=1024}]
2020-08-04 22:20:36.465 INFO 6816 --- [MessageThread_1] a.r.s.s.DefaultRocketMQListenerContainer : consume 240884E30114A66731EF8A0EAAFD768A1AA018B4AAC2143E13450004 cost: 2001 ms
Producer 发送顺序消息时,因为我们使用id = 1024
作为 hashKey ,所以都发送到了 Topic 为 "DEMO_06" ,队列编号为 1 的消息队列。
Consumer 顺序消费消息时,是在单线程中,顺序消费每条消息。
底线
本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址
下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。