一、kafka java客戶端數據生產流程解析
- 構造一個produceRecord對象, 需要要指定主題和值(value),key和分區可以暫時不指定。
- 發送信息,由於信息是通過網絡傳輸的,所以需要對傳輸的值進行序列化,將其變成字節碼進行傳輸。可以進行同步發送、異步發送。
- 序列化器:消息要到網絡上進行傳播,必須進行序列化,而序列化器的作用就是如此,kafka提供了大量的序列化器,如果不滿足需求,可以自定義序列化器。實現 Serializer 接口。
- 分區器:本身kafka是有分區策略的,如果未指定,則使用默認策略。kafka會根據傳遞消息的key進行分區的分配,即hash(key)%numPartitions,如果key相同的話,就會被分到同一分區。
- 攔截器:(kafka 0.10版本引入),實現客戶端控制化邏輯。
二、消息發送類型
- 同步發送
//發送消息
try {
Future<RecordMetadata> sendResult = producers.send(producerRecords);
RecordMetadata recordMetadata = sendResult.get();
//todo 成功發送後的處理邏輯
System.out.println("偏移量:獲取此分區下的消息的起始位置"+recordMetadata.hasOffset());
System.out.println("分區:"+recordMetadata.partition());
System.out.println("主題:"+recordMetadata.topic());
} catch (Exception e) {
e.printStackTrace();
//todo 拋出異常時候的處理邏輯
}
- 異步發送
try {
Future<RecordMetadata> sendResult = producers.send(producerRecords);
// RecordMetadata recordMetadata = sendResult.get();
// //todo 成功發送後的處理邏輯
// System.out.println("偏移量:獲取此分區下的消息的起始位置"+recordMetadata.hasOffset());
// System.out.println("分區:"+recordMetadata.partition());
// System.out.println("主題:"+recordMetadata.topic());
producers.send(producerRecords, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (Objects.isNull(e)) {
System.out.println("偏移量:獲取此分區下的消息的起始位置" + recordMetadata.offset());
System.out.println("分區:" + recordMetadata.partition());
System.out.println("主題:" + recordMetadata.topic());
}
}
});
} catch (Exception e) {
e.printStackTrace();
//todo 拋出異常時候的處理邏輯
}
注意:異步發送不影響當前主線程,當消費端收到收到消息的時候,會對生產者進行一個異步的回調, 然後消費端可以做相應的處理。
三、自定義序列化器
public class StringSerializer implements Serializer<String> {
private String encoding = "UTF8";
public StringSerializer() {
}
public void configure(Map<String, ?> configs, boolean isKey) {
String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
Object encodingValue = configs.get(propertyName);
if (encodingValue == null) {
encodingValue = configs.get("serializer.encoding");
}
if (encodingValue instanceof String) {
this.encoding = (String)encodingValue;
}
}
public byte[] serialize(String topic, String data) {
try {
return data == null ? null : data.getBytes(this.encoding);
} catch (UnsupportedEncodingException var4) {
throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + this.encoding);
}
}
}
四、分區器
- 默認分區規則源碼
public class DefaultPartitioner implements Partitioner {
private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap();
public DefaultPartitioner() {
}
public void configure(Map<String, ?> configs) {
}
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();
if (keyBytes == null) {
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 {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
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();
}
public void close() {
}
}
五、攔截器
- 使用場景:
- 按照某個規則過濾掉不符合要求的消息。
- 修改消息的內容,比如消息前綴
- 統計類需求(可以使用多個攔截器,組成攔截器鏈)
- 自定義攔截器
//設置自定義攔截器
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class.getName());
public class ProducerInterceptorPrefix implements ProducerInterceptor {
@Override
public ProducerRecord onSend(ProducerRecord producerRecord) {
//消息的處理
if (Objects.isNull(producerRecord)){
throw new KafkaProducerException(producerRecord,"recored is null",new Throwable());
}
return new ProducerRecord(producerRecord.topic(),"prefix-"+producerRecord.key(),producerRecord.value());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
//對消息返回meta信息的處理
System.out.println("攔截器recordMetaData"+recordMetadata.topic());
}
@Override
public void close() {
//關閉自定義攔截器的處理, 清理資源
System.out.println("已經處理完畢");
}
@Override
public void configure(Map<String, ?> map) {
//獲取配置信息和初始化時調用(父類方法)
map.forEach((key,value)->{
System.out.println("key="+key+"---value"+value);
});
}
}
六、發送原理圖
消息發送過程中,涉及到兩個線程的協同工作,主線程首先將業務數據封裝成producerRecord對象,之後調用sender()方法將消息放入到RecordAccumulator(消息收集器,也可以理解爲主線程與sender()線程之間的緩衝區)中暫存,判斷RecordAccumulator狀態,當緩存中的批次消息滿了的時候或者新建了批次,則sender線程將被喚醒,sender()負責將消息信息構成請求,並最終執行網絡IO線程,他從RecordAccumulator中取出消息,並且批量發送出去(kafka流程各個中間過程細緻化的詳解)。需要注意的是,kafkaproducer是線程安全的,多個線程可以共享kafkaproducer對象。
- 其他的一些參數:
- acks:這個參數用來指定必須有多少個副本收到這條消息,才被生產者認爲這條消息是寫入成功的。
- ack=0,生產者在成功寫入消息之前,不會等待任何來自服務器的響應。
- ack=1(默認),只要集羣leader節點收到消息,生產者就會收到一個來自服務器成功的響應。
- ack=-1,只有當所有參與複製的節點都收到消息時候,生產者會收到一個來自服務器成功的響應。
2.retries:重試次數,默認100ms
3.batch.size:該參數指定了一個批次可以使用的內存大小,按照字節數計算,而不是消息數。批次的發送,跟批次是否滿沒有關係,跟批次的內存是否達到預設值有關係,batch.size設置很大也不會造成延遲,只是會佔用更多的內存。
4.max.request.size:默認即可,brokder可接受的消息最大值也有自己的限制,這兩個參數最好是匹配的,避免生產者發送的消息被broker拒絕。