一、发送 & 接收顺序消息
1.1、概述
顺序消息是一种对消息发送和消费顺序有严格要求的消息。对于一个指定的Topic,消息严格按照先进先出(FIFO)的原则进行消息发布和消费,即先发布的消息先消费,后发布的消息后消费。在 Apache RocketMQ 中支持分区顺序消息,如下图所示。我们可以按照某一个标准对消息进行分区(比如图中的ShardingKey),同一个ShardingKey的消息会被分配到同一个队列中,并按照顺序被消费。需要注意的是 RocketMQ 消息的顺序性分为两部分,生产顺序性和消费顺序性。只有同时满足了生产顺序性和消费顺序性才能达到上述的FIFO效果。
1.2、生产顺序性
RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。如需保证消息生产的顺序性,则必须满足以下条件:
(一)单一生产者:
消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的分区键,不同生产者之间产生的消息也无法判定其先后顺序;
(二)串行发送:
生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。
满足以上条件的生产者,将顺序消息发送至服务端后,会保证设置了同一分区键的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:
1.3、应用场景
顺序消息的应用场景,其实在现实生活中也是随处可见,例如:工厂里边的流水线、网上商城购物等,就拿网上商城购物来说,一般会经历如下几个环节:
下订单===>减库存===>增加积分,该顺序需要严格一致,否则交易系统就会紊乱。
1.4、Demo06MQTestApp
/**
* @Author : 一叶浮萍归大海
* @Date: 2023/12/25 11:18
* @Description: 发送 & 接收顺序消息
*/
@Slf4j
public class Demo06MQTestApp {
/**
* 发送顺序消息
*/
@Test
public void demo6Producer() throws Exception {
// 1、创建一个生产者
DefaultMQProducer producer = new DefaultMQProducer("orderly-producer-group");
// 2、连接NameServer
producer.setNamesrvAddr(RocketMQConstant.NAME_SERVER_ADDR);
// 3、启动
producer.start();
for (Integer i = 1; i <= 4; i++) {
for (int j = 1; j <= 3; j++) {
String data = "";
switch (j % 3) {
case 1:
data = "下订单,订单ID:" + i;
break;
case 2:
data = "减库存,订单ID:" + i;
break;
case 0:
data = "增加积分,订单ID:" + i;
break;
}
// 4、创建顺序消息
Message message = new Message("orderly-topic","orderly-tag", i.toString(),data.getBytes(StandardCharsets.UTF_8));
// 5、发送消息
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> queues, Message msg, Object o) {
int hashCode = message.getKeys().hashCode();
// 队列的数量
int size = queues.size();
// 队列的索引
int index = hashCode % size;
log.info("当前操作:{},hashCode:{},size:{},index:{},keys:{}",StrUtil.utf8Str(message.getBody()),hashCode,size,index,message.getKeys());
return queues.get(index);
}
}, i);
}
}
log.info("【demo6Producer】发送消息成功!");
// 6、关闭生产者
producer.shutdown();
}
/**
* 接收顺序消息(Push方式)
*/
@Test
public void demo6PushConsumer1() throws Exception {
// 1、创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
// 2、连接NameServer
consumer.setNamesrvAddr(RocketMQConstant.NAME_SERVER_ADDR);
// 3、订阅消息,*表示订阅该主题所有的消息
consumer.subscribe("orderly-topic", "*");
// 4、设置监听器(采用异步回调方式,一直监听)
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> messages, ConsumeOrderlyContext context) {
for (MessageExt message : messages) {
log.info("我是消费者【demo6PushConsumer1】,我收到的消息是:{},队列ID:{}",StrUtil.utf8Str(message.getBody()),message.getQueueId());
}
/**
* 返回值:消费消息成功与否
* SUCCESS:表明消费成功,消息会从MQ出队
*/
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5、启动
consumer.start();
log.info("【demo6PushConsumer1】启动成功,正在等待接收消息...");
// 6、挂起当前JVM
System.in.read();
}
@Test
public void demo6PushConsumer2() throws Exception {
// 1、创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
// 2、连接NameServer
consumer.setNamesrvAddr(RocketMQConstant.NAME_SERVER_ADDR);
// 3、订阅消息,*表示订阅该主题所有的消息
consumer.subscribe("orderly-topic", "*");
// 4、设置监听器(采用异步回调方式,一直监听)
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> messages, ConsumeOrderlyContext context) {
for (MessageExt message : messages) {
log.info("我是消费者【demo6PushConsumer2】,我收到的消息是:{},队列ID:{}",StrUtil.utf8Str(message.getBody()),message.getQueueId());
}
/**
* 返回值:消费消息成功与否
* SUCCESS:表明消费成功,消息会从MQ出队
*/
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5、启动
consumer.start();
log.info("【demo6PushConsumer2】启动成功,正在等待接收消息...");
// 6、挂起当前JVM
System.in.read();
}
@Test
public void demo6PushConsumer3() throws Exception {
// 1、创建一个消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderly-consumer-group");
// 2、连接NameServer
consumer.setNamesrvAddr(RocketMQConstant.NAME_SERVER_ADDR);
// 3、订阅消息,*表示订阅该主题所有的消息
consumer.subscribe("orderly-topic", "*");
// 4、设置监听器(采用异步回调方式,一直监听)
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> messages, ConsumeOrderlyContext context) {
for (MessageExt message : messages) {
log.info("我是消费者【demo6PushConsumer3】,我收到的消息是:{},队列ID:{}",StrUtil.utf8Str(message.getBody()),message.getQueueId());
}
/**
* 返回值:消费消息成功与否
* SUCCESS:表明消费成功,消息会从MQ出队
*/
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 5、启动
consumer.start();
log.info("【demo6PushConsumer3】启动成功,正在等待接收消息...");
// 6、挂起当前JVM
System.in.read();
}
}
1.5、测试
先后启动demo6PushConsumer1、demo6PushConsumer2、demo6PushConsumer3和demo6Producer,观察控制台日志输出信息。