消息传输和消费的时序性,是消息队列应用中一个非常重要的问题
很多业务场景都需要考虑消息投递的时序性
例如,电商中的订单状态流转,数据库的binlog 分发
消息顺序消费有哪些困难
消息队列中的队列是一个有序的数据结构,消息传递是顺序的
在实际开发中,特别是分布式场景下,消息的有序性是很难保证的
分布式的时钟问题
消息的生产者,消费者和队列存储,可能分布在不同的机器上,不同的机器使用各自的本地时钟
由于服务器存在时钟偏斜等问题,本地时间会出现不一致
不能用消息发送和到达的时间戳作为时序判断标准
分布式系统下缺乏全局时钟,使得绝对的时间顺序实现起来更加困难
消息发送端和消费端的集群
生产者和消费者都是集群部署,通过 ProducerGroup 和 ConsumerGroup 的方式来运行
消息发送端发送时的时序不能用来作为消息发送的有序判断
消费端可能存在多个实例,即使队列内部是有序的
由于存在消息的分发过程,不同消费实例的顺序难以全局统一,也无法实现绝对的有序消费
消息重传等的影响
消息队列在传输消息时,可能会出现网络抖动导致的消息发送失败等
对这种场景的兼容,一般是通过进行合理地重传
消息的重传发生在什么时候是不可预知的,这也会导致消息传输出现乱序
网络及内部并发
如果单纯地依靠消息队列本身来保证,那么在跨实例的情况下
因为网络传输的不稳定会有先后顺序,以及内部消费的并发等,仍然无法实现绝对有序
保证消息绝对的有序,实现起来非常困难
除非在服务器内部,并且一个生产者对应一个消费者
解决消息队列的有序性有哪些手段呢?
消息传输的有序性和不同的消息队列,不同业务场景,以及技术方案的细节等都要关系
解决消息传输的有序性,需要依赖消息队列提供对应的方式
从消息队列自身的角度,可以分为全局有序和局部有序
- 全局有序 —— 无法使用多分区进行性能的优化
- 局部有序 —— 把业务消息分发到一个固定的分区,也就是单个队列内传输的方式
Kafka 顺序消息
Kafka 保证消息的 Partition 内的顺序
- 单分区 —— 天然满足消息有序性
- 多分区 —— 通过制定的分发策略,将同一类信息分发到同一个 Partintion 中
例如,电商系统中的订单流转信息,在写入 Kafka 时通过订单 ID 进行分发
保证同一个订单 ID 的消息都会被发送到同一个 Partition 中
比较特殊的情况——消息失败重发的场景
比如同一个订单下的消息1和2,如果1发送失败了,重发的时候可能会出现在2的后边,可以通过设置”max.in.flight.requests.per.connection“ 参数来解决
RocketMQ 顺序消息
RocketMQ 保证消息在同一个 Queue 中的顺序性,也就是满足队列的先进先出原则
如果把对应一个业务主键的消息都路由到同一个 Queue 中就可以实现消息的有序传输
并且 RocketMQ 额外支持 Tag 的方式
可以对业务消息做进一步的拆分,在消费时相对更加灵活
从业务角度保证顺序消费
消息消费的有序性,是一个业务场景的设计问题。可以在业务中进行规避,或者通过合理的设计方案来解决。
消息传输的有序性是否有必要
在你的业务中是否必须实现绝对的消息有序?或者是必须要有消息队列这样的技术手段?
比如在一个订单状态消息流转的业务场景中,
订单会有创建成功,待付款,已支付,已发货的状态,订单状态的更新需要保证有序性。
如果要实现的功能是根据发货的状态,进行物流通知用户的功能
因为这个状态是单调不可逆向的,可以只关注最后是否已经发货的状态。
业务中如何实现有序消费
根据不同的业务场景,以发送端或者消费端时间戳为准
比如在电商大促的秒杀场景中,如果要对秒杀的请求进行排队,可以使用秒杀提交时服务端的时间戳,在这个场景下,不需要保证绝对的有序。每次消息发送是生成唯一递增的ID
在消费端进行消费时,缓存最大的序列ID,只消费超过当前最大的序列 ID 的消息
可以保证每次只处理最新的数据,避免一些业务上的不一致问题。通过缓存时间戳的方式
当生产者在发送消息时,添加一个时间戳,消费端在处理消息时,通过缓存时间戳的方式,判断消息产生的时间是否最新,如果不是则丢弃,否则执行下一步。
总结
消息的有序性可以分为时间上的有序和业务上的有序
绝对的时间有序实现起来时非常困难的
消息队列只是一个消息传输的解决方案
可以通过业务中不通的场景,进行合理的设计,实现业务上的有序性