顺序消息的场景可能用的比较少,但是还是有的 比如一个电商的下单操作
,下单后先减库存然后生成订单,这个操作就需要顺序执行的 那怎么保证顺序呢?
首先生产者需要保证入队的顺序,入队都是乱的那再厉害的MQ也招架不住啊
一般的MQ都能保证内部Queue是FIFO的(先进先出),但是只是针对一个Queue
,所以在发送消息的时候可以使用Hash取模法将同一个操作的消息发送到同一个Queue里面
,这样就能保证出队时是顺序的了。
消费者也需要注意,如果多个消费者同时消费一个队列。一样可能出现顺序错乱的情况。这就相当于是多线程消费了!这种情况需要在业务代码中间保证消息的顺序消费,所以以下的解决方案只针对单线程消费者的顺序消费
RabbitMQ
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
kafka
一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低。
kafka的分区具有天然顺序性,只需要将有序的顺序发送到一个分区即可达到消息的顺序消费。写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue
;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
RocketMQ
局部有序
:相同类别的消息之间保持有序,不同的类别之间可以无序
全局有序
:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。
单机模式下,同一个queue,存储在里面的message 是按照先进先出的原则
,所以在构造消息体的时候,可以指定一个参数,用来确保消息的类别,让相同类别的消息体发送到相同的队列,即可保证消息的顺序性,但这是局部有序,并非全局有序
好比在一个消费者集群的情况下,消费者1先去Queue拿消息,它拿到了 订单生成,它拿完后,消费者2去queue拿到的是 订单支付。拿的顺序是没毛病了,但关键是先拿到不代表先消费完它。会存在虽然你消费者1先拿到订单生成,但由于网络等原因,消费者2比你真正的先消费消息。
针对这种情况,Rocker采用的是分段锁,它不是锁整个Broker而是锁里面的单个Queue
,因为只要锁单个Queue就可以保证局部顺序消费了。
上述情况的具体执行逻辑:消费者1去Queue拿 订单生成,它就锁住了整个Queue,只有它消费完成并返回成功后,这个锁才会释放。然后下一个消费者去拿到 订单支付 同样锁住当前Queue,这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出。
ActiveMQ
- 通过高级特性consumer独有消费者(exclusive consumer)
配置如下:
queue = new ActiveMQQueue("TEST.QUEUE?consumer.exclusive=true");
consumer = session.createConsumer(queue);
- 当在接收信息的时候有一个或者多个备份接收消息者和一个独占消息者的同时接收时候,无论两者创建先后,在接收的时候,均为独占消息者接收。
- 当在接收信息的时候,有多个独占消费者的时候,只有一个独占消费者可以接收到消息。
- 当有多个备份消息者和多个独占消费者的时候,
当所有的独占消费者均close的时候
,只有一个备份消费者接到到消息。- 当主消费者挂了话,会立刻启用故障切换转移到下一台消费者继续消费
独占消息就是在有多个消费者同时消费一个queue时,可以保证只有一个消费者可以消费消息,这样虽然保证了消息的顺序问题,不过也带来了一个问题,就是这个queue的所有消息将只会在这一个主消费者上消费,其他消费者将闲置,达不到负载均衡分配
,而实际业务我们可能更多的是这样的场景,比如一个订单会发出一组顺序消息,我们只要求这一组消息是顺序消费的,而订单与订单之间又是可以并行消费的,不需要顺序,因为顺序也没有任何意义。
- 利用Activemq的高级特性:messageGroups
Message Groups特性是一种负载均衡的机制
。在一个消息被分发到consumer之前,broker首先检查消息JMSXGroupID
属性。如果存在,那么broker会检查是否有某个consumer拥有这个message group
。如果没有,那么broker会选择一个consumer,并将它关联到这个message group。此后,这个consumer会接收这个message group的所有消息,直到:Consumer被关闭,Message group被关闭,通过发送一个消息,并设置这个消息的JMSXGroupSeq为-1
配置如下:
bytesMessage.setStringProperty("JMSXGroupID", "constact-20100000002");
bytesMessage.setIntProperty("JMSXGroupSeq", -1);
如上图所示,同一个queue中,拥有相同JMSXGroupID的消息将发往同一个消费者,解决顺序问题,不同分组的消息又能被其他消费者并行消费,解决负载均衡的问题,两全其美啦!