Kafka系列 推荐阅读:
奔跑吧,Kafka!
本文内容来源参考《深入理解Kafka核心设计与原理实践》——朱忠华 著
问:要把大象装冰箱总共分几步?
1. 打开冰箱门。
2. 把大象装冰箱。
3. 关闭冰箱门。
其实创建一个Kafka的生产者和大象装冰箱极度相似,一个正常的Kafka生产者开发逻辑大致有以下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 producer = new KafkaProducer<>(props);
//第二步
ProducerRecord 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个参数是必填的。
可以像示例代码一样将参数放入Properties对象,然后通过传入Properties对象来创建KafkaProducer示例,也可以直接将Properties对象替换成Map,再或者也可以在构造方法中添加对应的序列化器,其内部原理都是一样,不过一般还是像示例代码中那样来创建KafkaProducer实例。例如:
KafkaProducer producer = new KafkaProducer<(props,new StringSerializer(),new StringSerializer());
KafkaProducer是线程安全的,可以在多个线程中共享单个KafkaProducer实例,也可以将KafkaProducer实例进行池化来提供给其他线程调用。
2、构建待发送的消息。
ProducerRecord对象的属性结构
public class ProducerRecord {
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 headers) {}
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) {}
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable 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种模式:
发后即忘,意思就是它只管往Kafka中发送消息而并不关心消息是否正确到达。示例代码中就是这种发送方式。多数情况下,这种方式并没什么问题,但有些时候(比如发生不可重试异常时)会造成消息丢失。这种发送方式性能最高,但可靠性也是最差的。
KafkaProducer中一般会发生两种类型的异常:可重试异常和不可重试异常。常见的可重试异常有:NetWorkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException等。比如NetWorkException表示网络异常,可能是由于网络瞬时故障而导致的异常,可以通过重试解决;又比如LeaderNotAvailableException表示分 区的leadr不可用,这个异常通常发生在leader副本下线而新的leader副本选举完成之前,重试之后可以重新恢复。不可重试的异常,比如上一 篇提到的RecordTooLargeException异常,表示所发送的消息太大,对此不会进行任何重试,直接抛出异常。
对于可重试异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复了,就不会抛出异常。其默认值为0,配置方式如下:
props.put(ProducerConfig.RETRIES_CONFIG,10);
可以利用send()方法返回的Future
try{
producer.send(record).get();
}catch (Exception e){
logger.info("消息发送异常:" + e.getMessage());
}
看到返回类型为Future,就应该明白实际上send()方法本身就是异步的,Future对象可以使调用方稍后获得发送结果,但通过链式调用get()方法来阻塞等待Kafka的响应,直到消息发送成功或者发生异常。
如果有需要,也可以不链式调用get(),而是获取一个RecordMetadata对象,其中包含了消息的一些元数据:主题、分区号、所在分区中的偏移量、时间戳等。如下:
try{
Future 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 send(ProducerRecord 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生产者开发逻辑及步骤,实际应用中肯定比示例代码复杂的多。可能会涉及更多的参数设置,同步、异步的选型及发送成功或异常逻辑处理等。但总体来说只要理清楚并牢记生产者的开发步骤,其实还算是比较简单的。
如果文章对你有帮助的话
请关注并转发一下