Kafka—生产者

一、Kafka之生产者

  1、发送消息:ProducerRecord

  

   2、必要的参数配置项:

    bootstrap.servers:Kafka 集群地址,可以填写一个,它会通过这个找到相应的集群信息,但是不建议这样,因为这个集群一旦宕机,那就完了。建议填写多个地址。

    key.serializer:broker 在接收消息必须是以二进制接收的,所以只要别人给我发送消息,我只认二进制数据。所以对应的发送消息的生产者或者消费者发送消息,只要是往 Kafka 发送消息,必须要实现序列化。

             key 序列化,是把它计算出来存到哪个分区上。

      value.serializer:value 就是具体消息内容。

    client.id:这个参数主要设置 Kafka 对应的生产者ID ,默认为空,如果不设置,Kafka 自己内部也会帮你生成一个字符串。

    bootstrap.servers 连接 Kafka 集群的地址,IP+端口号。可以只填写一个,它会自动搜索集群多个可用的地址。但是不建议只填写一个,万一发生宕机,那直接就没了。

    key.serializer 和 value.serializer 因为 kafka 集群,broker 只接收二进制进行接收的,只认二进制的数据。必须要实现序列化,key 序列化的原因是计算出当前数据保存到哪个分区里,value 就是具体的消息内容。

    client.id Kafka 生产者默认的ID,如果不设置,kafka会自动生成一个。

    简化配置 Key:ProducerConfig

    KafkaProducer 是线程安全的

  3、生产者重要参数详解

    acks:指定发送消息后,Broker 端至少有多少个副本接收到该消息;默认为 acks =1

        acks=0:生产者发送消息之后不需要等待任何服务端的响应

        acks=-1 acks=all:生产者在消息发送之后,需要等待 ISR 中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。

    max.request.size:该参数用来限制生产者客户端能发送的消息的最大值(默认 1M)不建议更改

      retries 和 retry.backoff.msretries:重试次数和重试间隔,默认100

    compression.type:这个参数用来指定消息的压缩方式,默认值为“none”,可选配置:“gzip” “snappy”“lz4”

    connections.max.idle.ms:这个参数用来指定在多久之后关闭限制的连接,默认值是 540000(ms),即9分钟

    linger.ms:这个参数用来指定生产者发送 ProducerBatch 之前等待更多消息 (ProducerRecord) 加入 ProducerBatch 的时间,默认值 0

    batch.size:累计多少条信息,则一次进行批量发送;    

    buffer.memory:缓存提升性能参数,默认为32M

      receive.buffer.bytes:这个参数用来设置 Socket 接收消息缓冲区(SO_RECBUF)的大小,默认值为 32768(B),即 32KB;

    send.buffer.bytes:这个参数用来设置 Socket 发送消息缓冲区(SO_SNDBUF)的大小,默认值为 131072(B),即 128KB;

      request.timeout.ms:这个参数用来配置 Producer 等待请求响应的最长时间,默认值为 3000(ms);

  4、拦截器

    拦截器(interceptor):kafka 对应着有生产者和消费者两种拦截器

    生产和实现接口:

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class InterceptorProducer {


    public static void main(String[] args) {

        // 1、配置文件
        Properties properties = new Properties();
        // 连接 kafka 集群的服务列表,如果是多个使用“,”分割
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");
        // 标记 kafkaClient的ID
        properties.put(ProducerConfig.CLIENT_ID_CONFIG, "interceptor-producer");

        // 为什么 kafka 的 kay 和 value 要做序列化?
        // 因为 kafka broker 在接受消息的时候必须要以2进制的方式接收,所以要对 key value 序列化

        // 对 kafka 的 key 以及 value 做序列化
        // key 是 kafka 用于做消息投递计算具体投递到对应的主题的哪一个 partition 而需要的
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // value 是实际发送的消息内容进行
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 配置拦截器,生产者拦截器可以配置多个
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomProducerInterceptor.class.getName());

        // 2、创建 kafka 类
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

        // 3、构造消息内容
        for (int i = 0; i < 100; i++) {
            User user = new User("011" + i, "张三" + i);
            ProducerRecord<String, String> record =
                    // topic,实际的消息内容
                    new ProducerRecord<>(Const.TOPIC_INTERCEPTOR, JSON.toJSONString(user));
            // 4、发送消息
            producer.send(record);
        }

        producer.close();

    }

}

    拦截器

/**
 * @description
 * @author: hq
 * @create: 2022-03-18 21:51
 **/
public class CustomProducerInterceptor implements ProducerInterceptor<String, String> {

    private volatile long success = 0;
    private volatile long failure = 0;

    /**
     * 发送消息之前的切面拦截
     *
     * @param record
     * @return
     */
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        System.err.println("-----------------发送消息之前的拦截--------------------");
        String modifyValue = "prefix" + record.value();
        return new ProducerRecord<String, String>(record.topic(), record.partition()
                , record.timestamp(), record.key(), modifyValue, record.headers());
    }

    /**
     * 发送消息之后的切面拦截
     *
     * @param metadata
     * @param exception
     */
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        System.err.println("-----------------发送消息之后的拦截--------------------");
        if (null == exception) {
            success++;
        } else {
            failure++;
        }
    }

    @Override
    public void close() {
        System.err.println(String.format("成功率:%s,成功:%s,失败:%s", success * 1.0d / (success + failure), success, failure));
    }

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

    }
}

    消费者实现接口:

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class InterceptorConsumer {

    public static void main(String[] args) {
        // 1、创建配置文件
        Properties properties = new Properties();
        // 创建 kafka 连接集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");

        // 配置 key value 序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 创建 group
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "interceptor-group");
        // 常规属性:连接回话超时时间
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 15000);
        // 消费者提交 offset:自动提交,手工提交,默认是自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
        // 配置拦截器
        properties.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, CustomConsumerInterceptor.class.getName());

        // 2、创建 kafka 对象
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        // 3、订阅主题
        consumer.subscribe(Collections.singletonList(Const.TOPIC_INTERCEPTOR));

        System.out.println(Const.TOPIC_INTERCEPTOR + "消费端启动成功");

        try {
            // 4、拉取消息
            while (true) {
                // 等待多久拉取一次消息
                // 拉取主题里面所有的消息
                // topic 和 partition 是一对多的关系,一个topic可以有多个partition
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
                // 因为消息是在 partition 中存储的,所有需要遍历 partition 集合
                for (TopicPartition topicPartition : records.partitions()) {
                    // 获取到每一个TopicPartition对应的主题名称
                    String topic = topicPartition.topic();
                    System.out.println(topic);
                    // 通过每一个 TopicPartition 获取实际的 records 对象中的数据集合
                    List<ConsumerRecord<String, String>> partitionRecords = records.records(topicPartition);
                    // 获取当前 TopicPartition下的消息条数
                    int size = partitionRecords.size();

                    System.err.println(String.format("---获取topic:%s,分区位置:%s,消息总数:%s", topic, topicPartition.partition(), size));

                    for (int i = 0; i < size; i++) {
                        ConsumerRecord<String, String> consumerRecord = partitionRecords.get(i);
                        String key = consumerRecord.key();
                        String value = consumerRecord.value();
                        // 获取当前偏移量
                        long offset = consumerRecord.offset();
                        // 要提交的偏移量
                        long commitOffset = offset + 1;

                        System.err.println(String.format("获取实际消息value:%s,消息offset:%s,提交offset:%s", value, offset, commitOffset));
                    }
                }
            }
        } finally {
            consumer.close();
        }

    }

}

    拦截器

/**
 * @description
 * @author: hq
 * @create: 2022-03-18 21:51
 **/
public class CustomConsumerInterceptor implements ConsumerInterceptor<String, String> {
    /**
     * 消费者接到消息之前的拦截器
     *
     * @param records
     * @return
     */
    @Override
    public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {
        System.err.println("-------------消费者接到消息之前的拦截器--------------");
        return records;
    }

    /**
     * 消费者接到消息之后的拦截器
     *
     * @param offsets
     */
    @Override
    public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {
        System.err.println("-------------消费者接到消息之后的拦截器--------------");
    }

    @Override
    public void close() {

    }

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

    }
}

  5、序列化反序列化

    序列化反序列化:生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发给 Kafka;而在对侧,消费者需要用反序列化器(Deserializer)把从 kafka 中收到的字节数组转换成相应的对象。

    序列化接口:

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class SerializerProducer {


    public static void main(String[] args) {

        // 1、配置文件
        Properties properties = new Properties();
        // 连接 kafka 集群的服务列表,如果是多个使用“,”分割
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");
        // 标记 kafkaClient的ID
        properties.put(ProducerConfig.CLIENT_ID_CONFIG, "serial-producer");

        // 为什么 kafka 的 kay 和 value 要做序列化?
        // 因为 kafka broker 在接受消息的时候必须要以2进制的方式接收,所以要对 key value 序列化

        // 对 kafka 的 key 以及 value 做序列化
        // key 是 kafka 用于做消息投递计算具体投递到对应的主题的哪一个 partition 而需要的
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // value 是实际发送的消息内容进行
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserSerializer.class.getName());

        // 2、创建 kafka 类
        KafkaProducer<String, User> producer = new KafkaProducer<>(properties);

        // 3、构造消息内容
        User user = new User("011", "张三");
        ProducerRecord<String, User> record =
                // topic,实际的消息内容
                new ProducerRecord<>(Const.TOPIC_SERIAL, user);

        // 4、发送消息
        producer.send(record);

        producer.close();

    }

}
/**
 * @description
 * @author: hq
 * @create: 2022-03-18 22:42
 **/
public class UserSerializer implements Serializer<User> {

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {

    }

    @Override
    public byte[] serialize(String topic, User data) {
        if (null == data) {
            return new byte[0];
        }
        byte[] idBytes, nameBytes;
        try {
            String id = data.getId();
            String name = data.getName();

            if (id != null) {
                idBytes = id.getBytes("UTF-8");
            } else {
                idBytes = new byte[0];
            }
            if (name != null) {
                nameBytes = name.getBytes("UTF-8");
            } else {
                nameBytes = new byte[0];
            }

            ByteBuffer byteBuffer = ByteBuffer.allocate(4 + 4 + idBytes.length + nameBytes.length);
            // 4个字节 也就是一个 int 类型:putint 存放 idBytes 的实际真实长度
            byteBuffer.putInt(idBytes.length);
            // put bytes[] 实际存放的是 idBytes 真实的字节数组,也就是内容
            byteBuffer.put(idBytes);
            byteBuffer.putInt(nameBytes.length);
            byteBuffer.put(nameBytes);

            return byteBuffer.array();

        } catch (Exception e) {

        }

        return new byte[0];
    }

    @Override
    public void close() {

    }
}

    反序列化接口:

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class DeSerializerConsumer {

    public static void main(String[] args) {
        // 1、创建配置文件
        Properties properties = new Properties();
        // 创建 kafka 连接集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");

        // 配置 key value 序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserDeSerializer.class.getName());

        // 创建 group
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "serial-group");
        // 常规属性:连接回话超时时间
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 15000);
        // 消费者提交 offset:自动提交,手工提交,默认是自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);


        // 2、创建 kafka 对象
        KafkaConsumer<String, User> consumer = new KafkaConsumer<String, User>(properties);

        // 3、订阅主题
        consumer.subscribe(Collections.singletonList(Const.TOPIC_SERIAL));

        System.out.println(Const.TOPIC_SERIAL + "消费端启动成功");

        try {
            // 4、拉取消息
            while (true) {
                // 等待多久拉取一次消息
                // 拉取主题里面所有的消息
                // topic 和 partition 是一对多的关系,一个topic可以有多个partition
                ConsumerRecords<String, User> records = consumer.poll(Duration.ofMillis(1000));
                // 因为消息是在 partition 中存储的,所有需要遍历 partition 集合
                for (TopicPartition topicPartition : records.partitions()) {
                    // 获取到每一个TopicPartition对应的主题名称
                    String topic = topicPartition.topic();
                    System.out.println(topic);
                    // 通过每一个 TopicPartition 获取实际的 records 对象中的数据集合
                    List<ConsumerRecord<String, User>> partitionRecords = records.records(topicPartition);
                    // 获取当前 TopicPartition下的消息条数
                    int size = partitionRecords.size();

                    System.err.println(String.format("---获取topic:%s,分区位置:%s,消息总数:%s", topic, topicPartition.partition(), size));

                    for (int i = 0; i < size; i++) {
                        ConsumerRecord<String, User> consumerRecord = partitionRecords.get(i);
                        String key = consumerRecord.key();
                        User userValue = consumerRecord.value();
                        // 获取当前偏移量
                        long offset = consumerRecord.offset();
                        // 要提交的偏移量
                        long commitOffset = offset + 1;

                        System.err.println(String.format("获取实际消息value:%s,消息offset:%s,提交offset:%s", userValue, offset, commitOffset));
                    }
                }
            }
        } finally {
            consumer.close();
        }

    }

}
/**
 * @description
 * @author: hq
 * @create: 2022-03-18 22:42
 **/
public class UserDeSerializer implements Deserializer<User> {
    @Override
    public void configure(Map configs, boolean isKey) {

    }

    @Override
    public User deserialize(String topic, byte[] data) {
        if (data.length == 0) {
            return null;
        }
        if (data.length < 8) {
            throw new SerializationException("size is wrong, data length must be >=8");
        }

        ByteBuffer byteBuffer = ByteBuffer.wrap(data);
        // idBytes 字节数组的真实长度
        int idLen = byteBuffer.getInt();
        byte[] idBytes = new byte[idLen];
        byteBuffer.get(idBytes);

        int nameLen = byteBuffer.getInt();
        byte[] nameBytes = new byte[nameLen];
        byteBuffer.get(nameBytes);

        try {
            String id = new String(idBytes, "utf-8");
            String name = new String(nameBytes, "utf-8");

            User user = new User();
            user.setId(id);
            user.setName(name);
            return user;
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("deserializing");
        }
    }

    @Override
    public void close() {

    }
}

   6、分区器

  

 

    生产者发送消息之后会经历一系列的过程,最终到 Kafka broker。首先经历 Interceptor 也就是 onsSend 方法,经历序列化器 Serializer,然后走到 Partitioner,最后拿到 Partition 分区区号之后,再去 Kafka上把消息存储。

    分区器

/**
 * @description 分区器
 * @author: hq
 * @create: 2022-03-30 21:48
 **/
public class CustomPartitioner implements Partitioner {

    private AtomicInteger counter = new AtomicInteger(0);

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // 获取 Partition 集合
        List<PartitionInfo> partitionInfoList = cluster.partitionsForTopic(topic);
        int numOfPartition = partitionInfoList.size();
        System.err.println(String.format("当前分区共有:%s", numOfPartition));
        if (null == keyBytes) {
            return counter.getAndIncrement() % numOfPartition;
        }
        return Utils.toPositive(Utils.murmur2(keyBytes)) % numOfPartition;
    }

    @Override
    public void close() {

    }

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

    }
}

 

    生产者

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class InterceptorProducer {


    public static void main(String[] args) {

        // 1、配置文件
        Properties properties = new Properties();
        // 连接 kafka 集群的服务列表,如果是多个使用“,”分割
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");
        // 标记 kafkaClient的ID
        properties.put(ProducerConfig.CLIENT_ID_CONFIG, "partition-producer");

        // 为什么 kafka 的 kay 和 value 要做序列化?
        // 因为 kafka broker 在接受消息的时候必须要以2进制的方式接收,所以要对 key value 序列化

        // 对 kafka 的 key 以及 value 做序列化
        // key 是 kafka 用于做消息投递计算具体投递到对应的主题的哪一个 partition 而需要的
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // value 是实际发送的消息内容进行
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 创建分区
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class.getName());

        // 2、创建 kafka 类
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

        // 3、构造消息内容
        for (int i = 0; i < 100; i++) {
            User user = new User("011" + i, "张三" + i);
            ProducerRecord<String, String> record =
                    // topic,实际的消息内容
                    new ProducerRecord<>(Const.TOPIC_PARTITION, JSON.toJSONString(user));
            // 4、发送消息
            producer.send(record);
        }

        producer.close();

    }

}

    消费者

/**
 * @description
 * @author: hq
 * @create: 2022-03-06 22:26
 **/
public class InterceptorConsumer {

    public static void main(String[] args) {
        // 1、创建配置文件
        Properties properties = new Properties();
        // 创建 kafka 连接集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.200:9092,192.168.2.201:9092,192.168.2.202:9092");

        // 配置 key value 序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 创建 group
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "partition-group");
        // 常规属性:连接回话超时时间
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 15000);
        // 消费者提交 offset:自动提交,手工提交,默认是自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 默认 5s
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);

        // 2、创建 kafka 对象
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        // 3、订阅主题
        consumer.subscribe(Collections.singletonList(Const.TOPIC_PARTITION));

        System.out.println(Const.TOPIC_PARTITION + "消费端启动成功");

        try {
            // 4、拉取消息
            while (true) {
                // 等待多久拉取一次消息
                // 拉取主题里面所有的消息
                // topic 和 partition 是一对多的关系,一个topic可以有多个partition
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
                // 因为消息是在 partition 中存储的,所有需要遍历 partition 集合
                for (TopicPartition topicPartition : records.partitions()) {
                    // 获取到每一个TopicPartition对应的主题名称
                    String topic = topicPartition.topic();
                    System.out.println(topic);
                    // 通过每一个 TopicPartition 获取实际的 records 对象中的数据集合
                    List<ConsumerRecord<String, String>> partitionRecords = records.records(topicPartition);
                    // 获取当前 TopicPartition下的消息条数
                    int size = partitionRecords.size();

                    System.err.println(String.format("---获取topic:%s,分区位置:%s,消息总数:%s", topic, topicPartition.partition(), size));

                    for (int i = 0; i < size; i++) {
                        ConsumerRecord<String, String> consumerRecord = partitionRecords.get(i);
                        String key = consumerRecord.key();
                        String value = consumerRecord.value();
                        // 获取当前偏移量
                        long offset = consumerRecord.offset();
                        // 要提交的偏移量
                        long commitOffset = offset + 1;

                        System.err.println(String.format("获取实际消息value:%s,消息offset:%s,提交offset:%s", value, offset, commitOffset));
                    }
                }
            }
        } finally {
            consumer.close();
        }

    }

}

 

posted @ 2022-03-16 21:38  放手解脱  阅读(526)  评论(0)    收藏  举报