一、什么是顺序消息
顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。
默认情况下生产者会把消息以 Round Robin 轮询方式发送到不同的 Queue 分区队列;而消费消息时会从多个 Queue 上拉取消息,这种情况下的发送和消费是不能保证顺序的。如果将消息仅发送到同一个 Queue 中,消费时也只从这个 Queue 上拉取消息,就严格保证了消息的顺序性。
二、 为什么需要顺序消息
例如,现在有 TOPIC ORDER_STATUS
(订单状态),其下有 4 个 Queue 队列,该 Topic 中的不同消息用于描述当前订单的不同状态。假设订单有状态:未支付、已支付、发货中、发货成功、发货失败。
根据以上订单状态,生产者从时序上可以生成如下几个消息:
订单 T0000001:未支付 --> 订单 T0000001:已支付 --> 订单 T0000001:发货中 --> 订单 T0000001:发货失败
消息发送到 MQ 中之后,Queue 的选择如果采用轮询策略,消息在 MQ 的存储可能如下:
这种情况下,我们希望 Consumer 消费消息的顺序和我们发送是一致的,然而上述 MQ 的投递和消费方式,我们无法保证顺序是正确的。对于顺序异常的消息,Consumer 即使设置有一定的状态容错,也不能完全处理好这么多种随机出现组合情况。
基于上述的情况,可以设计如下方案:对于相同订单号的消息,通过一定的策略,将其放置在一个 Queue 中,然后消费者再采用一定的策略(例如,一个线程独立处理一个 queue,保证处理消息的顺序性),能够保证消费的顺序性。
三、有序性分类
根据有序范围的不同,RocketMQ 可以严格地保证两种消息的有序性:分区有序与全局有序。
1、全局有序
当发送和消费参与的 Queue 只有一个时所保证的有序是整个 Topic 中消息的顺序, 称为全局有序。
在创建 Topic 时指定 Queue 的数量。有三种指定方式:
1)在代码中创建 Producer 时,可以指定其自动创建的 Topic 的 Queue 数量
2)在 RocketMQ 可视化控制台中手动创建 Topic 时指定 Queue 数量
3)使用 mqadmin 命令手动创建 Topic 时指定 Queue 数量
2、分区有序
如果有多个 Queue 参与,其仅可保证在该 Queue 分区队列上的消息顺序,则称为分区有序。
如何实现 Queue 的选择?在定义 Producer 时我们可以指定消息队列选择器,而这个选择器是我们 自己实现了 MessageQueueSelector 接口定义的。
在定义选择器的选择算法时,一般需要使用选择 key。这个选择 key 可以是消息 key 也可以是其它数据。但无论谁做选择 key,都不能重复,都是唯一的。
一般性的选择算法是,让选择 key(或其 hash 值)与该 Topic 所包含的 Queue 的数量取模,其结果 即为选择出的 Queue 的 QueueId。
取模算法存在一个问题:不同选择 key 与 Queue 数量取模结果可能会是相同的,即不同选择 key 的 消息可能会出现在相同的 Queue,即同一个 Consuemr 可能会消费到不同选择 key 的消息。这个问 题如何解决?一般性的作法是,从消息中获取到选择 key,对其进行判断。若是当前 Consumer 需 要消费的消息,则直接消费,否则,什么也不做。这种做法要求选择 key 要能够随着消息一起被 Consumer 获取到。此时使用消息 key 作为选择 key 是比较好的做法。
以上做法会不会出现如下新的问题呢?不属于那个 Consumer 的消息被拉取走了,那么应该消费 该消息的 Consumer 是否还能再消费该消息呢?同一个 Queue 中的消息不可能被同一个 Group 中的 不同 Consumer 同时消费。所以,消费现一个 Queue 的不同选择 key 的消息的 Consumer 一定属于不同的 Group。而不同的 Group 中的 Consumer 间的消费是相互隔离的,互不影响的。
四、代码举例
public class OrderedProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 100; i++) {
Integer orderId = i;
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicA", "TagA", body);
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}