示例
import com.evan.kafka.interceptor.ProducerInterceptorPrefix;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.Random;
public class Producer {
public static final String brokerList = "ip:9092";
public static final String topic = "topic-demo";
public static Properties initConfig(){
Properties properties = new Properties();
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList);
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class);
return properties;
}
public static void main(String[] args) {
Properties properties = initConfig();
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
try {
int i = 0;
while (i < 10){
String msg = "Hello," + new Random().nextInt(100);
//消息对象
ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, msg);
producer.send(record);
System.out.println("消息发送成功:" + msg);
Thread.sleep(500);
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
}
}
}
生产者流程
- 配置生产者客户端参数及创建响应的生产者实例
- 构建待发送的消息
- 发送消息
- 关闭生产者实例
参数配置
必要参数配置
bootstrap.servers: 该参数用来指定生产者客户端连接Kafka集群所需的broker地址清》单,格式:
host1:port1,host2:port2key.serializer&value.serializer: broker端接收的消息必须以字节数组(byte[])的形式存在。
其他参数配置
properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
用来设定KafkaProducer对应的客户端id 如果不设置:KafkaProducer会自动生成一个非空字符串,内容形式如“producer-1”,“producer-2”
创建生产者实例
KafkaProducer是线程安全的,可以在多个线程中共享单个KafkaProducer实例
KafkaProducer中有多个构造方法,比如在创建KafkaProducer实例时并没有设定“key.serializer”& “value.serializer”,那么就需要在构造方法中添加对应的序列化器,如下:
KafkaProducer<String, String> producer = new KafkaProducer<>(properties, new StringSerialiser(), new StringSerialiser());
内部原理其实一样
消息的发送
属性
构造方法
发送消息的三种模式
发后即忘(fire-and-forget)
它只管往Kafka发送消息而并不关心消息是否正确到达。在大多数情况下,这种方式没有什么问题,不过在某些时候(比如发生不可重试异常)会造成消息的丢失。这种发送方式的性能最高,可靠性最差。
同步(sync)
KafkaProducer的send方法并非是void类型,而是Future类型 通过get方法阻塞等待Kafka的响应,直到消息发送成功,或者发生异常。发生异常,捕获,交由外界处理
不可重试异常:不会进行任何重试,直接抛出,如RecordToolLargeException,所发消息太大; 可重试异常:如果配置了retries参数,那么只要在规定的重试次数内自行恢复,就不会抛出异常。 如: properties.put(ProducerConfig.RETRIES_CONFIG, 10);
可靠性高,要么成功,要么异常,性能差很多,需要阻塞等待一条信息发送完之后才能发送下一条。
异步(async)
一般是在send()方法里指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的逻辑处理。
onCompletion()方法里的参数是互斥的。
拓展
producer.send(record1, callback1); producer.send(record2, callback2);
对于一个分区而言,如果消息record1先于消息record2发送,那么KafkaProducer就可以保证callback1先于 callback2调用,也就是说,回调函数的调用可以保证分区有序。
关闭
producer.close();
KafkaProducer不会只负责发送单条消息,更多的是发送多条消息,发送完后,需要调用close()方法来回收资源。
close()方法会阻塞等待之前所有的发送请求完成后在关闭KafkaProducer。
序列化
configure()方法用来配置当前类
serialize()方法用来执行序列化操作
close()方法用来关闭当前的序列化器
分区器
消息通过send()方法发往broker的过程中,有可能需要经过拦截器(Interceptor)、序列化器(Serializer) 和分区器(Partitioner)的一系列作用后才能被真正地发往broker。
消息经过序列化后就需要确定发往哪个分区,如果消息ProducerRecord中指定了partition字段,> 那么就不需要分区器的作用。
Partitioner
partition()方法用来计算分区号。 参数(主题、键、序列化后的键、值、序列化后的值、集群的元数据信息)
close()方法用来关闭分区器的时候回收一些资源
DefaultPartitioner
Kafka默认的分区器
public class DefaultPartitioner implements Partitioner {
//主题与nextValue映射
private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap();
public DefaultPartitioner() {
}
public void configure(Map<String, ?> configs) {
}
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();
//如果key字节数组为空,也就是producer没有指定key的情况下。采用轮询策略。
if (keyBytes == null) {
//根据topic获取nextValue值
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;
}
//如果指定了key就使用一致性hash算法来指定发送的分区。
} else {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
//这里获取该主题对应的随机数,为了保证多线程下的数据原子性使用juc下的原子整型类
AtomicInteger counter = (AtomicInteger)this.topicCounterMap.get(topic);
//随机数为空,也就是还没有进行初始化的的情况。
if (null == counter) {
//获取随机数,进行初始化。
counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
//如果没有这个值放入map中,还是为了确保多线程环境下随机值不会被覆盖。
AtomicInteger currentCounter = (AtomicInteger)this.topicCounterMap.putIfAbsent(topic, counter);
//如果不为空,把从map中获取到的值赋值给随机数。用map中随机值把我们生成的随机值覆盖掉。
if (currentCounter != null) {
counter = currentCounter;
}
}
//返回并且将随机值+1
return counter.getAndIncrement();
}
public void close() {
}
}
生产者拦截器
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某些规则过滤不符合要求的消息、 修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作
ProducerInterceptor
onsend ()方法用来KafkaProducer将消息序列化和计算分区之前会调用生产者拦截器的方法来 对消息进行相应的定制化操作
onAcknowledgement()方法,KafkaProducer会在消息被应答(Acknowledgement)之前或消息 发送失败时调用生产者拦截器的onAcknowledgement()方法,优先于用户设定的Callback之前 执行。这个方法运行在Producer的I/O线程中,所以这个方法中实现的代码逻辑越简单越好, 否则会影响消息的发送速度。
close()方法用于在关闭拦截器时执行一些资源的清理工作
手写一个拦截器ProducerInterceptorPrefix
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
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> producerRecord) {
String modifiedValue = "prefix-" + producerRecord.value();
return new ProducerRecord<>(producerRecord.topic(),
producerRecord.partition(), producerRecord.timestamp(),
producerRecord.key(), modifiedValue, producerRecord.headers());
}
@Override
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if (e == null) {
sendSuccess++;
} else {
sendFailure++;
}
}
@Override
public void close() {
double successRatio = sendSuccess / (sendFailure + sendSuccess);
System.out.println("[INFO]发送成功率="
+ String.format("%f", successRatio * 100) + "%");
}
@Override
public void configure(Map<String, ?> map) {
}
}
使用
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptorPrefix.class);