Kafka入门

参考图书kafka权威指南

Kafka介绍

kafka是一种消息队列,通过消息发布和订阅模式实现。为了方便,消息的发布在之后会称为消息生产者,消息订阅会称为消息消费者。

消费者订阅主题,生产者发布消息到主题下,消费者收到生产者发布的消息。

主题和分区

主题又称为Topic,每个主题下面有若干个分区。消费者订阅一个或多个分区,通过偏移量读取消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NFxUXZBK-1599092696304)(C:\Users\pxy\AppData\Roaming\Typora\typora-user-images\image-20200831144755572.png)]

Maven依赖

	<dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka_2.13</artifactId>
      <version>2.6.0</version>
    </dependency>

生产者

简单的例子

        Properties props = new Properties();
        props.put("bootstrap.servers","host:port");
        //设置key和value序列化方式
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        int i = 1;
        // 发送业务消息
        // 读取文件 读取内存数据库 读socket端口
        while (true) {
            Thread.sleep(1000);
            producer.send(new ProducerRecord<String, String>("sun", "key:" + i, "value:" + i));
            System.out.println("key:" + i + " " + "value:" + i);
            i++;
        }

:使用时将第二行的host:port改成kafka在的IP地址和端口号

使用produce.send来发送消息。ProducerRecord构造器属性(主题名,key,value), key、value相当于Map的键值对。

配置属性

上面的简单的例子中,只配置了key,value的序列化方法

1.ack:多少分区收到消息为发送成功。

ack=0:生产者写入就生成,无需要等待服务器的响应。

ack=1:集群的首领节点收到消息,生产者就会受到来自服务器的响应。

ack=all:所有节点收到消息时,生产者才会受到来自服务器的响应。

2.buffer.memory:缓冲区的大小。

max.block.ms:当消息发送速度大于发送到服务器的速度,会导致空间不足,这个时候调用send就会被阻塞或者抛出异常,这个参数为抛出异常前阻塞多 久。

3.compression.type:压缩算法,占用一部分CPU压缩发送的信息,降低网络带宽和提高性能。压缩算法:snappy、gzip和lz4,其中gzip压缩占用的CPU最大,但对网络带宽的占用和性能的提升也是最大的。

4.retries:消息发送失败重试次数,重试次数到了就抛出异常。

retry.backoff.ms:重试时间间隔,默认是100ms,建议测试一下服务器奔溃后恢复时间再设置间隔,避免还没回复就重试次数用尽。

5.batch.size:当消息需要发送给同一个分区时,会放在同一个批次里,按字节数计算,当这个批次占满后会发送。但并不一定需要全部满后才会发送,所以不会影响发送的延迟。设置太大会占用过多空间,设置太小会频繁发送增加不必要的开销。

6.linger.ms:发送延迟,当延迟到时或者发送批次被填满后发送消息。

7.client.id:该参数可以是任意字符串,服务器会用他来识别消息的来源,还可以用在日志和配额指标里。

8.max.in.flight.requests.per.connection:生产者在接受到服务器响应之前可以发送多少消息,值越大吞吐量和占用内存越大。当值为1时就是按照顺序写入。

9.timeout.ms、request.timeout.ms和metadata.fetch.timeout.ms

request.timeout.ms:生产者在发送消息是等待服务器返回响应时间。

metadata.fetch.timeout.ms:生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间。

​ 要是超时会根据retries进行重试或抛出异常

timeous.ms:指定了broker等待同步副本返回消息确认的时间,预asks的配置相匹配,如果在指定时间内没有收到同步副本的确认,那么broker就会返回一 个错误。

10.max.request.size:控制生产者发送请求的大小。他可以指发送单个消息的最大值,也可以指单个请求里所有消息总的大小。

11.receive.buffer.bytes和send.buffer.bytes:指TCP socket接收和发送数据包的缓冲区大小。路过他们被设置为-1,就使用操作系统的默认值。如果生产者和消费者与broker处于不同的数据中介,那么可以适当增加这些值,因为跨数据中心的网络一般都比较高的延迟和比较低的带宽。

分区

通过前文简单的例子,知道构建ProducerRecord对象的时候需要三个参数,分别是Topic,key和value。Topic决定将消息发送到哪个主题,key决定将消息发送到哪个分区。一般在默认没有设置的情况下,Kafka会对Key进行散列存储。但有些时候并不能这样做,有些时候一个分区就需要存放一样东西的key,比如电话号码,这种时候就可以使用自定义分区。

自定义分区

首先使用

./kafka-topics.sh --bootstrap-server localhost:9092 --topic 主题名字 --describe

查看主题下有多少分区

如果只有一个分区那自定义分区就没什么意义,因为他都是发到一个分区中去的。这种时候就要手动修改分区数量

./kafka-topics.sh --bootstrap-server localhost:9092 --topic 主题名字 --alter  --partitions 分区数量
自定义分区实现

自定义分区类,实现Partitioner类,重写partition方法。

当key为111时放到同一个,也就是最后一个分区中,其他的散列存储到其他分区

public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //等到topic中的分区信息
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int size = partitions.size();
        //如果是111放到同一个分区中
        if("111".equals(key)){
            return size-1;
        }
        return ((String)key).substring(0, 3).hashCode() % (size - 1);
    }

    @Override
    public void close() {

    }

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

    }
}

修改生产者,进行测试

 public void testPrdouct03() throws InterruptedException {
        String ss[]=new String[]{"123","111","101"};

        Properties props = new Properties();
        props.put("bootstrap.servers","58.247.129.42:9092");
        //设置分区器
        props.put("partitioner.class","org.example.config.MyPartitioner");
        //设置key和value序列化方式
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");//key序列化器
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");//value序列化器
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        // 发送业务消息
        for(String s:ss){
            try {
                RecordMetadata sun = producer.send(new ProducerRecord<String, String>("sun", s, "value:" + s)).get();
                System.out.println(s+" has been send to partition "+sun.partition());
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

消费者

简单的例子

		Properties props = new Properties();
        props.put("bootstrap.servers","host:port");
        props.put("group.id", "1111"); //定义消费组

        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList("sun"));//订阅主题

        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMinutes(1));
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
        }

:使用时将第二行的host:port改成kafka在的IP地址和端口号

使用consumer.poll等待延迟获取,里面是时间。

配置属性

  • fetch.min.bytes:服务器收到消费者请求消息的请求,当消息字节数加起来大于这个值的时候才会返回消息。
  • fetch.max.wait.ms:消息最长等待时间,比如设置成100ms,那么到100ms时就算没有达到最小字节数也会返回消息。
  • max.partition.fetch.bytes:默认是1MB,当消费者在poll消息的时候,最大不会过这个值。
  • session.timeout.ms:默认3s,消费者在认定死亡之前可以与服务器断开连接的时间。如果在这时间内,没有发送心跳给协调器,协调器就会触发再均衡,把他的分区分配给群组里的其他消费者。
    • heartbeat.interval.ms:像服务器发送心跳的时间,如果session.timeout.ms设置成3s,这个就要设置成1s比较合理。
  • auto.offset.reset:[latest,earliest],默认值为latest。指当分区偏移量失效时,默认是从最新的开始读取,而另一个是从起始位置开始读。
  • enable.auto.commit:提交偏移量的方式,默认为true。
    • auto.commit.interval.ms:消费者自动提交偏移量的时间间隔,默认为5秒。
  • partition.assignment.strategy:分区分配给消费者的策略,Kafka自带的2种org.apache.kafka.clients.consumer.RangeAssignor和org.apache.kafka.clients.consumer.RoundRobinAssignor。分别为Range策略,指分配主题的连续空间给消费者;Round策略,分配平均的空间给消费者。也可以自定义分配方式。
  • client.id:表示从客户端发过来的消息,通常被用在日志、度量指标和配额里。
  • max.poll.records:单次调用poll方法能够返回的最大数量。
  • receive.buffer.bytes和send.buffer.bytes:指TCP socket接收和发送数据包的缓冲区大小。路过他们被设置为-1,就使用操作系统的默认值。如果生产者和消费者与broker处于不同的数据中介,那么可以适当增加这些值,因为跨数据中心的网络一般都比较高的延迟和比较低的带宽。

从指定分区读数据

将subscribe用assign代替

        //consumer.subscribe(Arrays.asList("sun"));
        consumer.assign(Arrays.asList(new TopicPartition("sun",1)));

提交偏移量

经过测试不知道是IDEA编译器的问题还是新版本的问题,提交的偏移量都是在程序再次启动的时候才从那个点开始重新读

大部分开发者提交偏移量的方法是将auto.commit.interval.ms的时间间隔缩短,这种方法可以减少偏移量丢失的可能性,同时在再均衡的时候减少重复数据的数量。但有些时候还是要手动提交偏移量的,而Kafka也提供了手动提交偏移量的方法。

使用commitSync()提交偏移量
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("consumer02,offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            try {
                consumer.commitSync();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

在每次接收到数据后提交偏移量。

使用commitAsync()异步提交偏移量

手动提交有一个不足就是消费者在对提交偏移量,等待回复之前会一直阻塞,会降低吞吐量。Kafka也提供了异步提交请求的方法commitAsync();

		while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("consumer02,offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            consumer.commitAsync();
        }

同步提交会在提交成功之前或提交失败的时候一直重试,而异步提交不会,但这也是异步提交的问题所在,比如当异步提交偏移量2000的时候,可能因为网络问题没有提交到,而这时候已经偏移量3000,而之后2000才提交上去,这个时候会造成重复读取

之所以说到这个问题是因为异步提交也是支持回调的。

			consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {

                }
            });
提交特定偏移量
        HashMap<TopicPartition, OffsetAndMetadata> currentOffset = new HashMap<TopicPartition, OffsetAndMetadata>();
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("consumer02,offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
                currentOffset.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset()+1,"no metadata"));
            }
            consumer.commitAsync(currentOffset);
        }

从指定偏移量开始读取

从头部和尾部开始读取可以使用seekToBeginingseekToEnd

要从指定的偏移量读取使用seek

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("sun"));
        Set<TopicPartition> assignment = new HashSet<>();
        while(assignment.size()==0){
            consumer.poll(Duration.ofSeconds(1));
            assignment=consumer.assignment();
        }
        consumer.seek(new TopicPartition("sun",0),800);
        while (true){
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofNanos(10));
            for(ConsumerRecord<String,String> record:records){
                System.out.printf("consumer02,offset = %d,partition=%s, key = %s, value = %s%n", record.offset(),record.partition(), record.key(), record.value());
            }
        }

在每次设置分区偏移量之前,都先要获取一次分区,获取分区时不需要把时间设置的特别短,太短获取不到分区。

根据时间指定偏移量开始读取
		KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(initConfig());
        kafkaConsumer.subscribe(Arrays.asList(topic));
        Set<TopicPartition> assignment = new HashSet<>();
        while (assignment.size() == 0) {
            kafkaConsumer.poll(100L);
            assignment = kafkaConsumer.assignment();
        }
        Map<TopicPartition, Long> map = new HashMap<>();
        for (TopicPartition tp : assignment) {
            map.put(tp, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
        }
        Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(map);
        for (TopicPartition topicPartition : offsets.keySet()) {
            OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
            if (offsetAndTimestamp != null) {
                kafkaConsumer.seek(topicPartition,offsetAndTimestamp.offset());
            }
        }
        while (isRunning.get()) {
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(1000L);
            System.out.println("本次拉取的消息数量:" + consumerRecords.count());
            System.out.println("消息集合是否为空:" + consumerRecords.isEmpty());
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println("消费到的消息key:" + consumerRecord.key() + ",value:" + consumerRecord.value() + ",offset:" + consumerRecord.offset());
            }
        }

序列化

Kafka自带且常用的的序列化方式

org.apache.kafka.common.serialization.ByteArraySerializer
org.apache.kafka.common.serialization.ByteArrayDeserializer
org.apache.kafka.common.serialization.StringDeserializer
org.apache.kafka.common.serialization.StringSerializer

kafka权威指南这本书推荐使用Avro进行序列化对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值