使用消息队列的一个优点就是可以并发的产生和消费消息,提高的系统并发处理的能力,但是也会因此引发消息乱序消费的问题,什么是乱序消费,就是消息的消费顺序,和他们被加入到消息队列的先后顺序是不一样的,那么为什么会发生这样的情况呢,下面我们举一个例子。
加入现在一个producer向mq中添加了两条消息,消息a和消息b,并且是在消息a被成功发送之后,才发送消息b,保证消息a是在消息b之前被加入到消息队列的,但是这个时候,由于客户端的负载均衡策略,消息a被添加到了队列1中,消息b被添加到队里2中,这个时候,有两个consumer在采用集群消费的模式消费消息,又由于消费端的负载均衡策略,consumer1消费队列1,consumer2消费队列2,这里需要注意是,一个队列,同时只能被同一个组中的某一个consumer消费,不存在同组中多个consumer同时消费同一个队列的情况,那么这个时候,由于是两个线程,并行在消费,可能就会是consumer2先消费到队列2中的消息b,然后consumer1才消费到队列1中的消息a,跟消息加入到消息队列中的顺序是不一致的。
如果要保证全局的顺序消息,需要把对应的topic的队列数量设置成1,这样可以保证先放进去的消息,肯定是会被先取出来,同时消费端要使用单线程消费,注意这里说的是单线程消费,而不是单consumer消费,上面说过,一个队列同时只能被一个consumer消费,也就是说,当队列数量是1的时候,就算设置了多个consumer,那么也是没有意义的,因为在某一时候,肯定是只有一个consumer在工作,其他的consumer因为没有多余的队列,只能闲着,那么是不是使用了单个consumer就可以了呢,也不是,加入这个consumer在获取到消息之后,就把消息封装一个,扔到线程池中,仍然是不能保证消息是顺序执行的,这个就不解释了,大家应该自己可以理解,所以要保证全局的顺序消息,队列数量设置成1,单线程消费,两者缺一不可。但是如果真的这样做,那么可想而知,效率是非常低的。
一般情况下,我们并不需要保证全局的消息顺序性,只要保证局部的消息顺序性就可以了,比如某个订单的生成,付款,发货,这三步是必须保证顺序的,但是不同的订单之间,就算发生了乱序,也是没有关系的,并不会产生影响,只要每个订单的付款,发生在该订单的生成之后,该订单的发货,发生在该订单的付款之后就可以了,这个时候,就可以通过使用rocketmq提供的顺序生产和顺序消费的能力来实现,首先我们看一下producer的代码
int orderId = 1;
for(int i = 0; i < 100; ++i){
Message message = new Message("topic", "tag", ("订单消息体, 订单的id是:" + orderId++).getBytes());
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
int id = (int) o;
int queueNum = list.size();
//根据订单id,模上消息队列的数量,这样就能保证同一个订单的所有消息,都发送到同一个消息队列中,同时也能将消息均匀的分散到各个队列中
return list.get(id % queueNum);
}
}, orderId);
}
使用自定义的MessageQueueSelector,根据某一特征(例如orderId)来决定将消息发送到哪一个队列。下面再来看看consumer的代码
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
for(MessageExt messageExt : list){
//业务处理逻辑
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer使用的监听着是MessageListenerOrderly类,与这个类对应的是MessageListenerConcurrently,一个是顺序消费,一个是并发消费,使用顺序消费的时候,每个consumer在拉取对应messageQueue的消息的时候,都要实现获取对应的锁,保证了同一个consumerQueue的消息不能被并发消费,但是不同的consumerQueue的消息可以并发消费,保证了效率。
这就是使用rocketmq进行顺序消费的正确方式