producer分区器

315 阅读2分钟

消息经过send()方法发送往broker的过程中,需要依次经过Interceptor、Serializer、和Partitioner,其中Interceptor和Partitioner都不是必须的
消息的序列化和反序列化笔者打算写在同一个地方,此处只介绍分区器

使用默认的分区器DefaultPartitioner

查看DefaultPartitioner和KafkaProducer源码
建议直接打断点调试,调试到第404行就可以了

  • KafkaProducer的dosend()方法
  • DefaultPartitioner的partition()方法 通过调试发现,传入的topic、header、key将被序列化得到一个byte数组serializedKey,分区器会对该数组进行哈希得到分区号
    同理传入的topic、header、value将被序列化得到一个byte数组serializedValue,后续会用到
    其中的topic、header从生产者的配置文件获取,而key则由ProducerRecord(被发送的消息)的构造函数传入。下图是ProducerRecord的构造函数
    由此我们得到结果:
  • 如果不指定消息的key,被序列化得到一个byte数组将为空(跟下序列化的源码)
  • 注意,record还可以传入一个partition参数,不经过分区器直接指定分区 (不为null直接返回,当然指定分区如果是不可用的将会抛出异常) 接下来可以查看DefaultPartitioner源码(个人注释版)了,只有两个方法(close是一个空方法)
//参数分析:   keyBytes和valueBytes就是上述说的serializedKey和serializedValue
 public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //获取该主题的所有分区,并记下分区数目
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        //如果没有传入key(key空,keyBytes也是空的)
        if (keyBytes == null) {
            //因为key是空的,先看下面就知道nextValue是啥
            int nextValue = this.nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                //注意这里取余的是可用的分区号总数
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return ((PartitionInfo)availablePartitions.get(part)).partition();
            } else {
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            //如果key不空,根据serializedValue进行哈希运算,然后取余得到分区号
            //注意这里取余的是所有的分区号总数
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

private int nextValue(String topic) {
    //topicCounterMap是一个ConcurrentMap,key是topic,value是一个AtomicInteger类型的整数,用于标识当前消息是topic传入的第几个无key的消息
    AtomicInteger counter = (AtomicInteger)this.topicCounterMap.get(topic);
    if (null == counter) {
        counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
        AtomicInteger currentCounter = (AtomicInteger)this.topicCounterMap.putIfAbsent(topic, counter);
        if (currentCounter != null) {
            counter = currentCounter;
        }
    }
 
    return counter.getAndIncrement();
}

结论

  • record指定了分区号,直接发到对应分区。
  • 无分区号,指定了key,按上述方法哈希取余
  • 分区号和key都为空,编号+1,对可用的分区总数取余

调试代码


import java.util.Properties;
 
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
 
/**
 * @author TongHao on 2021/1/7
 */
public class Partitioner {
 
    public static void main(String[] args) {
        Properties properties = KafkaProducerAnalysis.initConfig();
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        //建议按照这个顺序调试,更容易看出结果
        ProducerRecord<String, String> record1 = new ProducerRecord<>(KafkaProducerAnalysis.topic, 0, "我是key", "指定分区");
        ProducerRecord<String, String> record2 = new ProducerRecord<>(KafkaProducerAnalysis.topic, "我是key", "带有value");
        ProducerRecord<String, String> record3 = new ProducerRecord<>(KafkaProducerAnalysis.topic, "只有value");
 
        producer.send(record1);
        producer.send(record2);
        producer.send(record3);
        producer.close();
    }
}