问:要把大象装冰箱总共分几步?
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<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生产者开发逻辑及步骤,实际应用中肯定比示例代码复杂的多。可能会涉及更多的参数设置,同步、异步的选型及发送成功或异常逻辑处理等。但总体来说只要理清楚并牢记生产者的开发步骤,其实还算是比较简单的。