在Kafka生产者做的所有事情总结一句话就是:想各种办法把手里的数据发送到Kafka集群
1.生产者消息发送原理:
1.1.发送原理解释:
生产者在消息发送的过程中,涉及到了
两
个线程—main 线程
和Sender 线程
-
1.首先生产者创建一个main线程,然后创建了一个Producer对象,调用send()方法后将数据传送到了拦截器,在拦截器位置,我们可以实现对数据进行一些加工处理
-
2.数据继续往下传,然后数据就来到了序列化器Serialzer,Kafka有自己的序列化器,不使用Java的序列化器,其原因是Java的序列化器太重,在大数据量下出现效率低情况
-
3.再往后就来到了分区器,在这里决定了数据应该发送到哪个分区。一个分区就会创建一个队列,分区器来判断过来的数据应该放在哪个队列中,队列的创建都是在内存中完成的,内存大小总和是32M,每个批次的大小默认是16K
-
4.然后有个sender线程,它是主动来读取主线程中缓冲队列的内容发往Kafka集群,它读取数据发送的规则是当内个批次的
数据达到16k的时候
,就会调用send线程发送数据,或者当设置的等待时长达到
之后,就调用send线程发送数据
-
5.然后再往下就是:send线程拉取数据,send线程是以每个broke为key,如以brok1为key、以broke2为key,然后后面就跟上一波一波的请求,他会把数据放在一个队列中传送到某个broke。
-
6.Selector是打通数据传送的链路
1.2.发送原理的流程图-完整版
1.3.生产者重要参数列表:
参数名称 | 描述 |
---|---|
bootstrap.servers | 生产者连接集群所需的 broker 地 址 清 单 。 例 如hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置 1 个或者多个,中间用逗号隔开。注意这里并非 需要所有的 broker 地址,因为生产者从给定的 broker里查找到其他 broker 信息。 |
key.serializer 和 value.serializer | 指定发送消息的 key 和 value 的序列化类型。一定要写全类名。 |
buffer.memory | RecordAccumulator 缓冲区总大小,默认 32m |
batch.size | 缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。 |
linger.ms | 如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间 |
acks | 0:生产者发送过来的数据,不需要等数据落盘应答。1:生产者发送过来的数据,Leader 收到数据后应答。-1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和all 是等价的。 |
max.in.flight.requests.per.connection | 允许最多没有返回 ack 的次数,默认为 5,开启幂等性要保证该值是 1-5 的数字。 |
retries | 当消息发送出现错误的时候,系统会重发消息。retries表示重试次数。默认是 int 最大值,2147483647。如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1否则在重试此失败消息的时候,其他的消息可能发送成功了 |
retry.backoff.ms | 两次重试之间的时间间隔,默认是 100ms。 |
buffer.memory | RecordAccumulator 缓冲区总大小,默认 32m。 |
enable.idempotence | 是否开启幂等性,默认 true,开启幂等性。 |
compression.type | 生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。支持压缩类型:none、gzip、snappy、lz4 和 zstd |
2.消息的异步发送API:
2.1.需求说明:
- 1.需求:创建 Kafka 生产者,采用异步的方式发送数据到到 Kafka Broker
2.2.异步发送的流程:
- 1.所谓的异步发送是指
将外部的数据发送到队列中,不管队列中的数据有没有发送到kafka集群,main线程会把数据一批的一批的发送到队列中
2.3.编码实现异步发送:
a.创建工程 kafka
b.导入依赖:
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
c.测试无回调功能的异步发送:
package com.qun.kafka.producer;
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;
public class CustomProducer {
public static void main(String[] args) {
//1.属性配置
Properties properties = new Properties();
//连接到Kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop103:9092,hadoop104:9092,hadoop105:9092");
//指定对应的key和value的序列化类型
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
//1.创建生产者对象
KafkaProducer<String, String> KafkaProducer = new KafkaProducer<>(properties);//first是代表的发送到的主题
//2.发送数据
for (int i = 0; i < 5; i++) {
KafkaProducer.send(new ProducerRecord<>("first","jianqun" + i));
}
//3.关闭资源
KafkaProducer.close();
}
}
d.带回调函数的异步发送流程:
- 1.带回调函数的异步发送就是指的数据发送后会返回数据所在的队列、分区等信息。这里返回的数据是指数据发送到对列之后,对列自动返回的数据
- 2.回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),
如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败
- 3.编码实现:
package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class CustomProducerCallback {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// key,value 序列化(必须):key.serializer,value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// 添加回调
kafkaProducer.send(new ProducerRecord<>("first","atguigu " + i), new Callback() {//该方法在Producer收到ack时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata,Exception exception) {
if (exception == null) {
// 没有异常,输出信息到控制台
System.out.println(" 主题: " + metadata.topic() + "->" + "分区:" + metadata.partition());
} else {
// 出现异常打印
exception.printStackTrace();
}
}
}
);
// 延迟一会会看到数据发往不同分区
Thread.sleep(2);
}
// 5. 关闭资源
kafkaProducer.close();
}
}
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试
- 4.代码测试:
- 1.在服务器上的消费者中观察是否收到生产者发送的消息:
- 2.在IDEA中查看是否打印回调日志内容
- 1.在服务器上的消费者中观察是否收到生产者发送的消息:
3.同步发送API:
- 1.只需在异步发送的基础上,再调用一下 get()方法即可
4.生产者分区:
4.1.分区器好处
- 1.
分区便于合理使用存储资源
:每个Partition在一个Broker上存储,可能导致存储不了,所以就可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡
的效果
- 2.
分区可以提高并行度
:生产者可以以分区为单位发送数据
;消费者可以以分区为单位进行消费数据
4.2.消息发送的分区策略:
a.查看默认的分区器 DefaultPartitioner
- 1.源码查看:在 IDEA 中 ctrl +n,全局查找
DefaultPartitioner
- 2.如下可以英文翻译过来的分区策略:
- 如过指定了分区就按照指定的分区来使用
- 如果未指定分区但是有key,就按照key的hash值对分区数来取模,这样就知道数据发送到哪个分区上了
- 如果没有指定分区也没有指定key,就选择粘性分区,等这一批次分区满了之后,就可以再选择别的分区了
- 3.在Kafka中的ProducerRecord类中各构造方法也可以分析出分区策略:
b.案例1:
- 1.将数据发往指定 partition 的情况下,例如将所有数据发往分区 2中
c.案例2:
- 1.没有指明 partition 值但
有 key 的
情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值
d.面试题:
在数据库中有很多数据表,然后我们
想把订单数据都发送到固定的某个分区来进行处理
,请问我们应该怎么实现??
- 1.我们可以在生产者发送数据的时候,以数据库中订单表的表名来作为数据发送时的key,这样在使用
Hash值%f分数数进行取模
的时候必定是固定的某个分区,这样就可以实现了把订单数据以某个固定的分区来处理了。
4.3.自定义分区器:
研发人员可以根据企业需求,自己重新实现分区器
a.需求说明:
- 1.需求:
例如我们实现一个分区器实现,发送过来的数据中如果包含 atguigu,就发往 0 号分区,不包含 atguigu,就发往 1 号分区
b.实现步骤:
- 定义类实现 Partitioner 接口
- 重写 partition()方法
c.编码实现:
-
1.定义分区器:
-
2.补充需求中的业务逻辑:
package com.atguigu.kafka.producer; 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) { // 获取数据 atguigu hello String msgValues = value.toString(); int partition; if (msgValues.contains("atguigu")){ partition = 0; }else { partition = 1; } return partition; } @Override public void close() { } @Override public void configure(Map<String, ?> configs) { } }
-
3.将自定义分区器与生产者产生关系,将生产者与自定义分区器关联:
package com.atguigu.kafka.producer; import org.apache.kafka.clients.producer.*; import org.apache.kafka.common.serialization.StringSerializer; import java.util.Properties; public class CustomProducerCallbackPartitions { public static void main(String[] args) throws InterruptedException { // 0 配置 Properties properties = new Properties(); // 连接集群 bootstrap.servers properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092"); // 指定对应的key和value的序列化类型 key.serializer // properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName()); // 关联自定义分区器 properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.atguigu.kafka.producer.MyPartitioner"); // 1 创建kafka生产者对象 // "" hello KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties); // 2 发送数据 for (int i = 0; i < 5; i++) { kafkaProducer.send(new ProducerRecord<>("first", 1,"","hello" + i), new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (exception == null){ System.out.println("主题: "+metadata.topic() + " 分区: "+ metadata.partition()); } } }); Thread.sleep(2); } // 3 关闭资源 kafkaProducer.close(); } }
5.生产经验:
5.1.生产者如何提高吞吐量:
a.方式方法:
- 1.默认kafka是对数据来一条传送一条
- 2.为了提高数据传输效率:
我们可以使用以下方式来实现
:
b.编码实现:
- 1.在编码时候根据业务情况配置了默认批次大小、等待时间、数据压缩类型、缓冲区大小的限制
package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class CustomProducerParameters {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息:bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key,value 序列化(必须):key.serializer,value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
// RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);//33554432代表32M
// batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);// 16384代表16K
// linger.ms:等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);//1代表1毫秒
// compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");//snappy代表压缩类型
// 1. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 2. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new
ProducerRecord<>("first","atguigu " + i));
}
// 3. 关闭资源
kafkaProducer.close();
}
}
c.测试:
- 1.在 hadoop102 上开启 Kafka 消费者:
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
- 2.在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息:
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
5.2.生产环境中如何提高数据可靠性:
a.回顾发送流程:
b.对ack应答级别分析:
c.ack应答级别的可靠性总结:
- 1.
acks=0
,生产者发送过来数据就不管了,可靠性差,效率高; - 2.
acks=1
,生产者发送过来数据Leader应答,可靠性中等,效率中等
; - 3.
acks=-1
,生产者发送过来数据Leader和ISR队列里面所有Follwer应答
,可靠性高,效率低;
在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据,对可靠性要求比较高的场景
d.代码实现acks应答级别设置:
e.数据重复分析:
- 1.当leader收到发过来的hello数据的时候,已经同步给了所有的fowler,但是在将要未回复的时候,此时挂掉了,那么就会再选择一个follower作为leader,然后生产者此前未收到回复的消息会再一次发送给leadr,然后leader再一次进行消息的同步,那么就会导致了消息的重复现象出现;后面会对数据重复的情况进行解决
5.3.生产环境中数据如何去重:
a.数据传递语义:
- 1.
至少一次
(就是生产者发送数据到kafka集群至少收到一次数据):ACK级别设置为-1
+分区副本大于等于2
+ISR里应答的最小副本数量大于等于2
;至少一次可以保证数据不丢失,但是不能保证数据不重复; - 2.
最多一次
(就是生产者发送数据到kafka集群最多只能发一次数据):ACK级别设置为0
;最多一次可以保证数据不重复,但是不能保证数据不丢失。 - 3.
精确一次
(只能发一次,不能多发也不能少发):对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失
。在Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务
b.什么是幂等性:
- 1.幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都
只会持久化一条
,保证了不重复。 - 2.精确一次(Exactly Once) =
幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2)
- 3.幂等性重复数据的判断标准:具有
<PID, Partition, SeqNumber>
相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的
;Partition 表示分区号;Sequence Number是单调自增的。
所以幂等性只能保证的是在单分区单会话内不重复
- 4.配置开启幂等性:开启参数
enable.idempotence
默认为 true,false 关闭
c.生产者事务:
c1.Kafka 事务原理:
c2.Kafka事务的5个API:
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
c3.单个 Producer,使用事务保证消息的仅一次发送,实现消息去重
package com.atguigu.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class CustomProducerTransactions {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key,value 序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 设置事务 id(必须),事务 id 任意起名
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_0");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 初始化事务
kafkaProducer.initTransactions();
// 开启事务
kafkaProducer.beginTransaction();
try {
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// 发送消息
kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i));
}
// int i = 1 / 0; //模拟数据发送失败,验证事务生效与否
// 提交事务
kafkaProducer.commitTransaction();
} catch (Exception e) {
// 终止事务
kafkaProducer.abortTransaction();
} finally {
// 5. 关闭资源
kafkaProducer.close();
}
}
}
5.4.生产环境中如何确保数据有序:
a.什么是数据有序:
- 1.这里的有序指的是
在多个队列之间接收的数据是否需要有序
,如果需要队列间接收数据有序,需要在消费者收到数据后进行排列
,如可以使用Spark进行排列
b.数据乱序问题:
这里的乱序问题是指的
在一个队列中的数据顺序问题
b1.乱序问题解决:
- 1.kafka在1.x版本之前保证数据单分区有序,条件如下:
max.in.flight.requests.per.connection=1
(不需要考虑是否开启幂等性) - 2.kafka在1.x及以后版本保证数据单分区有序,条件如下:
- 未开启幂等性:
max.in.flight.requests.per.connection
需要设置为1 - 开启幂等性:
max.in.flight.requests.per.connection
需要设置小于等于5
- 未开启幂等性:
b2.开启幂等性再设置解决乱序问题的原因:
- 1.原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。