Kafka学习笔记(二)· 生产者详解

二、生产者详解

1、相关介绍

1、java客户端数据生产流程图

image-20201204160140282

2、发送类型

  • 发送即忘记

    producer.send(record);
    
  • 同步发送

    //通过send()发送完消息后返回一个Future对象,然后调用Future对象的get()方法等待kafka响应
    //  如果kafka正常响应,返回一个RecordMetadata对象,该对象存储消息的偏移量
    //  如果kafka发生错误,无法正常响应,就会抛出异常,我们便可以进行异常处理
    producer.send(record).get();
    
  • 异步发送

    producer.send(record,new Callback(){
       public void onCompletion(RecordMetadata metadata,Exception exception){
           if(exception == null){
           	System.out.println(metadata.partition()+":"+metadata.offset());
           }
       } 
    });
    

3、序列化器

消息要到网络上进行传输,必须进行序列化,而序列号器的作用就是入磁。

kafka提供了默认的字符串序列化器(org.apache.kafka.common.serialization.StringSerializer),还有整型(IntegerSerializer)和字节数组(BytesSerializer)序列化器,这些序列化器都实现了接口(org.apache.kafka.common.serialization.Serializer)基本上能够满足大部分场景的需求。

4、自定义序列化器

/**
 * 自定义序列化器
 */
public class CompanySerializer implements Serializer<Company>{
    @Override
    public void configure(Map configs,boolean isKey){
        
    }
    
    @Override
    public byte[] serializer(String topic,Company data){
        if(data == null){
            return null;
        }
        byte[] name,address;
        try{
            if(data.getName()!=null){
                name = data.getName().getBytes("UTF-8");
            }else{
                name = new byte[0];
            }
            if(data.getAddress()!=null){
                address = data.getAddress().getBytes("UTF-8");
            }else{
                address = new byte[0];
            }
            ByteBuffer buffer = ByteBuffer.allocate(4+4+name.length+address.length);
            buffer.putInt(name.length);
            buffer.put(name);
            buffer.putInt(address.length);
            buffer.put(address);
            return buffer.array();
        }catch(UnsupportedEncodinfException e){
            e.printStackTrace();
        }
        return new byte[0];
    }
    
    @Override
    public void close(){
        
    }
}

5、分区器

本身kafka有自己的分区策略的,如果未指定,就会使用默认的分区策略

kafka根据传递消息的key来进行分区的分配,即hash(key)%nnumPartitions。如果key相同的化,那么就会分配到同一分区。

源码:org.apache.kafka.clients.producer.internals.DefaultPartitioner

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 = Uitls.toPositive(nextValue)%availablePartitions.size();
            return ((PartitionInfo)availablePartitions.get(part)).partition();
        }else{
            return Utils.toPositive(nextValue)%numPartitions;
        }
    }else{
        return Utils.toPositive(Utils.murmur2(keyBytes))% numPartitions;
    }
}
  • 自定义分区器
/**
 * 自定义分区器
 */
public class DefinePartitioner implements Partitioner{
    private final AtomicInteger counter = new AtomicInteger(0);
    
    @Override
    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){
            return counter.getAndIncrement()%numPartitions;
        }else{
        	return Utils.toPositive(Utils.murmur2(keyBytes))% numPartitions;

        }
    }
    @Override
    public void close(){
        
    }
}
  • 实现自定义分区器需要通过配置参数ProducerConfig.PARTITIONER_CLASS_CONFIG来实现
//自定义分区器的使用
props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,DefinePartitioner.class.getName());

6、拦截器

Producer拦截器(interceptor)是相当新的功能,它和consumer端interceptor是在Kafka0.10版本被引入的,主要用于实现client端的定制化控制逻辑。

生产者拦截器可以用在消息发送前做一些准备工作。

使用场景

  1. 按照某个规则过滤掉不符合要求的消息
  2. 修改消息的内容
  3. 统计类需求
/**
 * 自定义拦截器
 */
public class ProducerInterceptorPrefix implements ProducerInterceptor<String,String>{
    private volatile long sendSuccess = 0;
    private volatile long sendFailure = 0;
    
    @Override
    public ProducerReccord<String,String> onSend(ProducerRecord<String,String> record){
        String modifiedValue = "prefix1-"+record.value();
        return new ProducerRecord<>(record.topic(),record.partition(),record.timestamp(),
                                    record.key(),modifiedValue,record.headers());   
    }
    
    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata,Exception e){
        if(e == null){
            sendSuccess++;
        }else{
            sendFaulure++;
        }
    }
    
    @Override
    public void close(){
        double successRatio = (double)sendSuccess / (sendFailure + sendSuccess);
		System.out.println("[INFO]发送成功率="+String.format("%f",successRatio * 100)+"%");
    }
    
    @Override
    public void configure(Map<String,?> map){
        
    }
}
  • 实现自定义拦截器之后需要在配置参数中指定这个拦截器,此参数的默认值为空

    //自定义拦截器使用
    props.put(ProducerConfig.INTERCEPTOR_CLASS_CONFIG,
              ProducerDefineSerializer.class.getName());
    
  • 功能演示

    发送端

    image-20201204160203294

    接收端

    image-20201204160216170

2、发送原理刨析

image-20201204160231703

​ 消息发送的过程中,设计到两个线程协调工作,主线程首先将业务数据封装成ProducerRecord对象,之后调用send()方法将消息方入RecordAccumulator(消息收集器,也可以理解为主线程与Sender线程直接的缓冲区)中暂存,Sender线程负责将消息构成请求,并最终执行网络I/O的线程,它从RecordAccumulator中取出消息并批量发送出去,需要注意的是,KafkaProducer是线程安全的,多个线程间可以共享使用同一个KafkaProducer对象

3、其他生产者参数

1、acks

这个参数用来指定分区中必须有多少个副本收到这条消息,之后生产者才会认为这条消息是写入成功的。acks是生产者客户端中非常重要的一个参数,它涉及到消息的可靠性和吞吐量之间的权衡。

  • ack=0,生产者在成功写入消息之前不会等待任何来自服务器的响应。如果出现问题生产者是感知不到的,消息就丢失了,不过因为生产者不需要等待服务器响应,所以它可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量
  • ack=1,默认值为1,只要集群的主节点收到消息,生产者就会收到一个来自服务器的成功响应,如果消息无法达到主节点(比如主节点崩溃,新的主节点还没被选举出来),生产者会受到一个错误响应,为了避免数据丢失,生产者会重复发消息。但是,这样还有可能会导致数据丢失,如果收到写成功通知,此时主节点还没来得及同步数据到follower节点,主节点崩溃,就会导致数据丢失。
  • ack=-1,只有当所有参与复制的节点都收到消息时,生产者会收到一个来自服务器的成功响应,这种模式时最安全的,它可以保证不止一个服务器收到消息。

注意:acks参数配置的时一个字符串类型,而不是整数类型,如果配置为证书类型会抛出异常

2、retries

生产者从服务器收到的错误有可能时临时性的错误(比如分区找不到首领),在这种情况下,如果达到了retries设置的次数,生产者会放弃重试并返回错误,默认情况下,生产者会在每次重试之间等待100ms,可以通过retry.backoff.ms参数来修改这个时间间隔。

3、batch.size

当有多个消息被发送到同一个分区时,生产者会把他们放在同一个批次里,该参数指定了一个批次可能使用的内存大小,按照字节数计算,而不是消息个数,当批次被填满,批次里的所有消息会被发送出去。不过生产者并不一定都会等到批次被填满才发送,半满的批次,甚至只包含一个消息的批次也可能被发送。所以就算把batch.size设置的很大,也不会造成延迟,只会占用更多的内存而已,如果设置的大小,生产者会因为频繁发送消息而增加一些额外的开销。

4、max.request.size

该参数用于控制生产者发送的请求大小,它可以指定能发送的单个消息的最大值,也可以指单个请求里所有消息的总大小。broker对可接收的消息最大值也有自己的限制(message.max.size),所以两边的配置最好匹配,避免生产者发送的消息被broker拒绝。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值