KafKa(五) :生產者詳解

一、kafka java客戶端數據生產流程解析

圖1
  1. 構造一個produceRecord對象, 需要要指定主題和值(value),key和分區可以暫時不指定。
  2. 發送信息,由於信息是通過網絡傳輸的,所以需要對傳輸的值進行序列化,將其變成字節碼進行傳輸。可以進行同步發送異步發送
  3. 序列化器:消息要到網絡上進行傳播,必須進行序列化,而序列化器的作用就是如此,kafka提供了大量的序列化器,如果不滿足需求,可以自定義序列化器。實現 Serializer 接口。
  4. 分區器:本身kafka是有分區策略的,如果未指定,則使用默認策略。kafka會根據傳遞消息的key進行分區的分配,即hash(key)%numPartitions,如果key相同的話,就會被分到同一分區。
  5. 攔截器:(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() {
    }
}

五、攔截器

  •  使用場景:
  1. 按照某個規則過濾掉不符合要求的消息。
  2. 修改消息的內容,比如消息前綴
  3. 統計類需求(可以使用多個攔截器,組成攔截器鏈)
  • 自定義攔截器
//設置自定義攔截器
        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);
        });


    }
}

kafka自定義攔截器更加詳細的解釋

六、發送原理圖


消息發送過程中,涉及到兩個線程的協同工作,主線程首先將業務數據封裝成producerRecord對象,之後調用sender()方法將消息放入到RecordAccumulator(消息收集器,也可以理解爲主線程與sender()線程之間的緩衝區)中暫存,判斷RecordAccumulator狀態,當緩存中的批次消息滿了的時候或者新建了批次,則sender線程將被喚醒,sender()負責將消息信息構成請求,並最終執行網絡IO線程,他從RecordAccumulator中取出消息,並且批量發送出去(kafka流程各個中間過程細緻化的詳解)。需要注意的是,kafkaproducer是線程安全的,多個線程可以共享kafkaproducer對象。


  • 其他的一些參數:
  1. acks:這個參數用來指定必須有多少個副本收到這條消息,才被生產者認爲這條消息是寫入成功的。
  • ack=0,生產者在成功寫入消息之前,不會等待任何來自服務器的響應。
  • ack=1(默認),只要集羣leader節點收到消息,生產者就會收到一個來自服務器成功的響應。
  • ack=-1,只有當所有參與複製的節點都收到消息時候,生產者會收到一個來自服務器成功的響應。

    2.retries:重試次數,默認100ms

   3.batch.size:該參數指定了一個批次可以使用的內存大小,按照字節數計算,而不是消息數。批次的發送,跟批次是否滿沒有關係,跟批次的內存是否達到預設值有關係,batch.size設置很大也不會造成延遲,只是會佔用更多的內存

   4.max.request.size:默認即可,brokder可接受的消息最大值也有自己的限制,這兩個參數最好是匹配的,避免生產者發送的消息被broker拒絕。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章