顺序消息和幂等:如何实现顺序消息和数据幂等?

132 阅读5分钟

顺序消息的定义和实现

在消息队列中,消息的顺序性一般指的是时间的顺序性,排序的依据就是时间的先后。从功能来看,即生产端发送出来的消息的顺序和消费端接收到消息的顺序是一样的。

基于顺序存储结构的设计

基于顺序存储结构的设计,是指保持消息队列底层顺序存储结构不变的前提下,实现顺序消息的技术方案。

在当前的顺序存储结构中,消息队列实现顺序消息的前提是: 一个生产者同步发送消息到一个分区才能保证消息的有序。

单一生产者 + 同步发送是为了解决生产端数据发送的有序性,只有前面一条数据写入成功后,后面的数据才能继续写入。

只要保证局部有序的数据写入同一个分区即可,即根据某个标识将需要有序的数据发送到同一个分区中。

Kafka 和 Pulsar 是通过生产端按 Key Hash 的方案将数据写入到同一个分区。RocketMQ 是通过消息组(功能上类似消息 Key)将同一个消息组的数据写入到不同的 MessageQueue。RabbitMQ 是通过 Exchange 和 Route Key 的机制,将数据写入到不同的 Queue 里面。

幂等机制的定义和实现

生产幂等通常指同一条消息不会被重复写入到 Broker。即同一条消息客户端无论重复发送多少次,服务端也只会保存一份这条消息。

消息的唯一性应该是以 Producer 的 send 调用为准。即 Producer send 一次就表示发送了一条消息,send 两次表示发送两条消息。所以就需要在客户端调用 send 的时候标识消息的唯一性,以标识消息的唯一。

从技术上来看,主要有两种方案:

  1. 通过消息唯一 ID 实现幂等。通过消息唯一 ID 实现幂等是指在发送消息的时候,为每条消息分配唯一的消息 ID(MsgID),来表示消息的唯一性。基于这种方案的前提是,需要在生产端开启按 Key Hash 的机制,以保证同一个 MsgID 的消息可以发送到同一个分区中。

  2. 通过生产者 ID 和自增序号实现幂等。为每个生产者赋予唯一的 ID,生产者 ID 是全局唯一的。然后生产者启动时生成一个从 0 开始的自增序号,用来表示这个生产者发送出去的消息,每条消息分别有一个自增序号,比如 0、1、2……即通过 Producer 和 seqnum 二元组来唯一标识消息。Broker 会根据这个二元组判断是否收到过这条消息,是就保存,否就拒绝。和第一种区别:我们不保留所有的 seqnum,只保留最新收到的 seqnum。此时如果收到的消息的 seqnum 是下一条 msg,那么就正常保存数据。

目前业界主流的消息队列只有 Kafka 支持幂等,其他三款消息队列 RocketMQ、RabbitMQ、Pulsar 都不支持。

Kafka 的幂等机制的实现方案

kafka 的生产者在启动时会为每个生产者分配一个唯一 ID。这个唯一 ID 是客户端从 Broker 申请的,不是自己生成的,即PID。

从实现上看,Broker 通过在 ZooKeper 创建一个节点来生成自增 ID,然后返回给客户端,从而保证生产者的 ID 是这个集群唯一的,属于基于第三方系统来生成分布式唯一 ID。

Kafka 支持 Batch 语义,所以在发送消息的时候会为每批次消息赋予一个 seqnum,用来标识这个生产者发送的消息的唯一性。通过 Topic、PartitionNum、PID、seqnum 四元组来唯一标识一条消息。我们稍后再来说明为何这样实现。

Kafka 是怎样判断消息是否重复的? 从具体实现上看,Broker 端会缓存 PID 对应 Topic-Partition 的五个最近的 batch 信息。比如曾经接收过 1、2、3、4、5、6 六个消息,此时只会缓存 2~6 五个消息 ID。如下图所示,Broker 接收到数据后,会循环缓存中的数据,判断是否重复,重复就拒绝,不重复就直接保存。

此时有一个问题是,如果消息 1 过期后,客户端再把消息 1 发送过来,此时判断结果就是不重复,从而写入数据,那么数据就没办法达到真正意义上的幂等了。所以从具体实现上来看,Kafka 的幂等是无法实现完全幂等的,只能支持部分的幂等。

Kafka 的实现方案可以说是一个取舍的方案。因为 Kafka 主打的是高性能,不能因为幂等的特性导致性能下降太多。通过缓存少量的数据来实现大部分情况下的幂等,也不会对内存和性能造成太大影响,只是付出的代价是不能支持完全的幂等。

5 是 hard code 在代码里面的,不能改动。


此文章为11月Day28学习笔记,内容来源于极客时间《深入拆解消息队列 47 讲》