消息对象-消息顺序

在消息队列的语境下,我们所说的“顺序”,是指消费者处理消息的顺序,与生产者发送消息的顺序完全一致。

这里有一个非常关键的细节需要理清:所谓的“发送顺序”,并非指在生产者客户端代码执行send()方法的先后,而是指消息抵达Broker(消息队列服务器)的先后顺序。在一个典型的分布式环境下,假设生产者A和生产者B,A在10:00:00.000时执行了发送动作,但由于网络抖动、GC停顿等原因,其消息msg1在10:00:00.500时才到达Broker;而生产者B在10:00:00.100时发送的msg2,却在10:00:00.300时就抵达了Broker。那么从Broker的视角看,msg2就是先于msg1的。

因此,我们讨论的顺序性,其裁判是Broker,而非生产者客户端。至于如何协调多个生产者节点严格按时序向Broker发送消息,这已经超出了消息队列的范畴,属于分布式锁或分布式事务协调的领域了。

由Kafka引发的线上问题。当时我们有一个核心的订单处理业务,为了保证订单状态流转的正确性,最初的设计者采用了最稳妥的方案:为订单Topic只设置了一个分区。在业务初期,这个方案运行得非常稳定。

但随着公司业务的快速增长,问题开始暴露。我们发现这个Topic的消息积压越来越严重,消费延迟从几秒钟增长到十几分钟,直接影响了下游履约、发货等环节的效率。同时,监控也显示,承载这个唯一分区的那个Broker节点,其CPU和磁盘IO负载远高于集群中的其他节点,时常出现性能抖动,成为了整个系统的性能瓶颈。

首先深入分析了业务场景。我发现,虽然我们需要保证同一个订单的‘创建’、‘支付’、‘发货’等消息的顺序,但不同订单之间的消息处理其实是完全独立的,并不存在顺序依赖。换句话说,我们的真实需求是业务内的局部有序,而非全局有序。

为Topic增加了7个分区,总数达到8个,并相应地将消费者应用的实例数也扩展到8个,实现了真正的并行消费。

在生产者端,我们修改了发送逻辑,引入了基于orderId的分区策略。所有消息在发送时,都会根据其orderId的哈希值对8取模,来决定其目标分区。这样就确保了同一订单的所有消息始终会落入同一个分区。

当然,这个过程中还有一个非常关键的细节需要处理,那就是从单分区切换到多分区(或者说,分区数量发生变化)时,如何避免消息乱序。

当时确实遇到了消息乱序的问题。如果在切换过程中,一个旧订单的‘创建’消息还在原来的那个分区里积压着,而它后续的‘支付’消息,因为新的路由规则被发送到了一个空闲的新分区并被立刻消费,就会造成业务逻辑错误。

为了解决这个问题,我采用了一个简单而有效的策略:在部署完新的生产者和消费者代码后,我让新的消费者应用启动后,先暂停消费5分钟。这个‘静默期’的目的,就是为了给旧的、积压在唯一分区里的消息足够的时间被消费完毕。5分钟后,整个系统中的存量消息基本处理完成,此时再放开所有消费者进行消费,就能平滑地过渡到新的多分区模式,从而避免了潜在的乱序风险。