顺序收发消息实战
背景:
假如我们有三个任务,任务1ABC,任务2DQ,任务3NQR,ABC这些字母都代表一个业务消息都要按照自己的内部的顺序消费。
1 全局有序
这个时候我们可以往一个队列里面写入数据,也只选择一个消费者进行消费,那么这个时候肯定是由序的。哪怕我们有ABDC 中间穿插了D但是最终还是一致的。可以解决上面我们这个场景,但是这样的解决方案不是最好的,降低了消费效率和吞吐量。
2 局部有序
通过上面的思考我知道其实保证每个任务内部有序就行了,那么如果我们每个任务的消息都路由到同一个队列岂不是就可以了吗?如下面这张图,那么是不是大大的提高了消费的能力呢?
3 代码
启动类,这里开启了两个通道一个通道验证全局有序性,一个通道验证局部有序性。
@SpringBootApplication
@EnableBinding({CustomSink.class})
public class RocketOrderlyApplication {
public static void main(String[] args) {
SpringApplication.run(RocketOrderlyApplication.class, args);
}
@StreamListener("input")
public void receiveInput(String receiveMsg) {
System.out.println("input receive: " + receiveMsg);
}
@StreamListener("input2")
public void receiveInputSecond(String receiveMsg) {
System.out.println("input2 receive: " + receiveMsg);
}
}
配置
server:
port: 9520
spring:
application:
name: rocket-orderly
cloud:
stream:
bindings:
input:
content-type: application/json
destination: GLOBAL_ORDER_TOPIC
group: test-group-order
input2:
content-type: application/json
destination: PART_ORDER_TOPIC
group: test-order
rocketmq:
bindings:
input:
consumer:
orderly : true #配置是否有序
binder:
name-server: ip:9876
group: rocket-demo
下面来看全局有序性的生产者
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr("ip:9876");
producer.start();
List<Order> F = OrderBuilder.build(1, "A", "B", "C");
List<Order> S = OrderBuilder.build(2, "D", "Q");
List<Order> T = OrderBuilder.build(3, "N", "Q", "R");
ArrayList<Order> orders = new ArrayList<Order>() {{
addAll(F);
addAll(S);
addAll(T);
}};
for (Order order : orders) {
Message msg = new Message("GLOBAL_ORDER_TOPIC", "GLOBAL_ORDER_TOPIC_STR", order.toString().getBytes());
msg.setKeys("GLOBAL_ORDER_TOPIC_TRACE");
producer.send(msg);
}
}
下面来看局部有序性的生产者
public class Producer2 {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr("ip:9876");
producer.start();
List<Order> F = OrderBuilder.build(1, "A", "B", "C");
List<Order> S = OrderBuilder.build(2, "D", "Q");
List<Order> T = OrderBuilder.build(3, "N", "Q", "R");
ArrayList<Order> orders = new ArrayList<Order>() {{
addAll(F);
addAll(S);
addAll(T);
}};
for (Order order : orders) {
Message msg = new Message("PART_ORDER_TOPIC", "PART_ORDER_TOPIC_STR", order.toString().getBytes());
msg.setKeys("PART_ORDER_TOPIC_TRACE");
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 重要的逻辑在这里,这里通过取余的方式将同一个任务的ID路由到同一个队列
int size = mqs.size();
int idx = (int) arg;
return mqs.get(idx % size);
}
}, order.getOrderID());
}
}
}
我们启动了四个消费者去消费消息得到如下结果,课件保证了局部有序性:
保证消息的有序
参考资料
有序的发送
本质:将一堆的消息,按照一定的顺序进行发送
发送到哪里去:不管是单队列,还是多队列,我们要将有序的消息放在一起,而且这些所谓的“一起”概念的消息,将来也需要被同一个消费者消费
解决方案:
使用单 Topic 单队列的形式,可以控制一个全局力度的有效性
使用单 Topic 多队列的形式,这个时候需要生产者按照一定的规律,将一些需要有顺序性概念的消息发送到同一个队列中,实际使用中我们就保证MessageQueueSelector 将需要有序的消息路由到同一个消息中去。
public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
msg.setTopic(withNamespace(msg.getTopic()));
return this.defaultMQProducerImpl.send(msg, selector, arg);
}
回归最本质的解决方案:就是想办法将一堆有序的消息,发送至一个队列中,至于这个队列是 Topic 的单队列(全局队列),还是 Topic 的 N 个队列中的其中一个队列,这都问题不大,主要的因素就是将消息放到了一个队列中而已。
有序的存储
需要将消息进行有序存储的话,其实就是 CommitLog + MessageQueue + IndexFile 他们协同起来做的事情,其实顺序的核心要素就是要保证 MessageQueue 里面的消息是有序存储就好了。
有序的消费
并发性消费:MessageListenerConcurrently 其实对有序的诉求不太严格,不建议使用。但是如果对于线程池玩的很溜的开发人员来说,可以去研究底层的线程池最大最小数量以及队列的参数,然后想办法让线程池变成一个单队列的形式。
顺序性消费:MessageListenerOrderly 才是我们需要的正确进行有序消费的 API 接口
思考1:能否再多个队列中保持有序呢?
答:可以考虑在设计消息体的时候,考虑设计一个全局时间戳的字段,到时候将这一批消息全部存储到 db 中,然后从 db 中捞取数据并且按照【全局时间戳】字段生序排列,也就是从小到大排列,然后先消费【全局时间戳】小到记录,再消费【全局时间戳】大的记录。