顺序消息

顺序消息分类

全局顺序

  • 所有消息严格按照发送顺序被消费。
  • 通常只能使用一个队列(单队列),吞吐量受限。

分区顺序

  • 按照某个业务 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)消费同一个队列,还是会出现并发

注意事项

  1. 顺序的保证依赖队列的隔离,不同 key 的消息如果分布在不同的队列,就保证不了顺序

  2. 消费者单线程,串行消费,并给队列加锁,保证不出现并发,但牺牲了吞吐量

  3. 顺序消息消费失败时,会无限次(int 最大值)在原始队列重试,当前队列会被暂停消费,直至消费成功

  4. 因为消费失败会无限次原地重试和阻塞队列,所以需要做好重试幂等

  5. 全局顺序也能做到,但是没什么意义,只要保证每个业务的步骤有顺序就好了

    全局顺序只能通过单队列实现,这样会显著降低吞吐量和增加风险性

posted @ 2025-07-03 13:46  CyrusHuang  阅读(21)  评论(0)    收藏  举报