Kafka的使用

142 阅读5分钟

一、基本使用

依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.3.1</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

生产者

package com.xxx.test;

import com.alibaba.fastjson.JSON;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.concurrent.ExecutionException;

/**
 * @Author xxx
 * @Date 2023/1/30 10:10
 * @Description kafka生产者测试
 * @Version 1.0
 */
public class MyProducer {

    // 主题名(若不存在该主题,发送时会自动创建)
    private final static String TOPIC_NAME = "my-topic";

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 集合,存放Kafka配置项
        Properties props = new Properties();

        // kafka的主机和端口,集群的话可写多个“主机:端口”,用逗号隔开(注意检查Linux的防火墙是否开启了)
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.132:9092");

        // 如果生产者发送消息没有收到返回的ack,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试,重试的次数3次。
        // 对于ack来说,会有三个参数配置:
        // ack=0      kafka集群不需要任何的broker收到消息,就立即返回ack给生产者,最容易丢消息的,效率是最高的
        // ack=1      多副本之间的leader已经收到消息,并把消息写入到本地的log中,才会返回ack给生产者,性能和安全性是最均衡的
        // ack=-1/all 里面有默认的配置min.insync.replicas=2(默认为1,推荐配置大于等于2),此时就需要leader和一个follower同步完后才会返回ack给生产者
        //           (此时集群中有2个broker已完成数据的接收)这种方式最安全,但性能最差
        props.put(ProducerConfig.ACKS_CONFIG, "1");

        // 发送间隔
        props.put(ProducerConfig.RETRIES_CONFIG, 3);
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);

        // 设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,即32MB
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

        // kafka本地线程会从缓冲区取数据,批量发送到broker
        // 设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

        // 发送的延迟
        // 默认值是0,意思就是消息必须立即被发送,但这样会影响性能
        // 一般设置10毫秒左右,就是说这个消息发送完后会进入本地的一个batch,如果10毫秒内,这个batch满了16kb就会随batch一起被发送出去
        // 如果10毫秒内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

        // 将要发送的消息的键值对都进行序列化处理,将key和value由字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 创建发消息的客户端
        Producer<String, String> producer = new KafkaProducer<>(props);

        // 发之前先封装消息(key的作用是决定往哪个分区发送)
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, "myKey", JSON.toJSONString("消息体内容888"));// 1 未指定分区,会通过hash计算key后,计算出往哪个分区发送
        // ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0, "myKey", JSON.toJSONString("消息体内容2"));// 2 指定了分区“0”

        // 发送消息(同步发送),为了保证消息被消费,同步的发送用的多一点
        try {
            // 同步发送(生产者非得等到kafka返回的ack,不然阻塞3秒*3次)
            RecordMetadata metadata = producer.send(producerRecord).get();
            // 输出元数据
            System.out.println("同步发送消息返回的结果是:" + "主题:" + metadata.topic() + "\t分区:" + metadata.partition() + "\t偏移量:" + metadata.offset());
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 做一些事情,如记录日志
        } finally {
            // 做一些事情
            System.out.println("finally do something");
        }

        // 发送消息(异步发送),不会阻塞,kafka收到消息后,会回调Callback函数
        // producer.send(producerRecord, new Callback() {
        //     public void onCompletion(RecordMetadata metadata, Exception exception) {
        //         if (exception != null) {
        //             System.out.println("消息发送失败:" + exception.getStackTrace());
        //         }
        //         if (metadata != null) {
        //             System.out.println("异步发送消息返回的结果是:");
        //         }
        //     }
        // });
    }
}

消费者

package com.xxx.test;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.*;

/**
 * @Author xxx
 * @Date 2023/1/30 14:25
 * @Description kafka消费者测试
 * @Version 1.0
 */
public class MyConsumer {

    // 主题名
    private final static String TOPIC_NAME = "my-topic";

    // 分组名
    private final static String CONSUMER_GROUP_NAME = "my-group";

    public static void main(String[] args) {

        // 集合,存放Kafka配置项
        Properties props = new Properties();

        // kafka的主机和端口,集群的话可写多个“主机:端口”,用逗号隔开(注意检查Linux的防火墙是否开启了)
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.132:9092");

        // 消费分组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);

        // 消费者poll到消息后默认情况下,会自动向broker的_consumer_offsets主题提交当前主题-分区消费的偏移量。
        // 自动提交会丢消息:因为如果消费者还没消费完下来的消息就自动提交了偏移量,那么此时消费者挂了,于是下一个消费者会从已提交的offset的
        // 下一个位置开始消费消息。之前未被消费的消息就丢失掉了。
        // 设置是否自动提交offset,默认是TRUE
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

        // 若是自动提交,设置自动提交offset的间隔时间
        // props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

        // consumer给broker发送心跳的间隔时间
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);

        // kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

        // 设置一次poll最大拉取的条数,可以根据消费速度的快慢设置
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

        // 如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组。将分区分配给其他消费者。触发rebalance机制,重平衡
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);

        // 当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
        // latest(默认):只消费自己启动之后发送到主题的消息
        // earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer,seekToBeginning(每次都从头开始消费)
        // props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        // 将要接收的消息的键值对都进行序列化处理,将key和value由字符串序列化为字节数组
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 创建一个消费者的客户端
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 消费者订阅主题列表
        consumer.subscribe(Arrays.asList(TOPIC_NAME));

        // 也可以消费指定分区
        // consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

        // 消息回溯消费
        // consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        // consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

        // 指定offset消费
        // consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        // consumer.seek(new TopicPartition(TOPIC_NAME, 0), 3);

        // 从指定时间开始消费
        // List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
        // // 从1小时前开始消费
        // long fetchDataTime = System.currentTimeMillis() - 1000 * 60 * 60;
        // Map<TopicPartition, Long> map = new HashMap<>();
        // for (PartitionInfo par : topicPartitions) {
        //     map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDataTime);
        // }
        // Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
        // for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
        //     TopicPartition key = entry.getKey();
        //     OffsetAndTimestamp value = entry.getValue();
        //     if (key == null || value == null) {
        //         continue;
        //     }
        //     Long offset = value.offset();
        //     System.out.println("partition-" + key.partition() + "\toffset-" + offset);
        //     // 根据消费里的timestamp确定offset
        //     if (value != null) {
        //         consumer.assign(Arrays.asList(key));
        //         consumer.seek(key, offset);
        //     }
        // }

        // 拉取消息的长轮询
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("收到了消息:partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value());
            }

            // 所有的消息被消费完

            if (records.count() > 0) {// 有消息
                // 1 手动同步提交offset,会阻塞到成功(一般用同步提交,因为提交后一般没业务代码了)
                consumer.commitSync();

                // 2 手动异步提交offset,不会阻塞
                // consumer.commitAsync(new OffsetCommitCallback() {
                //     @Override
                //     public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                //         if (exception != null) {
                //             System.out.println("Commit failed for " + offsets);
                //             System.out.println("Commit failed exception: " + exception.getStackTrace());
                //         }
                //     }
                // });
            }
        }
    }
}

二、SpringBoot里的用法

依赖

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

yml配置

server:
  port: 8080

spring:
  kafka:
    bootstrap-servers: 192.168.190.132:9092
    producer: #生产者(实际中一般生产者和消费者不会同时出现)
      retries: 3 #设置大于0的值,则客户端会将发送失败的记录重新发送
      batch-size: 16384
      buffer-memory: 33554432
      acks: 1
      #进行消息key和value的序列化
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer: #消费者(实际中一般生产者和消费者不会同时出现)
      group-id: default-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      max-poll-records: 500
    listener:
        # 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交                                                  RECORD
        # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交                                          BATCH
        # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交                 TIME
        # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交            COUNT
        # TIME | COUNT 有一个条件满足时提交                                                                       COUNT_TIME
        # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,手动调用Acknowledgment,acknowledge()后提交    MANUAL
        # 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种                                   MANUAL_IMMEDIATE
      ack-mode: MANUAL_IMMEDIATE

生产者

package com.xxx.kafkademo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author xxx
 * @Date 2023/1/30 16:46
 * @Description
 * @Version 1.0
 */
@RestController
@RequestMapping("/msg")
public class MyProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    private static final String TOPIC_NAME = "my-topic";

    @RequestMapping("/send")
    public String sendMessage() {
        kafkaTemplate.send(TOPIC_NAME, 0, "myKey001", "msg...");
        return "success";
    }
}

消费者

简单配置

package com.xxx.kafkademo.consumer;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

/**
 * @Author yangpan
 * @Date 2023/1/30 16:58
 * @Description
 * @Version 1.0
 */

@Component
public class MyConsumer {
    @KafkaListener(topics = "my-topic", groupId = "MyGroup")
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        // 手动提交offset
        ack.acknowledge();
    }
}

详细配置

package com.xxx.kafkademo.consumer;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.annotation.PartitionOffset;
import org.springframework.kafka.annotation.TopicPartition;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

/**
 * @Author yangpan
 * @Date 2023/1/30 16:58
 * @Description
 * @Version 1.0
 */

@Component
public class MyConsumer {
    @KafkaListener(groupId = "testGroup", topicPartitions = {
            @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
            @TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
    }, concurrency = "3")// concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
    public void listenGroup2(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        // 手动提交offset
        ack.acknowledge();
    }
}