针对消息有序的业务需求, 分为全局有序和局部有序。
-
全局有序: 一个 Topic 下的所有消息都需要按照生产顺序消费。
-
局部有序: 一个Topic下的消息, 只需要满足同一业务字段的要按照生产顺序消费。例如:Topic 消息是订单的流水表, 包含订单orderId, 业务要求同一个 orderId 的消息需要按照生产顺序进行消费。
-
要想实现消息有序, 需要从 Producer 和 Consumer 两方面来考虑。
Kafka中的消息顺序保证原理
在Apache Kafka中, 消息顺序性的保障主要依托于其独特的分区(Partition)机制以及消息键(Key)的使用。
- 分区(Partition)的作用与消息顺序性的内在关联
- Kafka的主题(Topic)可以被划分为多个分区, 每个分区都是一个独立的顺序日志存储。如下图所示, 每个分区内部的消息按照其生成的先后顺序排列, 形成一个有序链表结构。
- 当生产者向主题发送消息时, 可以选择指定消息的键(Key)。若未指定或Key为空, 消息将在各个分区间平均分布; 若指定了Key, Kafka会根据Key和分区数计算出一个哈希值, 确保具有相同Key的消息会被发送到同一个分区, 从而确保这些消息在分区内部是有序的。
全局有序
-
由于 Kafka 的一个 Topic 可以分为多个 Partition, Producer 发送的消息是分散在不同 Partition的。当 Producer 按顺序发消息给 Broker, 但进入Kafka之后, 这些消息就不一定进到哪个Partition, 会导致顺序是乱的。
-
因此要满足全局有序, 需要 1 个 Topic 只能对应 1 个 Partition。
-
而且对应的 consumer 也要使用单线程或者保证消费顺序的线程模型, 否则会出现下图所示, 消费端造成的消费乱序。
局部有序
-
要满足局部有序, 只需要在发消息的时候指定 Partition Key, Kafka 对其进行 Hash 计算, 根据计算结果决定放入哪个 Partition。这样 Partition Key 相同的消息会放在同一个 Partition。此时, Partition 的数量仍然可以设置多个, 提升Topic的整体吞吐量。
-
如下图所示, 在不增加 partition 数量的情况下想提高消费速度, 可以考虑再次 hash 唯一标识(例如订单orderId) 到不同的线程上, 多个消费者线程并发处理消息(依旧可以保证局部有序)。
消息重试对顺序消息的影响
-
对于有着先后顺序的消息A、B, 正常情况下应该是 A 先发送完成后再发送 B, 但是在异常情况下, 在 A 发送失败的情况下, B发送成功, 而 A 由于重试机制在 B 发送完成之后重试发送成功了。这时对于本身顺序为 AB 的消息顺序变成了 BA。
-
针对这种问题, 严格的顺序消费还需要
max.in.flight.requests.per.connection参数的支持。 -
该参数指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高, 就会占用越多的内存, 同时也会提升吞吐量。把它设为1就可以保证消息是按照发送的顺序写入服务器的。
-
此外, 对于某些业务场景, 设置
max.in.flight.requests.per.connection=1会严重降低吞吐量, 如果放弃使用这种同步重试机制, 则可以考虑在消费端增加失败标记的记录, 然后用定时任务轮询去重试这些失败的消息并做好监控报警。