Kafka3.x学习笔记(二)——生产者

Kafka3.x学习笔记(二)——生产者

目录

  • Kafka3.x学习笔记(二)——生产者
    • 对应课程
    • 生产者发送原理
    • 发送数据的JavaAPI
      • 异步发送
      • 异步发送回调
      • 同步发送
    • 分区
      • 意义
      • 规则
    • 提升数据的吞吐量
    • 发送数据的可靠性
    • 数据重复问题和去重
      • 情景分析
      • 幂等性原理
      • 生产者事务
    • 解决数据乱序问题

对应课程

【尚硅谷】2022版Kafka3.x教程(从入门到调优,深入全面)

生产者发送原理

在消息发送的过程中,涉及到了两个线程——main线程Sender线程。在main线程中创建一个双端队列RecordAccumulator。main线程负责将消息发送给RecordAccumulatorsender线程则不断地从RecordAccumulator中拉取消息发送到Kafka Broker

Kafka3.x学习笔记(二)——生产者_第1张图片

缓冲区RecordAccumulator默认的大小为32M,触发sender线程的条件有两种:一种是数据达到batch.size时,或者时间到达linger.ms时。相应地,在生产中,为了提高数据吞吐量有以下方案:1)修改缓存的大小buffer.memory为64M;2)增加batch.size的大小为32K;3)修改linger.ms为5ms~100ms左右;4)添加数据压缩方式。

max.in.flight.requests.per.connection属性设置了允许最多没有返回ACK的Request的个数,若开启了幂等性,则需要小于等于5。当Request发送出错时,retries参数设置了重试的次数(默认为INT的最大值),retry.backoff.ms设置了两次重试的时间间隔(默认为100ms)。当收到Kafka集群的应答后,NetWorkClient就清理对应的Request,RecordAccumulator就清理对应的缓存数据。Producer发送数据流程结束。

其他参数:bootstrap.servers设置了生产者连接集群所需的broker地址清单,可以有一个或多个;key.serializer和value.serializer分别指定了发送消息的key和value的序列化类型;acks设置了生产者对Kafka集群的应答方式;enable.idempotence设置了是否开启幂等性,默认是true;compression.type设置了生产者发送的所有数据的压缩方式(包括gzip,snappy,lz4和zstd),默认为none。

发送数据的JavaAPI

前提:拥有一个包含节点hadoop101,hadoop102和hadoop103的Kafka集群并创造了一个主题名为first,分区数为3,副本因子为3的topic。

导入依赖:

<dependency>
    <groupId>org.apache.kafkagroupId>
    <artifactId>kafka-clientsartifactId>
    <version>3.0.0version>
dependency>

异步发送

CustomProducerAsync.java

package com.jd.springboot_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) {

        //0 配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        //2、发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first","msg_async"+i));
        }

        //3、关闭资源
        kafkaProducer.close();
    }
}

异步发送回调

Producer完成send()方法后,就可以执行之后的任务了,broker在收到消息后异步调用生产者提供的callback()方法。

CustomProducerCallback.java

package com.jd.springboot_kafka.producer;

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducerCallback {

    public static void main(String[] args) throws InterruptedException {
        //配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

        //2、发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<String, String>("first", "msg" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null) {
                        System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
                    }
                }
            });
            System.out.printf("之后的任务..."+i);
            Thread.sleep(1);
        }

        //3、关闭资源
        kafkaProducer.close();
    }
}

同步发送

CustomProducerSync.java

package com.jd.springboot_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;
import java.util.concurrent.ExecutionException;

public class CustomProducerSync {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        //2、发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first","msg_sync"+i)).get();
        }

        //3、关闭资源
        kafkaProducer.close();
    }
}

分区

意义

1)从存储上看,便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。

2)从计算上看,提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。

规则

1)若指定了生产者发送的分区,就按照指定的分区执行:

//2、发送数据
for (int i = 0; i < 5; i++) {
    kafkaProducer.send(new ProducerRecord<String, String>("first",1,"", "msg" + i), new Callback() {
        @Override
        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
            if (e == null) {
                System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
            }
        }
    });
    Thread.sleep(20);
}

2)若没有指定分区,但发送的数据包含了key,就计算hashcode(key)%分区数,得到分区值:

//2、发送数据
for (int i = 0; i < 5; i++) {
    kafkaProducer.send(new ProducerRecord<String, String>("first","", "msg" + i), new Callback() {
        @Override
        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
            if (e == null) {
                System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
            }
        }
    });
    Thread.sleep(1);
}

3)若没有指定key,就随机分配分区,使用黏性方案,即消息层面的黏性,批次层面上的轮询

//2、发送数据
for (int i = 0; i < 5; i++) {
    kafkaProducer.send(new ProducerRecord<String, String>("first", "msg" + i), new Callback() {
        @Override
        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
            if (e == null) {
                System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
            }
        }
    });
    Thread.sleep(1);
}

4)自定义分区:将数据中包含"msg"字段的数据分至1区,其他的分至0区。

创造Partition接口实现类MyPartition.java:

package com.jd.springboot_kafka.patitioner;

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) {
        String valueString = value.toString();
        if (valueString.contains("msg")) {
            return 1;
        }
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

测试CustomProducerMyPartitioner.java:

package com.jd.springboot_kafka.producer;

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducerMyPartitioner {

    public static void main(String[] args) {
        //配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        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.jd.springboot_kafka.patitioner.MyPartitioner");
        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

        //2、发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<String, String>("first", "message" + i), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null) {
                        System.out.println("主题:"+recordMetadata.topic()+",分区:"+recordMetadata.partition());
                    }
                }
            });
        }

        //3、关闭资源
        kafkaProducer.close();
    }
}

提升数据的吞吐量

在生产中,为了提高数据吞吐量有以下方案:1)修改缓存的大小buffer.memory为64M;2)增加batch.size的大小为32K;3)修改linger.ms为5ms~100ms左右;4)添加数据压缩方式。

CustomProducerParameter.java

package com.jd.springboot_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 CustomProducerParameter {

    public static void main(String[] args) {
        // 配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

        //配置缓冲区大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);

        //配置批次大小
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);

        //配置等待时间
        properties.put(ProducerConfig.LINGER_MS_CONFIG,1);

        //配置压缩类型
        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

        //1 建立生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
        //2 发送数据
        for (int i = 0; i < 5; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<String,String>("first","atguigu"+i);
            kafkaProducer.send(record);
        }
        //3 关闭资源
        kafkaProducer.close();
    }
}

发送数据的可靠性

设置ACKS的级别为0,代表生产者发送过来的数据,不需要等数据落盘应答,容易丢数,效率高;

设置ACKS的级别为1(默认),代表生产者发送过来的数据,Leader收到数据后应答,有丢数的可能,即Leader发送完ack后,瞬间宕机,还未向Follower同步数据;

设置ACKS的级别为"ALL",代表生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。很安全

ACKS=1适合传输普通的日志,上万条中丢失1条影响不大,而ALL适合传输销售信息,和钱相关的情况。

数据完全可靠的条件:ACK的级别为ALL,且副本数大于等于2,ISR中应答的最小副本数量大于等于2。

CustomProducerACK.java

package com.jd.springboot_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 CustomProducerACK {

    public static void main(String[] args) {
        //配置参数
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //设置acks
        properties.put(ProducerConfig.ACKS_CONFIG,"all");

        //设置重试次数,默认无限次
        properties.put(ProducerConfig.RETRIES_CONFIG,3);

        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        //2、发送数据
        for (int i = 0; i < 5; i++) {
            kafkaProducer.send(new ProducerRecord<>("first","msg2"+i));
        }

        //3、关闭资源
        kafkaProducer.close();
    }
}

数据重复问题和去重

情景分析

Kafka3.x学习笔记(二)——生产者_第2张图片

幂等性原理

幂等性:指多接口的多次调用所产生的结果和只调用一次是一致的,没有幂等性就会重复发送数据。具体来说,就是Producer无论向Broker发送多少次重复的数据,Broker端都只会持久化其中一条,保证不重复。

重复数据判断的标准:具有相同主键的消息提交时,Broker只会持久化一条。其中,PID是一个Producer初始化时,由Kafka集群分配的唯一的ID;Partition 表示分区号;Sequence Number是对每个PID的Producer,它每发送一个的消息,都会对应一个从0单增的。

注意:幂等性只能保证的是在单分区单会话内不重复

开启参数enable.idempotence即可,默认为true。

生产者事务

1)Kafka事务原理

Kafka3.x学习笔记(二)——生产者_第3张图片

2)Kafka的事务相关的API:

// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map offsets,String consumerGroupId) throws ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;

3)使用事务,保证数据的不重复。注意:必须指定事务的id,它被事务持久化分区所需要。

package com.jd.springboot_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 CustomProducerTransaction {

    public static void main(String[] args) {
        //配置参数
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092,hadoop102:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 指定事务id
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_01");

        //1、创建Kafka生产者对象
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        // 初始化事务
        kafkaProducer.initTransactions();

        // 开启事务
        kafkaProducer.beginTransaction();

        try {
            //2、发送数据
            for (int i = 0; i < 5; i++) {
                kafkaProducer.send(new ProducerRecord<>("first","trans_msg"+i));
            }
            // 事务提交
            kafkaProducer.commitTransaction();
        } catch (Exception e) {
            // 事务回滚
            kafkaProducer.abortTransaction();
        } finally {
            //3、关闭资源
            kafkaProducer.close();
        }
    }
}

解决数据乱序问题

首先,要求数据有序是指单分区的有序,多分区的有序需要等数据拉取完成后,在服务端全部重排序。

1)kafka在1.x版本之前保证数据单分区有序,条件如下:

max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)。

2)kafka在1.x及以后版本保证数据单分区有序,条件如下:

(1)未开启幂等性:

max.in.flight.requests.per.connection=1

(2)开启幂等性

max.in.flight.requests.per.connection需要设置小于等于5。

原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。

Kafka3.x学习笔记(二)——生产者_第4张图片

你可能感兴趣的:(后端开发,学习,kafka,java)