Kafka的顺序保证、序列化及分区

顺序保证
Kafka可以保证同一个分区里的消息是有序的。即如果生产者按照一定的顺序发送消息,那么Broker就会按照这个顺序把它们写入分区,消费者也会按照同样的顺序去读取它们。

 

 


但是在Kafka基本概念及常用场景中,我们介绍了一个主题是可以被分为若干个分区的。

 

那么我们将一个主题设置为只有一个分区的情况下,那么就可以保证其消息的顺序么?肯定也是不可以的,因为在Kafka的生产者中,我们介绍了Kafka生产者的一些配置,其中会有两个参数配置也会影响消息的顺序,如下:

 

 

 

 

 


如果我们将上述参数retries设置为大于0的数,即允许消息失败进行重发,那么如果Kafka生产者在安装顺序发送了A、B两条消息,因为max.in.flight.requests.per.connection参数默认为5,所以生产者是允许在消息A没有收到响应的情况下,直接发送消息B的,然后假设消息A发送失败、消息B发送成功,然后消息A会发送重试,重发消息后成功,那么分区中的消息顺序就为消息B、A了,和发送时的顺序不一致。


所以如果某些场景要求消息是有序的,那么消息是否写入成功也是很关键的,所以不建议把retires设为0 。可以把max.in.flight.request.per.connection设为1,这样在生产者尝试发送第一批消息时,就不会有其他的消息发送给Broker 。不过这样会严重影响生产者的吞吐量,所以只有在对消息的顺序有严格要求的情况下才会这么做。

 

序列化
创建生产者对象必须指定序列化器,如果默认的序列化器并不能满足我们所有的场景。那么我们完全可以自定义序列化器。只要实现org.apache.kafka.common.serialization.Serializer接口,以及org.apache.kafka.common.serialization.Deserializer即可。

public class MySerializer implements Serializer<Object> {
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        //do nothing
    }

    @Override
    public byte[] serialize(String topic, Object data) {
        byte[] bytes = null;
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(data);
            oos.flush();
            bytes = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bytes;
    }

    @Override
    public void close() {
        //do nothing
    }
}

 

public class MyDeserializer implements Deserializer<Object> {
    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        //do nothing
    }

    @Override
    public Object deserialize(String topic, byte[] data) {
        Object object = null;
        try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bis)) {
            object = ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return object;
    }

    @Override
    public void close() {
        //do nothing
    }
}

上述是我们使用了Jdk原生的序列化机制完成的,当然我们也可以使用一些优秀的第三方序列化框架,如我们在
Netty中介绍的ProtoBuf、MessagePack、Kryo,以及Kafka官方推荐的Apache Avro。


然后我们就Kafka的生产者和消费者中,使用上述我们自定义的序列化类和反序列化类即可,如下:

 

 

 

 

这里我们就来简单了解一下Kafka官方推荐的Avro,其实Avro和ProtoBuf有一点点类似,都会生成了额外的文件,Avro会使用一个JSON文件作为schema来描述数据,Avro在读写时会用到这个schema,可以把这个schema内嵌在数据文件中。这样,不管数据格式如何变动,消费者都知道如何处理数据。


但是内嵌的消息,自带格式,会导致消息的大小不必要的增大,消耗了资源。我们可以使用schema注册表机制,将所有写入的数据用到的schema保存在注册表中,然后在消息中引用schema的标识符,而读取的数据的消费者程序使用这个标识符从注册表中拉取schema来反序列化记录。

 

 

注意:Kafka本身并不提供schema注册表,需要借助第三方,现在已经有很多的开源实现,比如Confluent Schema Registry,可以从GitHub上获取。

至于如果使用其Avro,可以参考Kafka中使用Avro序列化

 

分区
我们在新增ProducerRecord对象中可以看到,ProducerRecord包含了目标主题,键和值,Kafka的消息都是一个个的键值对。(当然我们在ProducerRecord对象中也可以直接指定所要发送的分区)


键的主要用途有两个:

用来决定消息被写往主题的哪个分区,拥有相同键的消息将被写往同一个分区
还可以作为消息的附加消息

另外键可以设置为默认的null。如果键值为null,并且使用默认的分区器,分区器使用轮询算法将消息均衡地分布到各个分区上。


如果键不为空,并且使用默认的分区器,Kafka对键进行散列(Kafka内部实现自定义的散列算法),然后根据散列值把消息映射到特定的分区上。很明显,同一个键总是被映射到同一个分区。但是只有不改变主题分区数量的情况下,键和分区之间的映射才能保持不变,一旦增加了新的分区,就无法保证了,所以如果要使用键来映射分区,那就要在创建主题的时候把分区规划好,而且永远不要增加新分区。

 


但是某些情况下,数据特性决定了需要进行特殊分区,比如公司业务的特殊性,上海的业务量明显占主要部分,比如占据了总业务量的20%,我们需要对上海的订单进行单独分区处理,默认的散列分区算法不合适了, 我们就可以自定义分区算法,对上海的订单进行单独处理,其他地区沿用散列分区算法。或者某些情况下,我们用value来进行分区。


如果需要满足上述的要求,那么我们就需要使用到自定义分区器,如下我们就自定义一个简单的一value值的hashCode来进行分区,主要就是实现org.apache.kafka.clients.producer.Partitioner接口即可,如下:

public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
        int num = partitionInfos.size();
        //Integer num = cluster.partitionCountForTopic(topic);
        return value.hashCode() % num;
    }

    @Override
    public void close() {
        //do nothing
    }

    @Override
    public void configure(Map<String, ?> configs) {
        //do nothing
    }
}

然后在其Kafka的生产者和消费者中,添加相应的配置即可,和自定义序列化类类似

properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class);

 

posted @ 2021-05-18 19:55  姚春辉  阅读(1248)  评论(0编辑  收藏  举报