[000-01-020].第08节:Kafka生产者工作原理

我的后端大纲

我的Kafka大纲


在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.serializervalue.serializer指定发送消息的 key 和 value 的序列化类型。一定要写全类名。
buffer.memoryRecordAccumulator 缓冲区总大小,默认 32m
batch.size缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。
linger.ms如果数据迟迟未达到 batch.size,sender 等待 linger.time之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间
acks0:生产者发送过来的数据,不需要等数据落盘应答。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.memoryRecordAccumulator 缓冲区总大小,默认 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中查看是否打印回调日志内容

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的数据都是有序的。
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值