消息发送
Kafka Java客户端数据生产流程
发送类型
- 同步发送:
//通过send()发送完消息后返回一个Future对象,然后调用Future对象的get()方法等待Kafka响应。
//如果kafka正常响应,返回一个RecordMetadata对象,该对象存储消息的偏移量。
//如果kafka发生错误,无法正常响应,就会抛出异常,便可以进行异常处理。
producer.send(record).get();
- 异步发送:
producer.send(record,new Callback() {
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(metadata.partition()+":"+metadata.offset());
}
}
});
序列化器
消息到网络上进行传输,必须进行序列化,而序列化器的作用就是如此。
Kafka提供了默认的字符串序列化器(StringSerializer),还有整型(IntegerSerializer)和字节数组(BytesSerializer)序列化器,这些序列化器都实现了接口org.apache.kafka.common.serialization.Serializer,基本上能满足大部分场景的需求。
自定义序列化器:
// Company是自定义的公司类
public class Company {
private String name;
private String address;
...
}
public class MySerializer implements Serializer<Company> {
@Override
public void configure(Map configs, boolean isKey) {
}
@Override
public byte[] serialize(String topic, Company data) {
if (data == null) {
return null;
}
byte[] name, address;
try {
if (data.getName() != null) {
name = data.getName().getBytes("UTF-8");
} else {
name = new byte[0];
}
if (data.getAddress() != null) {
address = data.getAddress().getBytes("UTF-8");
} else {
address = new byte[0];
}
ByteBuffer buffer = ByteBuffer.allocate(4+4+name.length+address.length);
buffer.putInt(name.length);
buffer.put(name);
buffer.putInt(address.length);
buffer.put(address);
return buffer.array();
} catch(Exception e) {
}
}
}
分区器
本身Kafka有自己的分区策略,如未指定,就会使用默认的分区策略。
Kafka根据传递消息的key来进行分区的分配,即hash(key)%numPartitions。如果key相同的话,那么就会分配到统一分区。
org.apache.kafka.clients.producer.internals.DefaultPartitoner分析:
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
int nextValue = this.nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return ((PartitionInfo)availablePartitions.get(part)).partition();
} else {
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
也可以自定义分区器:
public class DefinePartitioner implements Partitioner {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes = null) {
return counter.getAndIncrement() % numPartitions;
} else {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
@Override
public void close() {
}
@Override
public void configure(Map<String,?> configs) {
}
}
拦截器
producer拦截器(interceptor)是个相当新的功能。它和consumer端interceptor是在Kafka 0.10版本中被引入,主要用于实现clients端的定制化控制逻辑。
生产者拦截器可以用在消息发送前做一些准备工作。
使用场景:
- 按照某个规则过滤掉不符合要求的消息。
- 修改消息的内容。
- 统计类的需求(统计发送量、发送成功率)。
拦截器直接在properties中配置使用。
自定义拦截器:
public class ProducerInterceptorPrefix implements ProducerInterceptor<String,String> {
private volatile long sendSuccess = 0;
private volatile long sendFailure = 0;
@Override
public ProducerRecord<String,String> onSend(ProducerRecord<String,String> record) {
String modifiedValue = "prefix-" + record.value();
return new ProducerRecord<>(record.topic(),record.partition(),record.timestamp(),record.key(),modifiedValue,record.headers());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if (e == null) {
sendSuccess++;
} else {
sendFailure++;
}
}
@Override
public void close() {
double successRatio = (double) sendSuccess / (sendSuccess + sendFailure);
System.out.println("发送成功率:"+String.format("%f",successRatio*100)+"%");
}
@Override
public void configure(Map<String,?> map) {}
}
发送原理剖析
消息发送过程中,涉及到两个线程协同工作,主线程首先将业务数据封装成ProducerRecord对象,之后调用send()方法将消息放入RecordAccumulator(消息收集器,也可以理解为主线程与sender线程直接的缓冲区)中暂存,Sender线程负责将消息信息构成请求,并最终执行网络I/O线程,它从RecordAccumulator中取出消息并批量发送出去,需要注意的是,KafkaProducer是线程安全的,多个线程可以共享使用同一个KafkaProducer对象。