消息经过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();
}
}