消息的顺序消费对于业务系统来说非常重要,一笔订单产生了3条消息,分别是订单创建、订单付款、订单完成。消费时,必须按照顺序消费才有意义,与此同时多笔订单之间又是可以并行消费的。
如何保证消息顺序消费?
接下来我们通过订单的例子来展示RocketMQ如何保证消息顺序消费的:
我们最容易想到的应该是如图这样,必须M1先消费后通知S2,M2才能够被消费
问题是:M1、M2分别发送到S1、S2,这样就无法保证M1先到达MQ集群,也不能保证M1先被消费如果把多个需要顺序消费的消息都发送到同一个MQ Server呢
这样看起来生产者到消费者的顺序绝对能保证,先发送M1后发送M2;根据先到先消费的原则,M1会先于M2被消费,这样就能保证M1、M2的消息顺序性
问题是:图中可以可以看到有多个消费者,M1虽然先于M2被发送,但如果S1到消费者1的网络慢于S1到消费者2,这个时候情况就是如下图这样:
要解决这样的问题,可以采用生产者到MQ Server中的同样思路,让S1的消息都发送到同一个消费者
让MQ Server到消费者都是一比一,这样就能够保证消息的顺序消费
但也会有问题:MQ Server没有消费者1的响应时,有两种情况:
- M1确实没有到达消费者1(数据可能在网络传输中丢失)
- 消费者发回了响应消息,但MQ Server没有收到,如果是这种情况会导致M1被重复消费
源码解析
private SendResult send() {
// 获取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
// 根据我们的算法,选择一个发送队列
// 这里的arg = orderId
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
}
}
获取到路由信息后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个订单号获取到的肯定是同一个队列
// RocketMQ通过MessageQueueSelector中实现的算法来确定消息发送到哪一个队列上
// RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash
// 当然你可以根据业务实现自己的MessageQueueSelector来决定消息按照何种策略发送到消息队列中
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);
这里就是我们实现的select算法,最后类似于这样
消息顺序消费总结
通过以上的过程分析,RocketMQ实现严格的顺序消费采用的方法是:
生产者 -> MQ Server -> 消费者 是一对一的关系,保证同一个id的消息发送到同一个队列
优点:
- 简单易行,容易理解
缺点:
- 并行度会成为消息系统的瓶颈(由于都是一比一导致吞吐量不足)
- 只要消费端出现问题,会导致整个系统流程阻塞(因为消息之间都相互依赖)
为什么不去解决消息重复问题?
会造成消息重复的根本原因是:网络不可达,所有通过网络交换数据都会有这样的问题
解决方案
- 让消费端自己进行处理,对于重复的消息能够识别,保持幂等性(多次接收到同一消息处理结果是一样的)
- 利用一张日志表来记录已经处理成功的消息ID,如果这个消息ID已经在日志表中,则不再处理消息;这个地方可以由消息系统或业务实现,如果由消息系统实现会影响到性能,所以最好还是由消费端进行处理,这也是RocketMQ不处理重复消息问题的原因
总的来说就是RocketMQ为了性能考虑不保证消息不重复,需要通过业务端自己实现重复消息的识别、处理