Kafka保证消息有序性

概述

在消息队列里面,有序消息是指消费者消费某个 topic 消息的顺序,和生产者生产消息的顺序一模一样,它也叫做顺序消息。前面你应该注意到了,Kafka 并不能保证不同分区之间的顺序,所以需要特殊手段来实现有序消息。

一般消息有序性指的是某个 topic 内的消息有序,而不是跨 topic 的有序消息;

  • 跨 topic 的有序消息:

    这种场景在事件驱动的架构中更加常见。在复杂的事件驱动架构下,我们可能会倾向于使用不同的 topic 来代表不同的事件,那么就会遇到要求在不同的 topics 下消息依旧需要保持有序的问题。

    • 这一类的问题是不能依赖于消息队列来解决的。要想支持这种跨 topic 的有序消息,一定要引入一个协调者,这个协调者负责把消息重组为有序消息。比如说,如果 msg2 先到了,但是 msg1 还没出来,那么这个协调者要有办法让 msg2 的消费者 B 停下来,暂时不消费 msg2。而在 msg1 来了之后,唤醒消费者 A 消费 msg1,并且在消费完 msg1 之后要再唤醒消费者 B 处理 msg2。

实现思路

消息发送单分区

要保证消息有序,最简单的做法就是让特定的 topic 只有一个分区。这样所有的消息都发到同一个分区上,那么自然就是有序的。

缺点:这种只有一个分区的方案性能差,没办法支撑高并发。对于生产端来说,所有的消息都在一个分区上,也同时意味着所有的消息都发送到了同一个 broker 上,这个服务器很可能撑不住压力;对于消费端来说,只有一个分区,那么就只能有一个消费者消费,很容易出现消息积压的问题。

根据业务多分区

第二个方案就是直接扩展为使用多个分区,只需要确保同一个业务的消息发送到同一个分区就可以保证同一个业务的消息是有序的。

计算分区:要想确保同一个业务的消息都发送到同一个分区,那么只需要发送者自己根据业务特征,直接计算出来一个目标分区。比如说最简单的策略就是根据业务 ID 对分区数量取余,余数就是目标分区。

问题:

  • 数据不均匀:这个缺点很容易理解,因为发送方要按照业务特征来选择分区,自然就容易导致一些分区有很多数据,而另外一些分区数据很少

    解决方案:

    • 第一种思路是借鉴 Redis 的槽与槽分配方案。不过 Redis 使用了 16384 个槽,一般的业务用不上那么多槽,所以可以考虑用 1024 个槽。根据业务的特征来计算一个哈希值,再除以 1024 取余就可以得到所在的槽。再根据不同槽的数据多少,合理地把槽分配到不同的分区。最好把槽和分区的绑定关系做成动态的(借助于配置中心),也就是说我可以随时调整槽到分区的映射关系,保证所有的分区负载都是均匀的。

      image-20240820213242198

      但是这个方案会存在增加分区引起消息失序问题,解决见后续介绍

    • 一致性哈希:另外一种思路是使用一致性哈希算法来筛选分区。首先要根据数据分布的整体情况,把分区分布在哈希环上,确保每一个分区上的数据分布大体上是均匀的。如果一部分哈希值上数据较多,就多插入几个分区节点。然后根据业务特征计算一个哈希值,从哈希环上找到对应的分区。

      image-20240820213448835

  • 增加分区引起消息失序:如果中间有增加新的分区,那么就可能引起消息失序。

    解决方案:

    • 新增加了分区之后,这些新分区的消费者先等一段时间,比如说三分钟,确保同一个业务在其他分区上的消息已经被消费了。

    本身增加分区也是一个很不常见的操作,再叠加消息失序的概率也很低,所以我们也可以通过监控发现这种失序场景,然后再手工修复一下就可以了。