KafKa(五) :生产者详解

一、kafka java客户端数据生产流程解析

KafKa(五) :生产者详解_第1张图片 图1
  1. 构造一个produceRecord对象, 需要要指定主题和值(value),key和分区可以暂时不指定。
  2. 发送信息,由于信息是通过网络传输的,所以需要对传输的值进行序列化,将其变成字节码进行传输。可以进行同步发送异步发送
  3. 序列化器:消息要到网络上进行传播,必须进行序列化,而序列化器的作用就是如此,kafka提供了大量的序列化器,如果不满足需求,可以自定义序列化器。实现 Serializer 接口。
  4. 分区器:本身kafka是有分区策略的,如果未指定,则使用默认策略。kafka会根据传递消息的key进行分区的分配,即hash(key)%numPartitions,如果key相同的话,就会被分到同一分区。
  5. 拦截器:(kafka 0.10版本引入),实现客户端控制化逻辑。

二、消息发送类型

  • 同步发送 
//发送消息
        try {
            Future 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 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 {
    private String encoding = "UTF8";

    public StringSerializer() {
    }

    public void configure(Map 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 topicCounterMap = new ConcurrentHashMap();

    public DefaultPartitioner() {
    }

    public void configure(Map configs) {
    }

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (keyBytes == null) {
            int nextValue = this.nextValue(topic);
            List 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 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拒绝。

 

你可能感兴趣的:(#,Kafka)