Java操作Kafka API以及Spring Boot集成Kafka

Kafka

官网:http://kafka.apache.org/

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。

主要特点

同时为发布和订阅提供高吞吐量

可进行持久化操作

分布式系统,易于向外扩展

消息被处理的状态是在consumer端维护,而不是由server端维护

支持online和offline的场景

基本概念

1.Broker

Kafka 集群包含一个或多个服务器,服务器节点称为broker

2.Topic

每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。

物理上不同Topic的消息分开存储,逻辑上一个Topic的消息保存于一个或多个broker上,使用时只需指定消息的Topic即可生产或消费数据而不必关心数据的存放

3.Partition

topic中的数据分割为一个或多个partition。每个topic至少有一个partition,当生产者产生数据的时候,根据分配策略,选择分区,然后将消息追加到指定的分区的末尾

Partation数据路由规则

指定patition,则直接使用

未指定patition但指定key,通过对 key的value进行hash选出一个patition

patition和key都未指定,使用轮询选出一个patition

每条消息都会有一个自增的编号,用于标识顺序与标识消息的偏移量

每个partition中的数据使用多个segment文件存储

partition中的数据是有序的,不同partition间的数据丢失了数据的顺序

如果topic有多个partition,消费数据时就不能保证数据的顺序。严格保证消息的消费顺序的场景下,需要将partition数目设为1。

4.Message

消息,是通信的基本单位,每个producer可以向一个topic(主题)发布一些消息。

5.Producers

消息和数据生产者,向Kafka的一个topic发布消息的过程叫做producers。

6.Consumers

消息和数据消费者,订阅topics并处理其发布的消息的过程叫做consumers。

发送消息的流程

1.Producer根据指定的partition方法(round-robin、hash等),将消息发布到指定topic的partition里面

2.kafka集群接收到Producer发过来的消息后,将其持久化到硬盘,并保留消息指定时长(可配置),而不关注消息是否被消费

3.Consumer从kafka集群pull数据,并控制获取消息的offset

Java操作Kafka API

引入依赖

<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.2.0</version>
</dependency>

生产者

public class MyProducer extends Thread {
    private Producer<String, String> producer;

    /**
     * 创建构造器
     */
    public MyProducer(String threadName) {
        //设置线程的名字
        super.setName(threadName);
        //创建kafka生产者的配置信息
        Properties properties = new Properties();
        // Kafka集群地址,多个地址用逗号分割
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        //网络传输,对key和value进行序列化
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        /**
         * 写出的应答方式,要保证消息生产者数据不丢失,使用消息确认机制(ACK机制)
         *
         * "0": producer无需等待来自broker的确认而继续发送下一批消息,传输效率最高但数据可靠性确最低
         * "1": producer只要收到一个分区副本成功写入的通知就认为推送消息成功,副本必须是leader副本,只有leader副本成功写入,producer才会认为消息发送成功
         * "-1": producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功
         */
        properties.put(ProducerConfig.ACKS_CONFIG, "-1");
        // 重试次数
        properties.put(ProducerConfig.RETRIES_CONFIG, "0");
        //批量写出
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, "16384");
        // 等待时间
        properties.put(ProducerConfig.LINGER_MS_CONFIG, "0");
        // 缓冲区大小
        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, "33554432");
        // 添加分区器
//        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "cn.ybzy.demo.kafka.MyPartitioner");
        //创建生产者对象,从properties对象或者从properties文件中加载信息
        producer = new KafkaProducer<>(properties);
    }

    public static void main(String[] args) {
        MyProducer producer = new MyProducer("thread-producer");
        producer.start();
    }

    @Override
    public void run() {
        int count = 0;
        while (count < 100000) {
            String key = String.valueOf(++count);
            String value = key;
            //将消息内容封装到ProducerRecord中
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytopic", key, value);

            /**
             * 异步发送消息到服务器
             * 生产者写一条消息是写到某个缓冲区,缓冲区里的数据还没写到broker集群里的某个分区,就返回到client端
             * 效率快但不能保证消息一定被发送出去了
             *
             */
            producer.send(producerRecord);

            /**
             * 同步发送消息到服务器
             * 生产者写一条消息就立马发送到某个分区
             * follower需要从leader拉取消息到本地,再向leader发送确认
             * leader再向客户端发送确认,客户端才能得到确认
             */
            Future<RecordMetadata> demo = producer.send(new ProducerRecord<>("mytopic", key, "生产者发送同步消息--" + count));
            try {
                RecordMetadata recordMetadata = demo.get();
                System.out.println("主题:" + recordMetadata.topic() + " 元数据分区:" + recordMetadata.partition() + " 偏移量:" + recordMetadata.offset());
            } catch (Exception e) {
                System.out.println("消息发送失败...");
                e.printStackTrace();
            }


            /**
             * 带回调函数发送消息
             * 回调函数会在producer收到ack 时调用,为异步调用
             * 方法有两个参数:
             *  RecordMetadata:元数据信息
             *  Exception:==null =》消息发送成功 ; !=null=》消息发送失败
             */
//            producer.send(new ProducerRecord<>("mytopic", key,"带回调方法的生产者发送消息--" + count), (metadata, exception) -> {
//                if (exception == null) {
//                    System.out.println("主题:"+metadata.topic()+"元数据分区:"+metadata.partition() + ",偏移量:" + metadata.offset());
//                } else {
//                    exception.printStackTrace();
//                }
//            });


            System.out.println("发送消息:" + "key:" + key + "value:" + value);
            //每个1秒发送1条
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 关闭资源
        producer.close();
    }
}


消费者

public class MyConsumer extends Thread {
    private KafkaConsumer<String, String> consumer;

    /**
     * 创建构造器
     */
    public MyConsumer(String cname) {
        super.setName(cname);
        //读取配置文件
        Properties properties = new Properties();
        //ZK地址
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        // key,value的反序列化
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        //消费者所在组的名称,消费者组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "testGroup");
        //ZK超时时间
        properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "45000");
        /**
         * 自动提交偏移量,可能导致消费者丢失数据
         *
         * 原因:由于Kafka consumer默认是自动提交位移的(先更新位移,再消费消息),如果消费程序出现故障,没消费完毕,则丢失消息,此时,broker并不知道
         *
         * 解决:关闭自动提交位移,在消息被完整处理之后再手动提交位移
         */
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        //消费者自动提交偏移量的时间间隔/延时
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 心跳时间
        properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "3000");
        properties.put(ConsumerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG, "540000");
        /**
         *  earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
         *  latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
         *  none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
         */
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        //创建消费者对象
        consumer = new KafkaConsumer<>(properties);
        // 订阅主题
        consumer.subscribe(Collections.singletonList("mytopic"));
    }

    public static void main(String[] args) {
        MyConsumer consumer01 = new MyConsumer("thread-consumer");
        consumer01.start();
    }


    @Override
    public void run() {
        while (true) {
        	/**
        	 * 自动提交offset
        	 */
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(100));
            // 解析并打印consumerRecords
            for (ConsumerRecord<String, String> record : poll) {
                int partition = record.partition();
                long offset = record.offset();
                String data = record.value();
                String key = record.key();
                System.out.println("消费消息:" + " value:" + data + " key:" + key + " 分区:" + partition + " 偏移量:" + offset);
            }

            /**
             * 数据漏消费和重复消费
             *  1.先提交offset后消费:可能造成数据的漏消费
             *  2.先消费后提交offset:可能造成数据的重复消费
             */

            // 同步提交,当前线程会阻塞直到 offset 提交成功
//            consumer.commitSync();


            // 异步提交
//            consumer.commitAsync((Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception)-> {
//                if (exception != null) {
//                    System.err.println("异步提交失败:" + offsets);
//                }
//            });
        }
    }
}

与SpringBoot集成

添加依赖

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

添加配置

spring:
  kafka:
    # kafka集群地址
    bootstrap-servers: localhost:9092
    # 生产者配置
    producer:
      #  发送消息失败时的一个重试的次数
      retries: 0
      # key/value的序列化
      key-serializer: org.apache.kafka.common.serialization.IntegerSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 应答方式
      # 0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # 1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # -1: 表示分区leader必须等待消息被成功写入到所有的ISR副本(同步副本)中才认为producer请求成功。这种方案提供最高的消息持久性保证,但是理论上吞吐率也是最差的。
      acks: 1
      # 批量发送数据的配置 
      batch-size: 16384
      # 设置kafka 生产者内存缓存区的大小(32M)
      buffer-memory: 33554432
      # 服务器地址
      bootstrap-servers: localhost:9092
    
    # 消费者配置
    consumer:
      # key/value的反序列化
      key-deserializer: org.apache.kafka.common.serialization.IntegerDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 指定一个默认的组名
      group-id: testGroup
      # 指定消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
      auto-offset-reset: earliest

生成者

@RestController
public class KafkaProducerController {

    @Autowired
    private KafkaTemplate<Integer, String> template;

    @Scheduled(initialDelay = 2000, fixedRate = 5000)
    public String send() {
        final ListenableFuture<SendResult<Integer, String>> future = this.template.send("mytopic", UUID.randomUUID().toString());

        try {
            final SendResult<Integer, String> sendResult = future.get();
            final RecordMetadata metadata = sendResult.getRecordMetadata();

            System.out.println("生产者发送消息: " + metadata.topic() + "\t" + metadata.partition() + "\t" + metadata.offset());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        return "success";
    }
}

消费者

@Component
public class KafkaConsumer {

    @KafkaListener(topics = "mytopic")
    public void onMassage(ConsumerRecord<Integer, String> record) {
        System.out.println("收到的消息:" + "\t" + record.topic() + "\t" + record.partition() + "\t" + record.offset() + "\t" + record.key() + "\t" + record.value());
    }
}

手动签收消息

开启手动签收

修改消费者配置,关闭自动签收,使用手动签收

spring:
  kafka:
    # kafka集群地址
    bootstrap-servers: localhost:9092
    # 消费者配置
    consumer:
      # consumer 消息的签收机制:手工签收
      enable-auto-commit: false
      # key/value的反序列化
      key-deserializer: org.apache.kafka.common.serialization.IntegerDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 指定一个默认的组名
      group-id: testGroup
      auto-offset-reset: earliest
    listener:
      # 签收模式
      ack-mode: manual
      # 并行线程数
      concurrency: 5

消费者

@Slf4j
@Component
public class KafkaProducerService {

    @Autowired
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void sendMessage(String topic, Object object) {

        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, object);

        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onSuccess(SendResult<String, Object> result) {
                log.info("发送消息成功: " + result.toString());
            }

            @Override
            public void onFailure(Throwable throwable) {
                log.error("发送消息失败: " + throwable.getMessage());
            }
        });
    }
}

生成者

@Slf4j
@Component
public class KafkaConsumerService {

    @KafkaListener(groupId = "testGroup", topics = "mytopic")
    public void onMessage(ConsumerRecord<String, Object> record, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
        log.info("消费端接收消息: {}", record.value());
        //	收工签收机制
        acknowledgment.acknowledge();
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeDevMaster

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值