使用Java开发生产者/消费者客户端需要先引入Pom依赖:
org.apache.kafka
kafka-clients
2.0.0
/**
* Java生产者客户端
*/
public class ProducerFastStart {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-learn";
/**
* 构建生产者客户端配置参数
*/
public static Properties initConfig() {
Properties proper = new Properties();
proper.put("bootstrap.servers", brokerList);
proper.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
proper.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
proper.put("client.id", "producer.client.id.demo");
return proper;
}
public static void main(String[] args) {
Properties proper = initConfig();
// 创建一个生产者客户端实例
KafkaProducer producer = new KafkaProducer<>(proper);
// 构建所需要发送的消息
ProducerRecord record = new ProducerRecord<>(topic, "Hello,Kafka");
// 发送消息
producer.send(record);
// 关闭生产者客户端实例
producer.close();
}
}
bootstrap.servers
、key.serializer
和value.serializer
是生产者客户端连接kafak必备的参数;ProductConfig
的常量配置,每一个生产者客户端需要的参数都有一个常量名;StringSerializer.class.getName()
,其中getName()
方法是获取全限定类名,getShortName()
方法是只获取类名;bootstrap.servers
:连接Kafka集群所需要的broker地址清单。可以设置一个或多个,中间以逗号隔开,默认值是"";
key.serializer
和value.serializer
:broker端接收的消息必须以字节数组byte[]
的形式存在,所以在将消息发往broker之前需要将消息中对应的key
和value
做相应的序列化来转换成字节数组(在消费者客户端再使用对应的反序列化类将字节数组反序列化回来);
client.id
:用来设定KafkaProducer对应客户端的id,默认值为"",如果客户端不设置,则KafkaProducer会自动生成一个非空字符串;
000-参数配置
;生产者实例是KafkaProducer
,发送的消息是ProducerRecord
:
/**
* 消息 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;
// ... 省略成员方法
key
和value
的类型;topic
属性指定消息要发送到哪个主题;value
是消息内容;topic
和value
属性是必填项;parition
指定消息要发送的分区,如果不指定partition字段,则由分区器根据key
字段计算partition
的值;消息发送方法send()
有两个重载方法:
/**
* KafkaProducer的send方法签名
*/
public Future send(ProducerRecord record);
public Future send(ProducerRecord record, Callback callback);
Future
,代表方法本身就是异步的,返回的Future
对象可以使调用方稍后获得发送的结果;Callback
的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认;所以,生产者发送消息可以有三种模式:发后即忘(fire-and-forget)、同步(sync)和异步(async);
这种方式只调用send方法发送消息,调用完后既不接收Future
等待发送结果,也不传递callback
回调方法;
// 构建所需要发送的消息
ProducerRecord record = new ProducerRecord<>(topic, "Hello,Kafka");
/**
* 发送方式1:发后即忘
*/
producer.send(record);
执行完send
方法后,调用返回的Future
的get
方法(这是一个阻塞方法,会一直等待返回结果,也可以使用带超时时间的get方法),返回的是一个RecordMeta
对象,对象中包含了消息的一些元数据信息,比如当前消息的主题、分区号、分区中的偏移量、时间戳等;
// 构建所需要发送的消息
ProducerRecord record = new ProducerRecord<>(topic, "Hello,Kafka");
/**
* 发送方式2:同步发送,使用返回的future调用get方法,阻塞等待返回结果
*/
Future future = producer.send(record);
try {
RecordMetadata metadata = future.get();
System.out.println("===metadata:"+metadata.topic() + "-" + metadata.partition() + "-" + metadata.offset());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Future
对象,也可以用带可超时的get(long tikeout, TimeUnit unit)
方法;get
方法时会阻塞等待发送结果;异步发送是在send()
方法里指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认;
// 构建所需要发送的消息
ProducerRecord record = new ProducerRecord<>(topic, "Hello,Kafka");
/**
* 发送方式3:异步发送,使用带Callback参数的send方法,发送完成后调用
*/
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e!=null){
e.printStackTrace();
}else {
System.out.println("===发送成功:"+recordMetadata.topic() + "-" + recordMetadata.partition() + "-" + recordMetadata.offset());
}
}
});
onCompletion()
;onCompletion()
方法的两个参数是互斥的:
RecordMetadata
不为null,Exception
为null;RecordMetadata
为null,而Exception
不为null;record1
于record2
之前发送,那么KafkaProducer就可以保证对应的callback1
在callback2
之前调用;key
发送到同一分区,也可以指定分区;KafkaProducer发送的消息会依次经过:拦截器、序列化器和分区器,然后到达消息累加器;
ProducerRecord
中指定了partition
字段(即,指定了消息发往的分区),那么就不需要分区器;Kafka中一共有两种拦截器:生产者拦截器和消费者拦截器。
生产者拦截器既可以用来在消息发送前做一些准备工作,也可以用来在发送回调逻辑前做一些定制化需求;
/**
* 生产者拦截器接口 ProducerInterceptor
*/
public interface ProducerInterceptor extends Configurable {
public ProducerRecord onSend(ProducerRecord record);
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
public void close();
}
onSend()
方法来对消息进行相应的定制化操作;onAcknowledgement()
方法,优先于用户设定的Callback
之前执行;close()
方法用于在关闭拦截器时执行一些资源清理工作;ProducerInterceptor
接口还有一个父接口org.apache.kafka.common.Configurable
,这个接口只有一个方法,主要用来获取配置信息及初始化数据:/**
* Configurable接口
*/
public interface Configurable {
void configure(Map configs);
}
ProducerInterceptor
接口的类;interceptor.classes
里指定使用自定义的拦截器;interceptor.classes
参数的默认值是"",代表没有默认拦截器;interceptor.classes
配置的顺序一一执行;生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka;
消费者需要用反序列化器(Deserializer)把从Kafka中接收的字节数组转换成相应的对象;
序列化器需要实现接口org.apache.kafka.common.serialization.Serializer
接口:
/**
* 序列化器接口 Serializer
*/
public interface Serializer<T> extends Closeable {
void configure(Map<String, ?> configs, boolean isKey);
byte[] serialize(String topic, T data);
@Override
void close();
}
configure
方法用来配置当前类;serialize
方法用来执行序列化操作;close
方法用来关闭当前的序列化器;
/**
* 内置的String序列化器StringSerializer
*/
public class StringSerializer implements Serializer {
private String encoding = "UTF8";
@Override
public void configure(Map configs, boolean isKey) {
String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
Object encodingValue = configs.get(propertyName);
if (encodingValue == null)
encodingValue = configs.get("serializer.encoding");
if (encodingValue instanceof String)
encoding = (String) encodingValue;
}
@Override
public byte[] serialize(String topic, String data) {
try {
if (data == null)
return null;
else
return data.getBytes(encoding);
} catch (UnsupportedEncodingException e) {
throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + encoding);
}
}
@Override
public void close() {
// nothing to do
}
}
除了用于String
类型的序列化器,Kafka还提供了用于ByteArray
、ByteBuffer
、Bytes
、Double
、Integer
、Long
这几种类型的序列化器,如果还无法满足应用需求,可以选择Json
、ProtoBuf
、Protostuff
等通用序列化工具实现自定义的序列化器
/**
* 使用Protostuff实现自定义序列化器
*/
public class ProtoStuffSerializer implements Serializer {
@Override
public void configure(Map configs, boolean isKey) {
}
@Override
public byte[] serialize(String topic, Object data) {
RuntimeSchema schema = null;
LinkedBuffer buffer = null;
byte[] result;
try {
schema = RuntimeSchema.createFrom(data.getClass());
buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
result = ProtostuffIOUtil.toByteArray(data, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e);
}
return result;
}
@Override
public void close() {
}
}
key.serializer
或value.serializer
中指定使用自定义序列化器,其中value.serializer
控制发送的消息内容使用的序列化器;分区器的作用是为消息分配分区
ProducerRecord
的partition
字段代表消息要发送的分区号;partition
字段指定了值,那么就不需要使用分区器;partition
字段没有指定值时,依赖分区器根据key
字段计算partition
的值;分区器需要实现接口 org.apache.kafka.clients.producer.Partitioner
/**
* 分区器接口 Partitioner
*/
public interface Partitioner extends Configurable, Closeable {
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
public void close();
}
org.apache.kafka.common.Configurable
,这个接口只有一个方法configure
,主要用来获取配置信息及初始化;partition
方法定义分区分配逻辑;org.apache.kafka.clients.producer.internals.DefaultPartitioner
DefaultPartitioner
会根据key
计算哈希,最终根据得到的哈希值来计算分区号,拥有相同key的消息会被写入同一个分区。key
为null
,那么消息会以轮询的方式发往主题内的各个可用分区;注意:
DefaultPartitioner
一样实现Partition
接口即可;partitioner.class
来显式指定使用这个分区器(如果不指定就会使用默认分区器);acks
参数消息发送的分区一般有多个副本(一个leader副本,0到多个follower副本),那么有多少个副本接收到消息后,服务器可以向生产者回复消息已经成功写入呢?答案是:由参数acks
控制,该参数的设置是消息的可靠性与吞吐量之间的权衡。
acks
参数用来指定分区中必须有多少个副本接收到这条消息后,生产者才会认为这条消息成功写入。acks参数又三种类型:
acks=1
默认值,生产者发送消息后,只要分区的leader副本
成功写入消息,那么它就会接收到来着服务端的成功响应。acks=0
生产者发送消息之后不需要等待任何服务器的响应。在其他配置环境相同的情况下,acks设置为0可以达到最大的吞吐量;acks=-1或acks=all
生产者在消息发送后,需要等待ISR中的所有副本都成功写入消息之后,才能够收到服务端的成功响应。在其他配置环境相同的情况下,acks=-1(或all)
可以达到最强的可靠性;
acks=1
的配置是一样的。注意:
properties.put("acks","0");
KafkaProducer中一般会发生两种类型的异常:可重试异常和不可重试异常;
可重试异常:比如由于网络故障导致当前消息未发送成功,可以在一定时间后重试发送消息来解决;
不可重试异常:比如发送的消息太大,不管重试多少次都不能解决的异常,称为不可重试异常;
retries参数:
retries
参数,那么只要在规定的重试次数内自行恢复了,就不会抛异常(KafkaProducer会自动重试,超过重试次数还没有发送成功时,才会抛异常);retries
参数的默认值为0;即代表不重试,配置的重试次数不把第一次发送消息算在内;max.in.flight.requests.per.connection
配置为1(限制每个连接,也就是客户端与Node之间的连接,最多缓存的请求数),该参数默认值是5;整个生产者客户端由两个线程协调运行:主线程和Sender线程(发送线程);
主线程:主线程中KafkaProducer
创建消息,然后通过可能的拦截器、序列化器、分区器作用之后,将消息缓存到消息累加器(RecordAccumulator)(也称为消息收集器);
Sender线程:负责从消息累加器中获取消息并将其发送到kafka中;
RecordAccumulator主要用来缓存消息,以便Sender线程可以批量发送,进而减少网络传输的资源消耗以提升性能;
RecordAccumulator内部为每个分区都维护了一个双端队列,队列中的内容是ProducerBatch,即Deque
;
ProducerRecord
,ProducerBatch
是指一个消息批次,可以包含一至多个ProducerRecord
;ProducerRecord
拼凑成较大的ProducerBatch
,可以减少网络请求的次数,以提升整体的吞吐量;ProducerRecord
)流入消息累加器(RecordAccumulator
)时,会先寻找消息分区所对应的双端队列,如果没有则新建,再从这个双端队列的尾部获取一个ProducerBatch
,如果没有也新建,查看ProcuderBatch中是否还可以写入这个ProducerRecord
,如果可以则写入,如果不可以则需要创建一个新的ProducerBatch
;消息写入时,追加到双端队列的队尾,Sender线程读取消息时,从双端队列的头部读取;
参数:
buffer.memory
配置,单位是B
默认值是33554432
即32MB
;batch.size
参数用于指定ProducerBath
可以复用内存区域的大小;单位是B
默认值是16384
即16KB
ProducerRecord => <分区,Deque>
ProducerRecord
到达消息累加器RecordAccumulator
时,RecordAccumulator
将消息封装成ProducerBatch
,将ProducerBatch
放入双端队列中,一个分区对应一个双端队列,消息结构是<分区,Deque>
;<分区,Deque> => >
>
;> =>
的形式,然后将Request
发送给kafka;请求在从Sender线程发送给kafka之前还会保存到InFlightRequests
中;它的主要作用是缓存已经发出去但还没有收到响应的请求。保存对象的具体形式为Map
;
max.in.flight.requests.per.connection
设置每个连接(即到每个NodeId)最多缓存的请求数,默认值是5
;超过该数值之后就不能再向这个连接发送更多的请求了,除非缓存的请求收到了响应。
通过比较Node节点未响应的请求消息Deque
的size与配置的最大参数的大小可以来判断对应的Node中是否已经积压了很多未响应的请求,如果真是如此,那么说明这个Node节点负载较大或网络连接有问题,再继续向其发送请求会增加请求超时的可能;