顺序消息
顺序消息分类
全局顺序
- 所有消息严格按照发送顺序被消费。
- 通常只能使用一个队列(单队列),吞吐量受限。
分区顺序
- 按照某个业务 key(如订单ID、用户ID)发送到 相同的队列,只在该 key 范围内保持顺序。
- 多个 key 的消息可以并发消费,各自内部有序。
使用示例
Producer
生产者发送顺序消息时,需要自定义一个 MessageQueueSelector
选择器,用来将具有相同业务标识的消息发送到同一个 MessageQueue(分区)
DefaultMQProducer producer = new DefaultMQProducer("ordered_producer_group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 3 个订单
String[] orderIds = {"ORDER_001", "ORDER_002", "ORDER_003"};
// 每个订单 4 个步骤,顺序执行(创建、支付、发货、完成)
String[] orderSteps = {"CREATE", "PAY", "SHIP", "COMPLETE", "CANCEL"};
for (String orderId : orderIds) { // 订单
for (String step : orderSteps) { // 步骤
// 创建消息,指定Topic、Tag和消息体
Message msg = new Message("OrderTopic", "Order", (orderId + ":" + step).getBytes());
// 发送顺序消息(是 MessageQueueSelector,而不是 MessageListenerConcurrently )
producer.send(msg, new MessageQueueSelector() {
// 这个方法就是在选择队列(arg 是外部 send() 的第三个参数 orderId)
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
String orderId = (String) arg;
int index = Math.abs(orderId.hashCode()) % mqs.size();
return mqs.get(index);
}
}, orderId); // 使用orderId作为选择队列的参数,会传到 select() 方法里
System.out.printf("Send ordered message: %s%n", new String(msg.getBody()));
// 模拟处理间隔
Thread.sleep(500);
}
}
// 关闭生产者
producer.shutdown();
这样保证了同一个业务标识(比如订单ID)相关的消息被发往同一个队列
Broker
Broker 并不干预顺序消息的逻辑。只保证消息进入了指定队列并按顺序写入
Consumer
消费顺序消息时,需要使用 MessageListenerOrderly
监听器:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ordered_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("OrderTopic", "Order");
// MessageListenerConcurrently 并发模式(多线程消费,消费失败默认重试16次)
// MessageListenerOrderly 顺序模式(单线程的,消费失败默认重试次数为 Integer.Max_Value)
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
String message = new String(msg.getBody());
System.out.printf(new String(msg.get(0).getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.println("Ordered consumer started.");
实现原理
发送时:相同业务唯一键的消息存入相同的队列
消费时:消费者使用单线程并为队列加锁来保证不出现并发
已经是单线程了,为什么还需要锁?消费者是单线程,但是可能多个消费者(多个消费者组订阅同一个 Topic)消费同一个队列,还是会出现并发
注意事项
-
顺序的保证依赖队列的隔离,不同 key 的消息如果分布在不同的队列,就保证不了顺序
-
消费者单线程,串行消费,并给队列加锁,保证不出现并发,但牺牲了吞吐量
-
顺序消息消费失败时,会无限次(int 最大值)在原始队列重试,当前队列会被暂停消费,直至消费成功
-
因为消费失败会无限次原地重试和阻塞队列,所以需要做好重试幂等
-
全局顺序也能做到,但是没什么意义,只要保证每个业务的步骤有顺序就好了
全局顺序只能通过单队列实现,这样会显著降低吞吐量和增加风险性