使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是:
- 用户等级升级。
- 根据用户等级下的订单价格
假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。每次添加消息到 Partition(分区) 的时候都会采用尾加法。
Kafka 只能为我们保证 Partition(分区) 中的消息有序。消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。
为什么同一个partition的消息是有序的?
- 因为当生产者向某个partition发送消息时,消息会被追加到该ppartition的日志文件(log)中,并且被分配一个唯一的offset,文件的读写是有顺序的。而消费者在从该分区消费消息时,会从该分区的最早offset开始逐个读取 消息,保证了消息的顺序性。
如何解决
Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。所以,我们就有一种很简单的保证消息消费顺序的方法: 1 个 Topic 只对应一个 Partition。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。
Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。
总结一下,对于如何保证 Kafka 中消息消费的顺序,有了下面两种方法:
- 1 个 Topic 只对应一个 Partition。
- (推荐)发送消息的时候指定 key/Partition。
如何发到同一个partition?
当我们发送消息的时候,如果key为null,那么Kafka默认采用Roiund-robin策略,也就是轮转,实现类是DefaultPartitioner。那么如果想要指定他发送到某个partition的话,有以下三个方式:
- 指定partitionID:我们可以在发送消息的时候,可以直接在ProducerRecord中指定partition
- 指定key:在没有指定Partition(null值)时,如果有 Key,Kafka将依据Key做hash来计算出一个Partition编号来。如果key相同,那么也能分到同一个partition中,但这个方案如果后续要增加删除partition就会出现短暂的乱序。
- 自定义Partitioner:可以实现自己的分区器(Partitioner)来指定消息发送到特定的分区。我们需要创建一个类实现Partitioner接口,并且重写partition()方法。