分布式消息队列是分布式系统架构中的关键组件,主要用于解决应用耦合、异步消息、流量调峰等问题。随着业务逻辑的拆分和业务系统的微服务化改造,不仅需要充分保证消息队列的性能和可靠性,还需要对一些特殊业务场景的功能支持。本文简要总结了分布式消息队列中顺序消息的基本逻辑和使用过程中存在的问题。分布式消息队列的消息顺序问题在分布式架构中,为了实现其高性能、高可用和弹性伸缩,消息队列的逻辑结构大多选择多分区方式存储数据,即划分一个topic分成多个Partition。多Partition的设计大大提高了架构的并发性和可用性,但是消息队列本身只能保证每个Partition内的消息顺序,无法保证整个Topic内和多个Partition之间的消息顺序。图1发送和接收普通消息的例子在图1中,四个顺序产生的消息a1-a4在消费过程中已经完全中断。在一般的业务场景下,消费结果是可以接受的,但在某些情况下有特殊需求的场景下,无法满足业务需求。例如,在向用户发送银行卡余额变动的场景中,必须保证同一账户的余额变动通知是顺序的。对于业务端依次生成的余额变动消息a1、a2、a3、a4,必须保证用户收到消息的顺序为a1、a2、a3、a4,如图2所示。图2业务对时序消息的需求在这种有时序需求的场景下,需要业务系统侧和消息队列服务器“合力”来保证业务逻辑的实现。顺序消息的基本实现。逻辑顺序消息是指生产者严格按照先进先出(FIFO)的原则,将一批需要保证顺序的消息发送到消息队列中。依次服用。按照业务场景,顺序消息一般分为局部顺序和全局顺序,但全局顺序是局部顺序的一种特殊实现,所以本文后续的讨论都将重点放在局部顺序上。偏序:对于一个指定的topic,只需要保证一批具有相同标识的消息严格按照先进先出的原则发布和消费即可。不同标识符的消息没有顺序要求。上述在给用户发送余额变动短信的场景中,只需要保证同一个账户ID的通知消息是有顺序的,不同账户之间的短信通知的顺序不需要保证。在实现上,大多数消息队列通过在投递时为Message设置ShardingKey,将具有相同ShardingKey的Message投递到同一个Partition来保证消息的顺序存储,如图3所示。图3实现了localorder和globalorderthroughShardingKey:对于指定的Topic,所有消息的发布和消费严格按照先进先出(FIFO)的顺序进行。全局时序消息实际上是一种特殊的局部时序消息,或者该主题的所有消息都标有相同的ShardingKey实现,或者在消息队列服务器上只为该主题提供一个Partition,那么它的并发性和性能都会受到严重的影响损坏的。分区变化导致的顺序错乱在正常场景下,可以通过ShardingKey来保证消息的顺序。但是分布式队列在使用过程中,经常会出现分区失效或者分区扩缩容的情况。这时候,就很难保证消息的严格排序。比如在RocketMQ的主从架构中,主Broker失效必然会导致分区数量发生变化。这时,ShardingKey计算出来的partitionID也会发生变化,导致消息顺序紊乱。图4Partition故障导致消息顺序紊乱,如图4所示。正常情况下,a1和a2被投递到Partition2。此时Partition3发生故障,消息队列服务器上的Partition数量发生变化,同一个ShardingKey的Hash算法结果发生变化,于是a3向Partition1投递了a4和a4两条消息。这时候两个队列之间的消费顺序是无法保证的。在Kafka的架构设计中,虽然在Leader失效后Partition副本会重新选举master,失效前后partition数量没有变化,但是需要注意的是整个Partition在失效期间处于不可用状态partitionmaster选举的过程。如果有顺序Message生成也会乱序。实际场景中必须注意的两个问题综上所述,顺序消息的实现只需要Producer在Message中添加一个ShardingKey即可,但是在实际使用中,还是需要结合不同消息队列产品的特点来实现有针对性的优化,下面简单介绍一下在使用Kafka和RocketMQ的顺序消息过程中需要注意的问题。1、同步发送保证消息传递的顺序要保证发送阶段消息的顺序,需要使用同步发送的方式,在同一个Producer线程中发送消息。做好Producer端的重试控制,避免投递失败导致的序列错误。在RocketMQ中,Producer提供的send()方法默认为同步发送,应用可以根据返回的SendResult判断当前消息是否发送成功。但是在Kafka中,所有的发送本质上都是异步发送。用户编码的Producer线程调用的send()方法只是将消息暂时存储在客户端本地的RecordAccumulator中,实际上是将消息从本地发送给Broker。后台Kafka发件人线程。图5Kafka发送消息的实际逻辑因此,在Kafka中,要达到同步发送的效果,首先要获取send()方法返回的Future对象,然后调用Future对象的get()方法来阻塞并等待KafkaBroker的响应。2、多工作线程消费的问题在分布式消息队列的消费模型中,为了保证同一个Partition中消息的顺序消费,一个Partition只能被同一个消费组中的一个消费者实例消费,所以消费组Capabilities的消费和Partition的数量密切相关。为了解决这个问题,很多应用在消费的时候只是将consumer作为拉取消息的实例,在内部实现多个工作线程来提高并发性。这个时候虽然consumer实例拉取的消息是可用的。顺序,但是消息是在不同的工作线程中处理的,也会出现顺序乱序的问题。图6多个工作线程的消费导致消息顺序紊乱。为了保证消息消费的顺序,需要保证同一个ShardingKey的消息在同一个线程中处理。client在消费时采用了多worker的逻辑,可以为每个worker线程引入一个阻塞队列。消费者分发消息时,将相同ShardingKey的消息放入同一个阻塞队列中消费,工作线程不断轮询从阻塞队列中获取。可以处理消息。综上所述,在系统微服务改造的过程中,顺序消息的使用是不可避免的。用户应该清楚地了解消息队列的实现逻辑,并提前预测其在故障场景中可能产生的影响。本文总结了顺序消息的基本实现逻辑,服务端故障导致的消息顺序紊乱,以及生产者和消费者端在应用设计中需要注意的问题。应该充分认识到,顺序消息相关的业务场景的实现不能仅仅依靠消息队列本身来保证,需要业务方共同努力来实现。
