java最简单的kafka生产者和消费者,未结合spring

目录

 

1 最简单的生产者和消费者

1.1 引入maven

1.2 基本的生产者和代码注释

1.3 最简单消费者

2 生产者发送消息的三种方式

2.1 直接send之后就不管了,会自动重试,可能丢失消息

2.2 同步非阻塞发送

2.3异步发送

3  KafkaProducer是线程安全的

3.1 多线程生产者-多线程安全

3.2 多线程消费者-多线程不安全

4 生产者常见配置

4.1 常用参数

4.2 参数详解

5 消费者常用参数

5.1 常用参数

5.2 参数详解

6 顺序保证

6.1 一个分区消息有序

6.2 保证顺序的参数配置


1 最简单的生产者和消费者

1.1 引入maven


    org.apache.kafka
    kafka-clients
    2.3.0

1.2 基本的生产者和代码注释

package cn.gg.hellokafka;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Properties;

/**
 * MyHelloProducer
 *
 * @author honry.guan
 * @date 2021-05-06 19:16
 */
public class MyHelloProducer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //配置连接ip和地址
        properties.put("bootstrap.servers","127.0.0.1:9092");
        //kafka自带序列化器,可以不用谢全类路径StringSerializer.class也可以,这里作为演示
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer producer = new KafkaProducer<>(properties);

        try {
            ProducerRecord producerRecord  = new ProducerRecord<>("my-hello","name","tom");
            producer.send(producerRecord);
        } finally {
            //关闭连接
            producer.close();
        }
    }
}

1.3 最简单消费者

package cn.gg.hellokafka;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

/**
 * MyHelloConsumer
 *
 * @author honry.guan
 * @date 2021-05-06 19:17
 */
public class MyHelloConsumer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //配置连接ip和地址
        properties.put("bootstrap.servers", "127.0.0.1:9092");
        //kafka自带反序列化器,可以不用谢全类路径StringDeserializer.class也可以,这里作为演示
        properties.put("key.deserializer", StringDeserializer.class);
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        //要指定一个群组,否则会报错
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");
        KafkaConsumer kafkaConsumer = new KafkaConsumer(properties);

        //订阅主题
        kafkaConsumer.subscribe(Collections.singleton("my-hello"));
        try {
            while (true) {
                //读取消息
                ConsumerRecords consumerRecords = kafkaConsumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord record : consumerRecords) {
                    System.out.println(String.format("topic:%s,分区:%d,偏移量:%d," + "key:%s,value:%s", record.topic(), record.partition(),
                            record.offset(), record.key(), record.value()));
                }

            }
        } finally {
            kafkaConsumer.close();
        }
    }
}

2 生产者发送消息的三种方式

2.1 直接send之后就不管了,会自动重试,可能丢失消息

ProducerRecord producerRecord  = new ProducerRecord<>("my-hello","name","tom");
//发送失败会重新发送,但是可能会丢失数据
producer.send(producerRecord);

2.2 同步非阻塞发送

获得send方法返回的Future对象,在合适的时候调用Future的get方法

Future future = producer.send(record);
                System.out.println("发送数据之后,先做其他操作");
//阻塞在这个位置
                RecordMetadata recordMetadata = future.get();
                if(null!=recordMetadata){
                    System.out.println("offset:"+recordMetadata.offset()+"-" +"partition:"+recordMetadata.partition());
                }

2.3异步发送

实现接口org.apache.kafka.clients.producer.Callback,然后将实现类的实例作为参数传递给send方法

producer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata,
                             Exception exception) {
        if(null!=exception){
            exception.printStackTrace();
        }
        if(null!=metadata){
            System.out.println("offset:"+metadata.offset()+"-"
                    +"partition:"+metadata.partition());
        }
    }
});

3  KafkaProducer是线程安全的

3.1 多线程生产者-多线程安全

可以使用同一个生产者发送消息,多个线程使用的同一个生产者

package cn.gg.concurrent;

import cn.enjoyedu.config.KafkaConst;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: honry.guan
 * @create: 2021-05-06 21:05
 **/
public class MyConProducer extends Thread {
    //自定义线程池
    static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    static CountDownLatch countDownLatch = new CountDownLatch(1000);
    static KafkaProducer producer = new KafkaProducer(KafkaConst.producerConfig(StringSerializer.class, StringSerializer.class));
    ProducerRecord record;

    public MyConProducer(ProducerRecord record) {
        this.record = record;
    }

    @Override
    public void run() {
        final String id = Thread.currentThread().getId()
                + "-" + System.identityHashCode(producer);
        try {
            //异步发送,
            producer.send(record, new Callback() {
                public void onCompletion(RecordMetadata metadata,
                                         Exception exception) {
                    if (null != exception) {
                        exception.printStackTrace();
                    }
                    if (null != metadata) {
                        System.out.println(id + "|" + String.format("偏移量:%s,分区:%s",metadata.offset(), metadata.partition()));
                    }
                }
            });
            countDownLatch.countDown();
            System.out.println(id + ":数据[" + record.toString() + "]已发送。");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            for (int i = 0; i < 1000; i++) {
                ProducerRecord record = new ProducerRecord("concurrent-test", null, i + "", i + "");
                executorService.submit(new MyConProducer(record));
            }
            countDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            producer.close();
            executorService.shutdown();
        }
    }
}

3.2 多线程消费者-多线程不安全

应该每个消费数据的线程拥有自己的 KafkaConsumer,每个线程都有属于自己的消费者
import cn.enjoyedu.config.BusiConst;
import cn.enjoyedu.config.KafkaConst;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: honry.guan
 * @create: 2021-05-06 21:22
 **/
public class MyConConsumer extends Thread {

    private static ExecutorService executorService = Executors.newFixedThreadPool(BusiConst.CONCURRENT_PARTITIONS_COUNT);
    /*消费配置的实例*/
    static Map config
            = KafkaConst.consumerConfigMap("concurrent",
            StringDeserializer.class,
            StringDeserializer.class);
    KafkaConsumer consumer;

    public MyConConsumer(KafkaConsumer consumer) {
        this.consumer = consumer;
        //订阅主题
        this.consumer.subscribe(Collections.singletonList("concurrent-test"));
    }

    public void run() {
        final String id = Thread.currentThread().getId() + "-" + System.identityHashCode(consumer);
        try {
            while (true) {
                ConsumerRecords records = consumer.poll(Duration.ofMillis(500));
                for (ConsumerRecord record : records) {
                    System.out.println(id + "|" + String.format("主题:%s,分区:%d,偏移量:%d," + "key:%s,value:%s",
                            record.topic(), record.partition(),
                            record.offset(), record.key(), record.value()));
                    //做其他的工作
                }
            }
        } finally {
            consumer.close();
        }
    }

    public static void main(String[] args) {
        //每个线程都有属于自己的消费者
        for (int i = 0; i < BusiConst.CONCURRENT_PARTITIONS_COUNT; i++) {
            executorService.submit(new MyConConsumer(new KafkaConsumer(config)));
        }
    }
}

 

4 生产者常见配置

4.1 常用参数

生产者有很多属性可以设置,大部分都有合理的默认值,无需调整。有些参数可能对内存使用,性能和可靠性方面有较大影响。
 
public class ConfigKafkaProducer {

    public static void main(String[] args) {
        //生产者三个属性必须指定(broker地址清单、key和value的序列化器)
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "127.0.0.1:9092");
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //更多发送配置(重要的)
        // ack 0,1,all
        properties.put("acks", "1");
        // 一个批次可以使用的内存大小 缺省16384(16k)
        properties.put("batch.size", 16384);
        // 指定了生产者在发送批次前等待更多消息加入批次的时间,  缺省0  50ms
        properties.put("linger.ms", 0L);
        // 控制生产者发送请求最大大小,默认1M (这个参数和Kafka主机的message.max.bytes 参数有关系)
        properties.put("max.request.size", 1 * 1024 * 1024);

        //非重要的
        // 生产者内存缓冲区大小
        properties.put("buffer.memory", 32 * 1024 * 1024L);
        //重发消息次数
        properties.put("retries", 0);
        //客户端将等待请求的响应的最大时间 默认30秒
        properties.put("request.timeout.ms", 30 * 1000);
        //最大阻塞时间,超过则抛出异常 缺省60000ms
        properties.put("max.block.ms", 60 * 1000);
        // 于压缩数据的压缩类型。默认是无压缩 ,none、gzip、snappy
        properties.put("compression.type", "none");
    }
}

4.2 参数详解

 
 
acks:指定了必须要有多少个分区副本收到消息,对消息丢失有重大影响
  • acks=0:生产者在写入消息之前不会等待任 何来自服务器的响应,容易丢消息,但是吞吐量高。
  • acks=1:只要集群的首领节点收到消息,生产者会收到来自服务器的成功响应。如果消息无法到达首领节点(比如首领节点崩溃,新首领没有选举出 来),生产者会收到一个错误响应,为了避免数据丢失,生产者会重发消息。不过,如果一个没有收到消息的节点成为新首领,消息还是会丢失。默认 使用这个配置。
  • acks=all:只有当所有参与复制的节点都收到消息,生产者才会收到一个来自服务器的成功响应。延迟高。
金融业务,主备外加异地灾备。所以很多高可用场景一般不是设置 2 个副本,有可能达到 5 个副本,不同机架上部署不同的副本,异地上也部署一 套副本。 buffer.memory 设置生产者内存缓冲区的大小(结合 生产者发送消息的基本流程 ),生产者用它缓冲要发送到服务器的消息。如果数据产生速度大于向 broker 发送 的速度,导致生产者空间不足, producer 会阻塞或者抛出异常。缺省 33554432 (32M)
max.block.ms:指定了在调用 send()方法或者使用 partitionsFor()方法获取元数据时生产者的阻塞时间。
当生产者的发送缓冲区已满,或者没有可用的元数据时,这些 方法就会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。缺省 60000ms
retries:发送失败时,指定生产者可以重发消息的次数(缺省 Integer.MAX_VALUE
默认情况下,生产者在每次重试之间等待 100ms,可以通过参数 retry.backoff.ms 参数来改变这个时间间隔。
receive.buffer.bytes 和send.buffer.bytes:指定 TCP socket 接受和发送数据包的缓存区大小。
如果它们被设置为 -1,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么 可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。缺省 102400
batch.size: 当多个消息被发送同一个分区时,生产者会把它们放在同一个批次里。
该参数指定了一个批次可以使用的内存大小,按照字节数计算。当批次内存 被填满后,批次里的所有消息会被发送出去。但是生产者不一定都会等到批次被填满才发送,半满甚至只包含一个消息的批次也有可能被发送。缺省 16384(16k) ,如果一条消息超过了批次的大小,会写不进去。
 
linger.ms: 指定了生产者在发送批次前等待更多消息加入批次的时间。
它和 batch.size 以先到者为先。也就是说,一旦我们获得消息的数量够 batch.size 的数量 了,他将会立即发送而不顾这项设置,然而如果我们获得消息字节数比 batch.size 设置要小的多,我们需要 “linger” 特定的时间以获取更多的消息。这个设
置默认为 0 ,即没有延迟。设定 linger.ms=5 ,例如,将会减少请求数目,但是同时会增加 5ms 的延迟,但也会提升消息的吞吐量。 compression.type producer 用于压缩数据的压缩类型。默认是无压缩。正确的选项值是 none gzip snappy 。压缩最好用于批量处理,批量处理消息越多,压缩性能越
好。 snappy 占用 cpu 少,提供较好的性能和可观的压缩比,如果比较关注性能和网络带宽,用这个。如果带宽紧张,用 gzip ,会占用较多的 cpu ,但提供
更高的压缩比。
client.id:当向 server 发出请求时,这个字符串会发送给 server
目的是能够追踪请求源头,以此来允许 ip/port 许可列表之外的一些应用可以发送信息。这项 应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪。
max.in.flight.requests.per.connection
指定了生产者在接收到服务器响应之前可以发送多个消息,值越高,占用的内存越大,当然也可以提升吞吐量。发生错误时,可能会造成数据的发 送顺序改变 , 默认是 5 ( 修改)。
如果需要保证消息在一个分区上的严格顺序,这个值应该设为 1 。不过这样会严重影响生产者的吞吐量。
request.timeout.ms
客户端将等待请求的响应的最大时间 , 如果在这个时间内没有收到响应,客户端将重发请求 ; 超过重试次数将抛异常,默认 30 秒。
metadata.fetch.timeout.ms
是指我们所获取的一些元数据的第一个时间数据。元数据包含: topic host partitions 。此项配置是指当等待元数据 fetch 成功完成所需要的时间, 否则会跑出异常给客户端
max.request.size
控制生产者发送请求最大大小。默认这个值为 1M ,如果一个请求里只有一个消息,那这个消息不能大于 1M,如果一次请求是一个批次,该批次包 含了 1000 条消息,那么每个消息不能大于 1KB 。注意: broker 具有自己对消息记录尺寸的覆盖,如果这个尺寸小于生产者的这个设置,会导致消息被拒
绝。这个参数和 Kafka 主机的 message.max.bytes 参数有关系。如果生产者发送的消息超过 message.max.bytes 设置的大小,就会被 Kafka 服务器拒绝。

5 消费者常用参数

5.1 常用参数

public class ConfigKafkaConsumer {

    public static void main(String[] args) {
        //消费者三个属性必须指定(broker地址清单、key和value的反序列化器)
        Properties properties = new Properties();
        properties.put("bootstrap.servers","127.0.0.1:9092");
        properties.put("key.deserializer", StringDeserializer.class);
        properties.put("value.deserializer", StringDeserializer.class);
        //群组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test1");

        //更多消费者配置(重要的)
        properties.put("auto.offset.reset","latest"); //消费者在读取一个没有偏移量的分区或者偏移量无效的情况下,如何处理
        properties.put("enable.auto.commit",true); // 表明消费者是否自动提交偏移 默认值true
        properties.put("max.poll.records",500); // 控制每次poll方法返回的的记录数量 默认值500
        //分区分配给消费者的策略。系统提供两种策略。默认为Range
        properties.put("partition.assignment.strategy",Collections.singletonList(RangeAssignor.class));
        KafkaConsumer consumer = new KafkaConsumer(properties);

    }
}

5.2 参数详解

 
auto.offset.reset
消费者在读取一个没有偏移量的分区或者偏移量无效的情况下,如何处理。
  • 默认值是 latest,从最新的记录开始读取,
  • 另一个值是 earliest,表示消费者从起始位置读取分区的记录。
enable .auto.commit 默认值 true,表明消费者是否自动提交偏移。为了尽量避免重复数据和数据丢失,可以改为 false,自行控制何时提交。
partition.assignment.strategy
分区分配给消费者的策略。系统提供两种策略。默认为 Range 。允许自定义策略。
  1. Range 把主题的连续分区分配给消费者。(如果分区数量无法被消费者整除、第一个消费者会分到更多分区)
  2. RoundRobin 把主题的分区循环分配给消费者。
  3. 自定义策略
extends AbstractPartitionAssignor ,然后在消费者端增加参数:
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, .class.getName()); 即可。
max.poll.records 控制每次 poll 方法返回的的记录数量。
fetch.min.bytes
每次 fetch 请求时, server 应该返回的最小字节数。如果没有足够的数据返回,请求会等待,直到足够的数据才会返回。缺省为 1 个字节。多消费者 下,可以设大这个值,以降低 broker 的工作负载
fetch.wait.max.ms
如果没有足够的数据能够满足 fetch.min.bytes ,则此项配置是指在应答 fetch 请求之前, server 会阻塞的最大时间。缺省为 500 个毫秒。和上面的 fetch.min.bytes 结合起来,要么满足数据的大小,要么满足时间,就看哪个条件先满足。
max.partition.fetch.bytes
指定了服务器从每个分区里返回给消费者的最大字节数,默认 1MB 。假设一个主题有 20 个分区和 5 个消费者,那么每个消费者至少要有 4MB 的可
用内存来接收记录,而且一旦有消费者崩溃,这个内存还需更大。注意,这个参数要比服务器的 message.max.bytes 更大,否则消费者可能无法读取消息。
session.timeout.ms
如果 consumer 在这段时间内没有发送心跳信息,则它会被认为挂掉了。默认 3 秒。
client.id
当向 server 发出请求时,这个字符串会发送给 server 。目的是能够追踪请求源头,以此来允许 ip/port 许可列表之外的一些应用可以发送信息。这项
应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪。
receive.buffer.bytes send.buffer.bytes
指定 TCP socket 接受和发送数据包的缓存区大小。如果它们被设置为 -1 ,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么
可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。

6 顺序保证

6.1 一个分区消息有序

Kafka 可以保证同一个分区里的消息是有序的;即主题只有一个分区,写入数据是顺序的,消费数据就是顺序的。

6.2 保证顺序的参数配置

  • 如果设置retires >0,把最大连接数设置max.in.flight.requests.per.connection>1,如果顺序发送1,2两个消息,结果消息1失败,消息2成功,重新发送消息1成功,这个时候1和2的顺序就反过来了
  • 如果设置retires=0,消息可能会丢失
  • 最好设置retires >0,把最大连接数设置max.in.flight.requests.per.connection=1:同时只有一个生成者操作主题,故而可以保证顺序

 

 

你可能感兴趣的:(kafka,kafka)