深入理解Kafka系列(二)--Kafka生产者
系列文章目录
前言
本系列是我通读《Kafka权威指南》这本书做的笔录和思考。
正文
Kafka生产者
Kafka发送消息的主要步骤
首先放图:向Kafka发送消息的主要步骤
用文字描述:
- 创建一个ProducerRecord对象,该对象包含主题、发送的内容等属性。
- 指定键和分区,用于把ProducerRecord对象发送到指定的分区。并且发送对象的时候,生产者需要把键和值序列化成字节数组(需要我们设定序列化器),这样,他们才能够在网络上传输。
- 数据发送给分区器,决定消息发送到哪个主题和分区上。紧接着,这条记录被添加到一个记录批次里面(RecordAccumulator,消息累加器)。
- 消息到达一定程度后,会有一个独立的线程(Sender)线程将同一个批次的消息全部发送到对应的broker上。
- 服务器收到消息后,返回一个响应
1.若成功:
返回一个RecordMetaData对象,包含了主题、分区、偏移量。
2.若失败:
返回一个错误,生产者收到错误后,会尝试重新发送消息,在一定次数后若还是失败,返回错误信息。
创建Kafka生产者(API)
这里记得提前把kafka开起来,安装步骤在上一篇博客中:
深入理解Kafka系列(一)–初识kafka
pom包依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>0.11.0.0</version>
</dependency>
简单的demo:
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class Test {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.237.130:9092");
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
for (int i = 0; i < 3; i++) {
producer.send(new ProducerRecord<String, String>("test", Integer.toString(i), "message" + i));
}
producer.close();
}
}
执行后会发现终端上:
验证:
拷贝一个会话,输入命令:
./bin/kafka-console-consumer.sh --zookeeper 192.168.237.130:2181 --from-beginning --topic test
可见消息成功写入。
接下来就是对生产者API的细节介绍了:
Kafka生产者参数详解
以上的demo是最基本的一个kafka生产者。大家可以观察到,我只设置了3个属性,而这3个属性是必须要设置的。
- bootstrap.servers
该属性指定broker的地址清单,地址的格式为host:port。
如果kafka是集群,不必把所有的broker都写上去,因为生产者会从给定的broker里查找到其他broker的信息,但是注意:建议至少提供2个broker节点,以防宕机引发其他问题。
- key.serializer
broker希望接收到的消息的键值对都是字节数组,而key.serializer
必须被设置为一个实现了org.apache.kafka.common.serialization.StringSerializer接口的类,而生产者会使用这个类把键对象序列化成字节数组,以用于网络传输。
- value.serializer
和key.serializer一样,必须设置。
- acks
acks参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。(这个参数非常重要,对消息丢失的可能性影响很大)
(一)acks=0:
1.生产者在成功写入消息之前不会等待任何来自服务器的响应。
2.即:若发送消息出了问题,生产者是不会知道的,消息也就丢失了。
3.也因为第二条原因,生产者不需要等待服务器的响应,因此这种模式可以支持最大速度去发送消息,吞吐量高。
(二)acks=1
1.只要集群的主节点收到消息,生产者就会收到一个响应。
2.若消息没有发送到主节点或者主节点宕机了,一个没收到消息的从节点成为了主节点。都会发生消息的丢失。
(三)acks=all
1.当所有参与复制的节点全部收到消息的时候,生产者才会收到来自服务器的成功响应。这种模式是最安全的,但是他的吞吐量最低,延迟也最高。
- buffer.memory
1.用来设置生产者内存缓冲区的大小。
2.因为kafka的消息是先放到生产者的缓冲区的,如果缓冲区满了,再启一个独立Sender线程,去把缓冲区的消息发送到服务器。
- compression.type
1.默认情况下,消息发送的时候是不会被压缩的。这个参数也就是设置kafka消息的压缩格式。
2.压缩格式支持:snappy,gzip,lz4.
- retries
该参数的值决定了生产者可以重复发送消息的次数,如果达到这个次数,生产者就会放弃重试并返回错误。
- batch.size
1.当多个消息需要被发送到一个分区的时候,生产者会把他们放到同一个批次里面。
2.而这个参数决定了一个批次可以使用的内存大小,按照字节数来计算。
- linger.ms
1.该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。
2.kafkaProducer会在批次填满或者该参数达到上限时把批次发送出去。
- client.id
该参数可以试试任意的字符串,服务器会用他来识别消息的来源。
- max.in.flight.requests.per.connection
1.该参数决定了生产者在收到服务器响应前可以发送多少个消息。
2.值越高,占用的内存越高,但是吞吐量越高。
- max.block.ms
1.该参数指定了在调用send()方法的时候,获取元数据时,生产者的堵塞时间。
2.何时发生堵塞?当生产者的发送缓冲区已经满了,或者没有可用的元数据,这个send方法就会堵塞。
3.超过堵塞时间,抛异常。
- max.request.size
该参数用于控制生产者发送消息的请求大小
- receive.buffer.bytes和send.buffer.bytes
这两个参数分别指定了TCP socket接收和发送数据包的缓冲区大小。
Kafka生产者发送方式详解
kafka发送消息的方式主要有3种
- 发送并忘记(fire-and-forget)
- 同步发送
- 异步发送
1.最基础的发送方式:
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
// 构造格式: topic,key,value
ProducerRecord<String, String> record = new ProducerRecord<>("test", Integer.toString(4), "message" + 4);
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
2.同步发送消息:
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
ProducerRecord<String, String> record = new ProducerRecord<>("test", Integer.toString(4), "message" + 4);
try {
// 我们调用send方法返回的是一个Future对象。然后调用get方法等待kafka响应
// 在调用Future的get()方法,若写入成功,则返回一个正确的相应RecordMetadata。
RecordMetadata recordMetadata = producer.send(record).get();
System.out.println(recordMetadata.offset());
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
结果:
3.异步发送消息:
自定义一个回调类(注意实现类的包):
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.RecordMetadata;
public class DemoProducerCallBack implements Callback {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
// 意思是不报错的话,我们会返回消息。
if (e == null) {
System.out.println("发送成功!");
}
}
}
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
ProducerRecord<String, String> record = new ProducerRecord<>("test", Integer.toString(5), "message" + 5);
try {
// 直接将我们自定义的回调类放入send方法中即可
producer.send(record, new DemoProducerCallBack());
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
结果:
序列化器
首先我们应该明白一点,序列化器是干啥用的?
说白了,就是为了让生产者端产生的消息,能够顺利的在网络上传输。
那如果说,我们要把一个对象作为消息,进行发送,无疑,我们一定要自定义一个序列化器,否则会报错。
自定义序列化器Demo
假设我们要把Customer对象作为消息进行传输。
Customer类:
public class Customer {
private int customerId;
private String customerName;
public Customer(int customerId, String customerName) {
this.customerId = customerId;
this.customerName = customerName;
}
public int getCustomerId() {
return customerId;
}
public void setCustomerId(int customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
}
自定义序列化器(注意包不要导错了):
CustomerSerializer
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;
import java.nio.ByteBuffer;
import java.util.Map;
public class CustomerSerializer implements Serializer<Customer> {
@Override
public void configure(Map<String, ?> map, boolean b) {
// 不做任何事
}
@Override
public byte[] serialize(String topic, Customer data) {
try {
byte[] serializedName;
int stringSize;
if (data == null) {
return null;
} else {
if (data.getCustomerName() != null) {
serializedName = data.getCustomerName().getBytes("UTF-8");
stringSize = serializedName.length;
} else {
serializedName = new byte[0];
stringSize = 0;
}
}
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
buffer.putInt(data.getCustomerId());
buffer.putInt(stringSize);
buffer.put(serializedName);
return buffer.array();
} catch (Exception e) {
throw new SerializationException("Error!!!!");
}
}
@Override
public void close() {
}
}
测试类:(注意序列化器已经改变了)
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class Test2 {
public static void main(String[] args) {
Properties properties = new Properties();
// 这里的value,填你的自定义序列化器的引用地址。
properties.put("bootstrap.servers", "192.168.237.130:9092");
properties.put("key.serializer", "kafka.CustomerSerializer");
properties.put("value.serializer", "kafka.CustomerSerializer");
KafkaProducer<String, Customer> producer = new KafkaProducer<>(properties);
Customer customer = new Customer(1, "hello");
ProducerRecord record = new ProducerRecord("test", customer);
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
}
}
结果:
这里为了作对比,再创建个Customer类,名叫Customer2(复制即可)
如果代码改成:
看看结果是什么?
为什么?因为我们自定义的序列化器这里写着:
要一一对应!
使用自定义序列化器的缺点:
- 以上demo是一个简单的入门,那问题来了,如果现实项目中,如果存在多种类型的对象,那如果有100个对象都要作为消息传输,那我们是不是也要创建100个自定义的序列化器呢?
- 因此,不建议使用自定义序列化器,最好使用一些序列化器的框架,如:JSON,Avro,Protobuf等等。
总结
本文大概从这么几个方面进行概述:
1.Kafka生产者的大致流程。
2.Kafka生产者API的相关参数和发送消息的方式。
3.Kafka-序列化器。
下一篇文章会根据Kafka的消费者以及API层面去详细的介绍。