Kafka 问:把大象装冰箱,总共要几步?

Kafka系列 推荐阅读:

奔跑吧,Kafka!

本文内容来源参考《深入理解Kafka核心设计与原理实践》——朱忠华 著

                                                                     强烈推荐

问:要把大象装冰箱总共分几步?

    1. 打开冰箱门。

    2. 把大象装冰箱。

    3. 关闭冰箱门。

其实创建一个Kafka的生产者和大象装冰箱极度相似,一个正常的Kafka生产者开发逻辑大致有以下4个步骤:

  1. 配置生产者参数及创建相应的生产者实例。
  2. 构建待发送的消息。
  3. 发送消息。
  4. 关闭生产者实例。

>>>>示例代码

public class MyKafkaProducer {
    private static final Logger logger = Logger.getLogger("MyKafkaProducer");
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";
    /**
    * @Description //生产者参数设置
    * @CreateDate 2020-02-29 16:03
    * @Param []
    * @return java.util.Properties
    **/
    public static Properties initConfig(){
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.CLIENT_ID_CONFIG,"producer.client.id.demo");
        return props;
    }
    /**
    * @Description //创建实例并发送消息
    * @CreateDate 2020-02-29 16:19    
    * @Param []
    * @return void
    **/
    public static void produce(){
        //第一步
        Properties props = initConfig();
        KafkaProducer<String,String> producer = new KafkaProducer<>(props);
        //第二步
        ProducerRecord<String,String> record = new ProducerRecord<>("topic","Hello,Kafka!");
        //第三步
        try{
            producer.send(record);
        }catch (Exception e){
            logger.info("消息发送异常:" + e.getMessage());
        }
        //第四步
        producer.close();
    }
    // for test
    public static void main(String[] args) {
        produce();
    }
}

>>>>逐步学习

1、配置生产者参数及创建相应的生产者实例

    KafkaProducer中参数众多,并非示例中的那样只有4个。每个参数在ProducerConfig类中都有对应的名称,开发人员可以根据业务实际需求来修改这些参数的默认值。在Kafka生产者客户端中有3个参数是必填的。

  • BOOTSTRAP_SERVERS_CONFIG:该参数用来指定生产者客户端连接Kafka集群所需的broker地址清单,可以设置一个或者多个地址,中间以逗号分隔,例如:host1:port1,host2:port2,此参数默认值为“”。生产者会从给定的broker里查找其他broker的信息,所以这里设置也并不需要所有的broker地址,不过为了提高系统的稳定性,建议至少设置两个以上的broker地址信息。
  • KEY_SERIALIZER_CLASS_CONFIG和VALUE_SERIALIZER_CLASS_CONFIG:broker端接收的消息必须以字节数组(byte[])的形式存在。所以,KafkaProducer在将消息发送给broker之前需要将消息中对应的key和value做相应的序列化操作。这两个参数无默认值,所以在开发时务必设置,且必须是序列化器的全限定名
  • CLIENT_ID_CONFIG:这个参数用来设置KafkaProducer对应的客户端id,默认值为“”。如果不设置,则会自动生成一个非空字符串,类似:“producer-1”、“producer-2”。

    可以像示例代码一样将参数放入Properties对象,然后通过传入Properties对象来创建KafkaProducer示例,也可以直接将Properties对象替换成Map,再或者也可以在构造方法中添加对应的序列化器,其内部原理都是一样,不过一般还是像示例代码中那样来创建KafkaProducer实例。例如:

KafkaProducer<String,String> producer = new KafkaProducer<(props,new StringSerializer(),new StringSerializer());

     KafkaProducer是线程安全的,可以在多个线程中共享单个KafkaProducer实例,也可以将KafkaProducer实例进行池化来提供给其他线程调用。

2、构建待发送的消息。

      ProducerRecord对象的属性结构

public class ProducerRecord<K, V> {
    private final String topic; //主题
    private final Integer partition; //分区号
    private final Headers headers; //消息头部
    private final K key; //键
    private final V value; //值
    private final Long timestamp;//消息的时间戳
    //省略其他成员方法和构造方法方法体
    public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {}
    public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {}
    public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) {}
    public ProducerRecord(String topic, Integer partition, K key, V value) {}
    public ProducerRecord(String topic, K key, V value) {}
    public ProducerRecord(String topic, V value) {
       this(topic, (Integer)null, (Long)null, (Object)null, value, (Iterable)null);
    }
}

    其中,topic和partition字段分别代表消息要发往的主题和分区号。headers字段是消息的头部,它大多用来设定一些与应用相关的信息,如无需要也可以不设置。key是用来指定消息的键,如果设置了key,它可以用来计算分区号进而可以让消息根据key发往特定的分区,同一个key的消息会被划分到同一个分区中。而且有key到消息还可以支持日志压缩的功能。value指消息体,一般不会为空,如果为空则表示特定的消息——墓碑消息。timestamp是指消息的时间戳,分为CreateTime和LogAppendTime两种类型,前者表示消息创建的时间,后者表示消息追加到日志文件到时间。在这些属性中,topic和value属性是必填项。

    ProducerRecord的构造方法总共有6种,代码示例中使用的是最后一种,但其内部其实也是调用了第一种构造方法,不过是将其他属性全部置为null。

    注意:针对不同的消息,需要构建不同的ProducerRecord对象,在实际应用中创建ProducerRecord对象是一个非常频繁的动作。

3、发送消息。

    构建好要发送的消息之后,就可以通过KafkaProducer对象的send方法进行发送消息了。发送消息主要有3种模式:

  • 发后即忘(fire-end-forget)

    发后即忘,意思就是它只管往Kafka中发送消息而并不关心消息是否正确到达。示例代码中就是这种发送方式。多数情况下,这种方式并没什么问题,但有些时候(比如发生不可重试异常时)会造成消息丢失。这种发送方式性能最高,但可靠性也是最差的。

      KafkaProducer中一般会发生两种类型的异常:可重试异常和不可重试异常。常见的可重试异常有:NetWorkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException等。比如NetWorkException表示网络异常,可能是由于网络瞬时故障而导致的异常,可以通过重试解决;又比如LeaderNotAvailableException表示分    区的leadr不可用,这个异常通常发生在leader副本下线而新的leader副本选举完成之前,重试之后可以重新恢复。不可重试的异常,比如上一   篇提到的RecordTooLargeException异常,表示所发送的消息太大,对此不会进行任何重试,直接抛出异常。

    对于可重试异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复了,就不会抛出异常。其默认值为0,配置方式如下:

props.put(ProducerConfig.RETRIES_CONFIG,10);

 

  • 同步(async)

    可以利用send()方法返回的Future<RecordMetadata>对象实现同步发送消息:

try{
    producer.send(record).get();
}catch (Exception e){
    logger.info("消息发送异常:" + e.getMessage());
}

    看到返回类型为Future,就应该明白实际上send()方法本身就是异步的,Future对象可以使调用方稍后获得发送结果,但通过链式调用get()方法来阻塞等待Kafka的响应,直到消息发送成功或者发生异常。

    如果有需要,也可以不链式调用get(),而是获取一个RecordMetadata对象,其中包含了消息的一些元数据:主题、分区号、所在分区中的偏移量、时间戳等。如下:

try{
    Future<RecordMetadata> future =  producer.send(record);
    RecordMetadata metadata = future.get();
    logger.info(metadata.topic() + "-" + metadata.partition() + "-" + metadata.offset());
}catch (Exception e){
    logger.info("消息发送异常:" + e.getMessage());
}

    通过Future中的get(long timeout,TimeUnit unit)方法可以实现超时的阻塞。同步发送的方式可靠性最高,但是性能也会差很多,需要阻塞等待一条消息发送完之后才能发送下一条消息。

  • 异步

    send()方法另一个重载的方法:

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {}

    通过指定一个回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认,要么发送成功,要么抛出异常。

producer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if(e != null){
            e.printStackTrace();
        }else{
            logger.info(recordMetadata.topic() + "-" + recordMetadata.partition() + ":" + recordMetadata.offset());
        }
    }
});

    onCompletion()方法的两个参数是互斥的,消息发送成功时,recordMetadata不为null而exception为null;消息发送异常时,recordMetadata为null而exception不为null。对于同一分区而言,如果消息record1先于record2发送,那么KafkaProducer就可以保证callback1在callback2之前调用,即回调函数的调用也可以保证分区有序。

4、关闭生产者实例。

    close()方法会阻塞等待之前所有的发送请求完成后再关闭KafkaProducer。同时,KafkaProducer还提供了一个带有超时时间的close()方法,只会在等待timeout时间内来完成所有尚未完成的请求处理,然后强行退出:

public void close(Duration timeout) {
    this.close(timeout, false);
}

总结:

        到现在为止,我们通过示例代码逐步学习了Kafka生产者开发逻辑及步骤,实际应用中肯定比示例代码复杂的多。可能会涉及更多的参数设置,同步、异步的选型及发送成功或异常逻辑处理等。但总体来说只要理清楚并牢记生产者的开发步骤,其实还算是比较简单的。

如果文章对你有帮助的话

关注转发一下

                                                                                   

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值