Loading

高级API

1.offset自动控制

kafka消费者的默认首次消费策略为latest

可以通过auto.offset.reset进行配置,这个配置只针对消费者之前没有提交偏移量到kafka服务器上也就是第一次消费的时候的行为,如果系统有消费者偏移量的话那么这个配置的不同值后续操作是一样的

earliest - 自动将偏移量重置为最早的偏移量

latest - 自动将偏移量重置为最新的偏移量

none - 如果未找到消费者组的先前偏移量,则向消费者抛出异常

kafka消费者在消费数据的时候默认会定期的提交消费的偏移量,这样就可以保证所有的消息至少可以被消费者消费1次,用户可以通过以下两个参数配置:

enable.auto.commit = true 默认

auto.commit.interval.ms = 5000 默认

手动提交offset,如果用户需要自己管理offset的提交,可以关闭offset的自动提交,手动管理offset提交的偏移量,注意用户提交的offset偏移量永远都要比本次消费的偏移量+1,因为提交的offset是kafka消费者下一次抓取数据的位置

import org.apache.kafka.clients.consumer.ConsumerConfig;
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 KafkaProducerDemo {
    public static void main(String[] args) {
        // 创建链接参数
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 创建生产者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);

        for (Integer i = 0; i < 10; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>("topic04", "key" + i, "value" + i);
            producer.send(record);
        }

        producer.close();
    }
}
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.*;

/**
 * 消费者,手动提交偏移量
 */
public class KafkaConsumerDemo {
    public static void main(String[] args) {
        // 创建Kafka链接参数
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "g4");

        // 这个配置只有在消费者第一次消费时才有区别,如果kafka服务器存在消费者的偏移量,那么这个配置不同值的后续操作其实是一样的
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        // 取消自动提交偏移量
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        // 创建Topic消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
        consumer.subscribe(Arrays.asList("topic04"));

        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
            while (recordIterator.hasNext()) {
                ConsumerRecord<String, String> record = recordIterator.next();
                String key = record.key();
                String value = record.value();
                long offset = record.offset();
                int partition = record.partition();
                Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<TopicPartition, OffsetAndMetadata>();

                // 手动提交的offset永远都要是本次消费的offset+1
                offsets.put(new TopicPartition(record.topic(), partition), new OffsetAndMetadata(offset + 1));
                consumer.commitAsync(offsets, new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                        System.out.println("完成:" + offset + "提交!");
                    }
                });
                System.out.println("key:" + key + ",value:" + value + ",partition:" + partition + ",offset:" + offset);

            }
        }
    }
}

2.Acks & Retries

kafka生产者在发送完一个消息之后,要求Broker在规定的时间Ack应答,如果没有在规定时间内应答,kafka生产者会尝试n次重新发送消息

通知acks进行设置

acks=1 - Leader会将Record写到其本地日志中,但会不等待所有Follower完全确认的情况下做出响应。在这种情况下,如果Leader在确认记录后立即失败,但在Follower复制记录之前失败,则记录将丢失。

acks=0 - 生产者根本不会等待服务器的任何确认。该记录将立即添加到套接字缓冲区中并视为已发送。在这种情况下,不能保证服务器已收到记录。

acks=all - 这意味着Leader将等待全套同步副本确认记录。这保证了只要至少一个同步副本仍处于活动状态,记录就不会丢失。这是最有力的保证。这等效于acks = -1设置。

如果生产者在规定的时间内没有得到kafka的Leader的Ack应答,kafka可以开启reties机制。

request.timeout.ms = 30000 默认
retries = 2147483647 默认

image

import org.apache.kafka.clients.consumer.ConsumerConfig;
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;

/**
 * 生产者,测试ack和重试机制
 */
public class KafkaProducerDemo {
    public static void main(String[] args) {
        // 创建链接参数
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 设置acks和重试次数
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        // 设置重试次数,不包含第一次发送,如果尝试3次发送失败,则不发送
        props.put(ProducerConfig.RETRIES_CONFIG, 3);

        // 设置acks超时时间
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 1);

        // 创建生产者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);

        for (Integer i = 0; i < 1; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>("topic04", "key" + i, "value" + i);
            producer.send(record);
            producer.flush();
        }
        producer.close();
    }
}

3.幂等性

HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

kafka在0.11.0.0版本增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证生产者发送的消息不会丢失而且不会重复。实现幂等的关键点就是服务端可以区分请求是否重复,过滤掉重复的请求。要区分请求是否重复的有两点:

唯一标识:要想区分请求是否重复,请求中就得有唯一标识。例如支付请求中,订单号就是唯一标识

记录已处理过的请求标识:光有唯一标识还不够,还需要记录哪些请求是已经处理过的,这样当收到新的请求时,用新请求中的标识和处理记录进行比较,如果处理记录中有相同的标识,说明是重复记录,拒绝掉。

kafka幂等实现:在初始化期间,kafka会给生产者生成一个唯一的ID称为Producer ID或PID。PID和序列号与消息捆绑在一起,然后发送给Broker。由于序列号从零开始并且单调递增,因此仅当消息的序列号比该PID对应的TopicPartition中最后提交的消息正好大1时,Broker才会接受该消息。如果不是这种情况,则Broker认定是生产者重新发送该消息。

image

开启幂等:enable.idempotence= false 默认,在使用幂等性的时候要求必须开启retries=true和acks=all

import org.apache.kafka.clients.consumer.ConsumerConfig;
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 KafkaProducerDemo {
    public static void main(String[] args) {
        // 创建链接参数
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 设置acks
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        // 设置重试次数,不包含第一次发送,如果尝试3次发送失败,则不发送
        props.put(ProducerConfig.RETRIES_CONFIG, 3);

        // 设置acks超时时间
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 1);

        // 开启幂等
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);

        // 创建生产者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
        for (Integer i = 0; i < 1; i++) {
            ProducerRecord<String, String> record = new ProducerRecord<>("topic04", "key" + i, "value" + i);
            producer.send(record);
            producer.flush();
        }
        producer.close();
    }
}



4.事务控制

kafka的幂等性只能保证一条记录的在分区发送的原子性,如果要保证多条记录(多分区)之间的完整性,这个时候就需要开启kafka的事务操作

kafka0.11.0.0除了引入的幂等性的概念,同时也引入了事务的概念。通常Kafka的事务分为生产者事务Only、消费者&生产者事务。

一般来说消费者消费消息的时候默认的级别是read_uncommited数据,这有可能读取到事务失败的数据,所以在开启生产者事务之后,需要用户设置消费者的事务隔离级别

isolation.level = read_uncommitted 默认,该选项有两个值read_committed|read_uncommitted,如果开始事务控制,消费端必须将事务的隔离级别设置为read_committed

开启生产者事务的时候,只需要指定transactional.id属性即可,一旦开启了事务,默认生产者就已经开启了幂等性。但是要求"transactional.id"的取值必须是唯一的,同一时刻只能有一个"transactional.id"存储在,其他的将会被关闭。

  • 生产者事务Only

    import org.apache.kafka.clients.consumer.ConsumerConfig;
    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 KafkaProducerDemo {
        public static void main(String[] args) {
            // 创建链接参数
            Properties props = new Properties();
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
            props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
            props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
            // 配置事务id
            props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction-id");
    
            // 创建生产者
            KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
    
            // 初始化事务
            producer.initTransactions();
            try {
                // 开启事务
                producer.beginTransaction();
                for (Integer i = 0; i < 10; i++) {
    //                Thread.sleep(10000);
    //                if(i == 5){
    //                  抛出异常事务回滚,观察消费者是否有消费到数据,如果没有消费到则说明事务生效
    //                  int c=10/0;
    //                }
                    ProducerRecord<String, String> record = new ProducerRecord<>("topic05", "key" + i, "value" + i);
                    producer.send(record);
                    producer.flush();
                }
                // 提交事务
                producer.commitTransaction();
            } catch (Exception e) {
                System.out.println("出现错误了" + e.getMessage());
                // 终止事务
                producer.abortTransaction();
            }
            producer.close();
        }
    }
    
    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 java.time.Duration;
    import java.util.Arrays;
    import java.util.Iterator;
    import java.util.Properties;
    
    /**
     * 消费者
     */
    public class KafkaConsumerDemo {
        public static void main(String[] args) {
            // 创建Kafka链接参数
            Properties props=new Properties();
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
            props.put(ConsumerConfig.GROUP_ID_CONFIG,"gg5");
            // 设置隔离级别,因为kafka事务不会回滚,实际上就算失败,也写到日志中了,只是有个标识
            props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
    
            // 创建Topic消费者
            KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
            // 订阅topic开头的消息队列
            consumer.subscribe(Arrays.asList("topic06"));
    
            while (true){
                ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
                Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
                while (recordIterator.hasNext()){
                    ConsumerRecord<String, String> record = recordIterator.next();
                    String key = record.key();
                    String value = record.value();
                    long offset = record.offset();
                    int partition = record.partition();
                    System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
                }
            }
        }
    }
    
  • 消费者&生产者事务

    import org.apache.kafka.clients.consumer.*;
    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.TopicPartition;
    import org.apache.kafka.common.serialization.StringDeserializer;
    import org.apache.kafka.common.serialization.StringSerializer;
    
    import java.time.Duration;
    import java.util.*;
    
    
    /**
     * 消费者&生产者事务
     */
    public class KafkaProducerDemo02 {
        public static void main(String[] args) {
            //生产者&消费者
            KafkaProducer<String, String> producer = buildKafkaProducer();
            KafkaConsumer<String, String> consumer = buildKafkaConsumer("gg9");
    
            consumer.subscribe(Arrays.asList("topic05"));
    
            // 初始化事务
            producer.initTransactions();
    
            try {
                while (true) {
                    ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
                    Iterator<ConsumerRecord<String, String>> consumerRecordIterator = consumerRecords.iterator();
                    // 开启事务控制
                    producer.beginTransaction();
                    Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<TopicPartition, OffsetAndMetadata>();
                    while (consumerRecordIterator.hasNext()) {
                        ConsumerRecord<String, String> record = consumerRecordIterator.next();
                        System.out.println("topic05数据:" + record);
                        //创建Record
                        ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>("topic06", record.key(), record.value());
                        producer.send(producerRecord);
                        //记录元数据
                        offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
                        if ("key5".equals(record.key())) {
                            // 抛出异常,观察消费者
                            int j = 10 / 0;
                        }
                    }
    
                    //提交事务
                    producer.sendOffsetsToTransaction(offsets, "gg9");//提交消费者的偏移量
                    producer.commitTransaction();
                }
            } catch (Exception e) {
                System.out.println("出现错误了" + e.getMessage());
                producer.abortTransaction();//终止事务
            } finally {
                producer.close();
            }
        }
    
        public static KafkaProducer<String, String> buildKafkaProducer() {
            Properties props = new Properties();
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
            props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
            props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
            props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction-id-1");
            return new KafkaProducer<String, String>(props);
        }
    
        public static KafkaConsumer<String, String> buildKafkaConsumer(String group) {
            Properties props = new Properties();
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");
            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
            props.put(ConsumerConfig.GROUP_ID_CONFIG, group);
            //消费者&生产者事务必须关闭offset自动提交
            props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
            props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
            return new KafkaConsumer<String, String>(props);
        }
    }
    
posted @ 2022-02-02 23:32  ZT丶  阅读(27)  评论(0编辑  收藏  举报