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);