为了备战四级,这段时间就没有写博客,今天考完,废话不多说,来一篇
下图为Kafka发送消息的主要步骤:
步骤1:
我们首先创建一个ProducerRecord对象,从上图可以看出,里面包含着发送的目标主题,分区,键,值,
Partition和key可以不指定,但是Topic和Value必须指定。在发送ProducerRecord对象时,生产者先把键和值对象序列化成字节数组,便于之后的网络传输的进行。
步骤2:
接下来通过序列化器,把键和值序列化成字节数组,数据传给分区器,但是如果之前在ProducerRecord对象中指定了Partition,那么分区器就不会做任何事情。如果没有指定分区,那么分区器会根据Key的对分区哈希映射来选择一个分区。之后,生产者就可以把数据发送到目标的主题和分区,这些数据都会被添加到一个批次里,这个批次的所有消息都会被发到相同的主题和分区。有一个独立的线程负责把这些记录批次发送到相应的broker上。
服务器在收到这些消息时会返回一个响应。如果消息成功写入Kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的便宜量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。
下面我们来创建Kafka生产者:
首先我们在package org.apache.kafka.clients.producer包下找到KafkaProducer有一个构造方法
/**
* A producer is instantiated by providing a set of key-value pairs as configuration. Valid configuration strings
* are documented <a href="http://kafka.apache.org/documentation.html#producerconfigs">here</a>.
* @param properties The producer configs
*/
public KafkaProducer(Properties properties) {
this(new ProducerConfig(properties), null, null);
}
我们在写自己的逻辑代码的时候给KafkaProducer构造器传入一个Properties,所以我们来创建,properties配置Kafka生产者的一些属性,看如下代码:
//创建Kafka生产者的配置信息
Properties properties = new Properties();
//指定连接的Kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop108:9092");
//ACK应答级别
properties.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数
properties.put("retries", 1);
//批次大小 单位:字节
properties.put("batch.size", 16384);
//等待时间
properties.put("linger.ms", 1);
//RecordAccumulator缓冲区大小
properties.put("buffer.memory", 33554432); //32M
//Key Value的序列化类
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
我在可以在KafkaProducerConfig.java文件里找到一些属性的键,值我们自己来指定。
但是要往kafka里面写入消息,首先要创建一个生产者对象,并设置一些属性。Kafka生产者有3个必选的属性。
bootstrap.servers
该属性指定broker的地址清单,地址的格式为host:port。清单里不需要包含所有的broker地址,生产者会从给定的broker里查找到其他broker的信息。不过建议至少要提供两个broker的信息,一旦其中一个宕机,生产者仍然能够连接到集群上。
key.serializer
broker希望接收到的消息的键和值都是字节数组。生产者接口允许使用参数化类型,因此可以把Java对象作为键和值发送给broker。这样的代码具有良好的可读性,不过生产者需要知道如何把这些Java对象转换成字节数组。key.serializer必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer 接口的类,生产者会使用这个类把键对象序列化成字节数组。Kafka 客户端默认提供了ByteArraySerializer(这个只做很少的事情)、StringSerializer和IntegerSerializer,因此,如果你只使用常见的几种Java对象类型,那么就没必要实现自己的序列化器。要注意,key.serializer是必须设置的,就算你打算只发送值内容。
value.serializer
与key.serializer一样,value.serializer指定的类会将值序列化。如果键和值都是字符串,可以使用与key.serializer一样的序列化器。如果键是整数类型而值是字符串,那么需要使用不同的序列化器。
下面我们来看看完整的生产者代码:
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class MyProducer {
public static void main(String[] args) {
//创建Kafka生产者的配置信息
Properties properties = new Properties();
//指定连接的Kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop108:9092");
//ACK应答级别
properties.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数
properties.put("retries", 1);
//批次大小 单位:字节
properties.put("batch.size", 16384);
//等待时间
properties.put("linger.ms", 1);
//RecordAccumulator缓冲区大小
properties.put("buffer.memory", 33554432); //32M
//Key Value的序列化类
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 = 1 ; i < 5 ; i++){
try {
producer.send(new ProducerRecord<String,String>("first","ly",""+i));
} catch (Exception e) {
e.printStackTrace();
}
}
//关闭资源
producer.close();
}
}
下面我们启动hadoop108的zookeeper和kafka服务
开启消费者进程,同时执行上述的程序代码:
自定义分区器:
Kafka的消息是一个个键值对,ProducerRecord对象可以只包含目标主题和值,键的默认值为null,不过大多数应用程序会用到键。键有两个用途:可以作为消息的附加消息,也可以用来决定消息该被写到哪个主题的分区。拥有相同键的消息将被写到同一个分区。
比如:
ProducerRecord<String,String> record = new ProducerRecord<("CustomerCountry","Laboratory","USA");
如果我们创建键为null的消息,不指定键就可以了
ProducerRecord<String,String> record = new ProducerRecord<("CustomerCountry","USA");
下面为上面两种方式对应的源码
/**
* Create a record to be sent to Kafka
*
* @param topic The topic the record will be appended to
* @param key The key that will be included in the record
* @param value The record contents
*/
public ProducerRecord(String topic, K key, V value) {
this(topic, null, null, key, value, null);
}
/**
* Create a record with no key
*
* @param topic The topic this record should be sent to
* @param value The record contents
*/
public ProducerRecord(String topic, V value) {
this(topic, null, null, null, value, null);
}
分区源码剖析:
如果key为null,则按照一种轮询的方式来计算分区分配:第一次调用时,获得一个数,将他存入topicCounterMap中,下次调用时,取出该数,并counter.getAndIncrement(),这样每次获得的数都不一样。
如果key不为null,则使用Hash算法:murmur的hash算法:非加密型Hash函数,具备高运算性能及低碰撞率,减少重复碰撞 来计算分区分配;key的hash值取模,除以分区数量,取余数
public class DefaultPartitioner implements Partitioner {
private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
public void configure(Map<String, ?> configs) {}
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes serialized key to partition on (or null if no key)
* @param value The value to partition on or null
* @param valueBytes serialized value to partition on or null
* @param cluster The current cluster metadata
*/
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 = nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private int nextValue(String topic) {
AtomicInteger counter = topicCounterMap.get(topic);
if (null == counter) {
counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
if (currentCounter != null) {
counter = currentCounter;
}
}
return counter.getAndIncrement();
}
public void close() {}
}
只有在不改变主题分区数量的情况下,键对分区的哈希映射保持不变。但是如果使用键来映射分区,那么最好在创建主题的时候把分区规划好,不要增加新分区
下面来实现自定义分区器:
首先我们要实现自定义分区器就要实现Partitioner接口,接口中会有三个方法,模拟默认分区器的代码,我们只需要在partition方法中编写自己的逻辑代码就行了,这里我们直接简单的返回一个分区号(不管key的值是什么,都会返回1号分区)
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] ValueBytes, Cluster cluster) {
return 1;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
然后在程序中要指定分区器:
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.ly.partitioner.MyPartitioner");
好了,生产者的部分知识就介绍完毕了!喜欢的点个👍!