kafka API实践(kafka原理验证)

kafka生产者API

kafka生产者发送消息采用异步发送的方式,我们在写发送消息的代码的时候,会调用send方法,整个发送的过程涉及两个线程:main线程和sender线程,有一个重要的线程共享变量:RecordAccumulator,main线程将消息放到RecordAccumulator中缓存,serder线程不断从 RecordAccumulator中poll消息发送到 kafka(producer >> interceptors >> serializer >> partitioner)

测试代码中topic的partition数量为3,ProducerConfig这个类中对kafka producer的各种配置都有详细描述,值得参考。

package com.cf.framework.kafka.client;
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class CustomProducer {
    private static final String BROKERS = "192.168.72.127:9092,192.168.72.128:9092,192.168.72.129:9092";
    private static final String TOPIC = "cat";

    public static void main(String[] args) {
        sendMsg();
    }
    public static void sendMsg() {
        KafkaProducer<String, String> producer = newStrProduer();
        for (int i=0;i<10;i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, TOPIC + "_" + i);
            producer.send(record, new Callback() {
                // 异步回调,producer在收到ack时调用
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    System.out.println("partition:" + metadata.partition() + ",offset:" + metadata.offset());
                }
            });
        }
        // 关闭资源,清除内存中的数据,很重要
        producer.close();
    }
    public static KafkaProducer<String, String> newStrProduer() {
        Properties pro = getProperties();
        KafkaProducer<String, String> producer = new KafkaProducer<>(pro);
        return producer;
    }
    public static Properties getProperties() {
        // 创建生产者配置信息
        Properties pro = new Properties();
        // 指定KAFKA集群
        pro.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BROKERS);
        // ack应答机制
        pro.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数
        pro.put(ProducerConfig.RETRIES_CONFIG, 1);
        // 批次数据大小(byte)
        pro.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        // 等待的时间,如果数据未达到BATCH_SIZE_CONFIG,则在LINGER_MS_CONFIG时长后发送消息
        pro.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // RecordAccumulator缓冲区大小(byte),缓存待发送消息的内存大小
        pro.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        // 指定key序列化器
        pro.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        // 指定value序列化器
        pro.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        return pro;
    }
}

这里有个很重要的方法:producer.close(),这里面会资源回收相关的许多事情。

在没有调用producer.close()的情况下,如果生产者的消息大小没到达BATCH_SIZE_CONFIG,时长也没达到LINGER_MS_CONFIG,消息将不会发送。

发送消息的callback回调方法输出如下:

partition:1,offset:3
partition:1,offset:4
partition:1,offset:5
partition:0,offset:7
partition:0,offset:8
partition:0,offset:9
partition:2,offset:3
partition:2,offset:4
partition:2,offset:5
partition:2,offset:6

我们可以看到每个partition内部都维护了自己的offset,而不是全局的一个offset,且每个partition内部是有序的

kafka消费者消费到的数据如下:

[root@rabbit-node2 bin]# sh kafka-console-consumer.sh --bootstrap-server 192.168.72.127:9092 --from-beginning --topic cat           
cat_0
cat_3
cat_6
cat_9
cat_2
cat_5
cat_8
cat_1
cat_4
cat_7

这里数据分成了三部分:cat_0,cat_3,cat_6,cat_9; cat_2,cat_5,cat_8; cat_1,cat_4,cat_7;分别分配到三个分区中。
这个结果足以说明kafka发送数据确实是批量发送的,三个分区的数据同时写入,其默认的生产者分区策略是轮询的发送方式。而且consumer消费数据时,也是批量消费的,它要把一个partition的数据消费全部拉取出来后,才会拉取下一个partition的数据。
 

指定分区发送消息:

   public static void sendMsgWithPartition() {
        KafkaProducer<String, String> producer = newStrProduer();
        for (int i=0;i<10;i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, 0, i+"",TOPIC + "_" + i);
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    System.out.println("partition:" + metadata.partition() + ",offset:" + metadata.offset());
                }
            });
        }
        // 关闭资源,很重要
        producer.close();
    }

指定producer的key值,按照key值hash取模分配分区:

    public static void sendMsgWithKey() {
        KafkaProducer<String, String> producer = newStrProduer();
        for (int i=0;i<10;i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC,i + "",TOPIC + "_" + i);
            producer.send(record, new Callback() {
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    System.out.println("partition:" + metadata.partition() + ",offset:" + metadata.offset());
                }
            });
        }
        // 关闭资源,很重要
        producer.close();
    }

打印结果如下:
partition:2,offset:19
partition:2,offset:20
partition:2,offset:21
partition:2,offset:22
partition:1,offset:26
partition:1,offset:27
partition:0,offset:28
partition:0,offset:29
partition:0,offset:30
partition:0,offset:31

消息同步发送:

 public static void sendMsg() throws Exception{
        KafkaProducer<String, String> producer = newStrProduer();
        for (int i=0;i<10;i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, TOPIC + "_" + i);
            Future<RecordMetadata> future = producer.send(record);
            future.get();
        }
        // 关闭资源,很重要
        producer.close();
    }

同步发送应用场景:要保证全局消息顺序,此时可以把partiton的数量设置为1,再加上future.get()阻塞sender线程就可以实现这种效果。

光是partition设置为1,无法保证全局消息有序性,因为producer 在发送消息时,第一批数据发出去后,第二批数据并不会等待第一批数据

接收到,立马就会发送出去。如果第一批数据broker没收到,这个时候顺序就乱了。

kafka消费者API

kafka消费者是以消费者组为单位的,先看一下自动提交offset:

package com.cf.framework.kafka.client;
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 java.util.Collections;
import java.util.Properties;

public class CustomConsumer {
    private static final String BROKERS = "192.168.72.127:9092,192.168.72.128:9092,192.168.72.129:9092";
    private static final String TOPIC = "cat";
    public static void main(String[] args) {
        consume();
    }
    public static void consume() {
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(getProperties());
        consumer.subscribe(Collections.singleton(TOPIC));
        while (true) {
            // 拉取数据,延迟时间为100(如果没有数据,它会每隔100ms拉取一次)
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.key() + "-" + consumerRecord.value());
            }
        }

    }
    public static Properties getProperties() {
        // 创建消费者配置信息
        Properties pro = new Properties();
        // 指定KAFKA集群
        pro.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BROKERS);
        // 允许自动提交offset
        pro.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 自动提交offset的间隔(ms)
        pro.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
        // 指定消费者组
        pro.put(ConsumerConfig.GROUP_ID_CONFIG, "bigdata");
        // 自动重置offset,从现有offset的最早的位置开始消费
        pro.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        // 指定key反序列化器
        pro.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        // 指定value反序列化器
        pro.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        return pro;
    }
}

这里面涉及到一个特殊的配置:AUTO_OFFSET_RESET_CONFIG(offset自动重置),此参数在两种情况下生效:

  • 1.当前consumer group第一次消息该topic
  • 2.当前消费的数据不存在

接着看手动提交offset的情况:

手动提交offset有两种:commitSync和commitAsync。不管是同步提交还是异步提交,都会将本次最大偏移量提交,同步提交会阻塞线程,而且提交失败会自动重试。异步提交则没有失败重试机制,所以是有可能提交失败的。

同步提交:

   public static Properties getProperties2() {
        // 创建消费者配置信息
        Properties pro = new Properties();
        // 指定KAFKA集群
        pro.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BROKERS);
        // 允许自动提交offset
        pro.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        // 指定消费者组
        pro.put(ConsumerConfig.GROUP_ID_CONFIG, "bigdata");
        // 自动重置offset,从现有offset的最早的位置开始消费
        pro.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        // 指定key反序列化器
        pro.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        // 指定value反序列化器
        pro.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        return pro;
    }
    public static void consumeSync() {
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(getProperties());
        consumer.subscribe(Collections.singleton(TOPIC));
        while (true) {
            // 拉取数据,延迟时间为100(如果没有数据,它会每隔100ms拉取一次)
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.key() + "-" + consumerRecord.value());
            }
            // 同步提交offset
            consumer.commitSync();
        }
    }

异步提交:

    public static Properties getProperties2() {
        // 创建消费者配置信息
        Properties pro = new Properties();
        // 指定KAFKA集群
        pro.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BROKERS);
        // 允许自动提交offset
        pro.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        // 指定消费者组
        pro.put(ConsumerConfig.GROUP_ID_CONFIG, "bigdata");
        // 自动重置offset,从现有offset的最早的位置开始消费
        pro.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        // 指定key反序列化器
        pro.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        // 指定value反序列化器
        pro.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        return pro;
    }
public static void consumeAsync() {
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(getProperties());
        consumer.subscribe(Collections.singleton(TOPIC));
        while (true) {
            // 拉取数据,延迟时间为100(如果没有数据,它会每隔100ms拉取一次)
            ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.println(consumerRecord.key() + "-" + consumerRecord.value());
            }
            // 异步提交offset
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    System.out.println(exception);
                }
            });
        }
    }

添加拦截器

interceptor1:

package com.cf.framework.kafka.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
public class HeaderInterceptor implements ProducerInterceptor<String,String> {
    // 消息发送到broker或者发送异常时调用,
    @Override
    public ProducerRecord<String,String> onSend(ProducerRecord <String,String> record) {
        String value = record.value();
        value =  value + "_" + System.currentTimeMillis();
        ProducerRecord<String,String> producerRecord = new ProducerRecord<>(record.topic(), record.partition(),record.key(),value);
        return producerRecord;
    }
    // 消息在被是序列化前调用,该方法实现逻辑不宜复杂,容易影响效率
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

    }
    // 资源回收
    @Override
    public void close() {

    }
    // 获取配置信息
    @Override
    public void configure(Map<String, ?> configs) {

    }
}

interceptor2:

package com.cf.framework.kafka.interceptor;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class CounterInterceptor  implements ProducerInterceptor<String,String> {
    AtomicInteger successCounter;
    AtomicInteger failureCounter;
    // 消息发送到broker或者发送异常时调用,
    @Override
    public ProducerRecord onSend(ProducerRecord  record) {
       return record;
    }
    // 消息在被是序列化前调用,该方法实现逻辑不宜复杂,容易影响效率
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (metadata != null) {
            successCounter.incrementAndGet();
        } else{
            failureCounter.incrementAndGet();
        }
    }
    // 资源回收
    @Override
    public void close() {
        System.out.println("发送成功数量:" + successCounter.get());
        System.out.println("发送失败数量:"+ failureCounter.get());
    }
    // 获取配置信息
    @Override
    public void configure(Map<String, ?> configs) {

    }
}

为生产者配置interceptor

   public static Properties getPropertiesWithInterceptor() {
        // 创建生产者配置信息
        Properties pro = new Properties();
        // 指定KAFKA集群
        pro.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BROKERS);
        // ack应答机制
        pro.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重试次数
        pro.put(ProducerConfig.RETRIES_CONFIG, 1);
        // 批次数据大小(byte)
        pro.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        // 等待的时间,如果数据未达到BATCH_SIZE_CONFIG,则在LINGER_MS_CONFIG时长后发送消息
        pro.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // RecordAccumulator缓冲区大小(byte),缓存待发送消息的内存大小
        pro.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        // 指定key序列化器
        pro.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        // 指定value序列化器
        pro.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        List<String> list = Arrays.asList(new String[]{"com.cf.framework.kafka.interceptor.CounterInterceptor", "com.cf.framework.kafka.interceptor.HeaderInterceptor"});
        // 指定拦截器
        pro.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, list);
        return pro;
    }

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值