【Kafka】Kafka快速实战与基本原理详解

Kafka基本概念

kafka是一个分布式的,分区的消息(官方称之为 commit log )服务。首先,让我们来看一下基础的消息( Message )相关术语:

|---------------|--------------------------------------------------------------------------------------------------------|
| 名称 | 解释 |
| Broker | 消息中间件处理节点,一个Kafka节点就是一个broker,一 个或者多个Broker可以组成一个Kafka集群 |
| Topic | Kafka根据topic对消息进行归类,发布到Kafka集群的每条 消息都需要指定一个topic |
| Producer | 消息生产者,向Broker发送消息的客户端 |
| Consumer | 消息消费者,从Broker读取消息的客户端 |
| ConsumerGroup | 每个Consumer属于一个特定的Consumer Group,一条消 息可以被多个不同的Consumer Group消费,但是一个 Consumer Group中只能有一个Consumer能够消费该消息 |
| Partition | 物理上的概念,一个topic可以分为多个partition,每个 partition内部消息是有序的 |

因此,从一个较高的层面上来看,producer通过网络发送消息到Kafka集群,然后consumer来进行消费,服务端(brokers)和客户端(producer、consumer)之间通信通过TCP协议 来完成。

kafka基本使用


安装前的环境准备

由于Kafka是用Scala语言开发的,运行在JVM上,因此在安装Kafka之前需要先安装JDK。

XML 复制代码
yum install java‐1.8.0‐openjdk* ‐y

kafka依赖zookeeper,所以需要先安装zookeeper

java 复制代码
#下载
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.5.8/apache-zookeeper-3.5.8-bin.tar.gz

# 解压
tar -zxvf apache-zookeeper-3.5.8-bin.tar.gz

cd apache-zookeeper-3.5.8-bin

# 复制配置文件
cp conf/zoo_sample.cfg conf/zoo.cfg

# 启动zookeeper
 bin/zkServer.sh start

bin/zkCli.sh

# 查看zk的根目录相关节点
ls / 

如图所示:

第一步:下载kafka安装包

bash 复制代码
# 2.11是scala的版本,2.4.1是kafka的版本
wget https://archive.apache.org/dist/kafka/2.4.1/kafka_2.11-2.4.1.tgz

然后解压缩

bash 复制代码
# 解压
tar -xzf kafka_2.11-2.4.1.tgz
cd kafka_2.11-2.4.1/

第二步:修改配置

修改配置文件config/server.properties:

bash 复制代码
# broker.id属性在kafka集群中必须要是唯一
broker.id=0

# kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT://192.168.43.63:9092

# kafka的消息存储文件 可自定义
log.dirs=/tmp/kafka-logs

# kafka连接zookeeper的地址
 zookeeper.connect=192.168.43.63:2181

第三步:启动服务

现在来启动kafka服务:
启动脚本语法: kafkaserverstart.sh daemon server.properties
可以看到, server.properties 的配置路径是一个强制的参数, daemon 表示以后台进程运行,否则ssh客户端退出后, 就会停止服务。 (注意,在启动kafka时会使用linux主机名关联的ip地址,所以需要把主机名和linux的ip映射配置到本地 host里,用vim /etc/hosts)。查看启动是否报错的文件日子为 server.log 。

bash 复制代码
# 启动kafka,运行日志在logs目录的server.log文件里
./bin/kafka-server-start.sh -daemon ./config/server.properties
或者用
./bin/kafka-server-start.sh ./config/server.properties &

# 我们进入zookeeper目录通过zookeeper客户端查看下zookeeper的目录树
./bin/zkCli.sh

ls / #查看zk的根目录kafka相关节点
ls /brokers/ids #查看kafka节点

# 停止kafka
./bin/kafka‐server‐stop.sh

server.properties核心配置详解:

|----------------------------|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| Property | Default | Description |
| broker.id | 0 | 每个broker都可以用一个唯一的非负整数id进行标识;这个id可以作为 broker的"名字",你可以选择任意你喜欢的数字作为id,只要id是唯 一的即可。 |
| log.dirs | /tmp/kafka-logs | kafka存放数据的路径。这个路径并不是唯一的,可以是多个,路径之间 只需要使用逗号分隔即可;每当创建新partition时,都会选择在包含最 少partitions的路径下进行。 |
| listeners | PLAINTEXT://192.168.43.63:909 2 | server接受客户端连接的端口,ip配置kafka本机ip即可 |
| zookeeper.connec | localhost:2181 | zooKeeper连接字符串的格式为:hostname:port,此处hostname和 port分别是ZooKeeper集群中某个节点的host和port;zookeeper如果 是集群,连接方式为 hostname1:port1, hostname2:port2, hostname3:port3 |
| log.retention.hours | 168 | 每个日志文件删除之前保存的时间。默认数据保存时间对所有topic都一 样。 |
| num.partitions | 1 | 创建topic的默认分区数 |
| default.replication.factor | 1 | 自动创建topic的默认副本数量,建议设置为大于等于2 |
| min.insync.replicas | 1 | 当producer设置acks为-1时,min.insync.replicas指定replicas的最小 数目(必须确认每一个repica的写数据都是成功的),如果这个数目没 有达到,producer发送消息会产生异常 |
| delete.topic.enable | false | 是否允许删除主题 |

Spring Boot整合Kafka

首先我们需要引入kafka的依赖,如下所示:

XML 复制代码
        <dependency>
		    <groupId>org.springframework.kafka</groupId>
		    <artifactId>spring-kafka</artifactId>
		</dependency>

application.yml配置如下:

XML 复制代码
server:
  port: 8080
spring:
  kafka:
    bootstrap-servers: 192.168.190.129:9092,192.168.190.130:9092,192.168.190.131:9092
    producer:
      retries: 3
      batch-size: 16384
      buffer-memory: 33554432
      acks: 1
      # 指定消息key和消息体的编解码方式  
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: testGroup
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      ack-mode: manual_immediate
  cloud:
    zookeeper:
      connect-string: 192.168.190.128:2181

发送者代码:

java 复制代码
package com.example.testcode.demos.controller;

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

/**
 * @description:
 * @author: 黎剑
 * @create: 2024-04-10 22:20
 **/
@RestController
public class KafkaController {

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

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @RequestMapping("/test")
    public String send() {
        kafkaTemplate.send(TOPIC_NAME, 0, "key", "this is a msg");
        return "success";
    }
}

消费者代码:

java 复制代码
package com.example.testcode.demos.consumer;

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

/**
 * @description:
 * @author: 黎剑
 * @create: 2024-04-10 22:22
 **/
@Component
public class MyConsumer {  

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

    // 配置多个消费组
    @KafkaListener(topics = "my-replicated-topic",groupId = "lijianGroup")
    public void listenLiJianGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        ack.acknowledge();
    }
}

运行结果如图:

java 复制代码
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.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: 黎剑
 * @create: 2024-04-13 02:20
 **/
public class MyMsgProducer {

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

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.129:9092,192.168.190.130:9092,192.168.190.131:9092");
         /*
         发出消息持久化机制参数
        (1)acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。
        (2)acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一
             条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
        (3)acks=-1或all: 需要等待 min.insync.replicas(默认为1,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,这种策略会保证
            只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
         */
        /*props.put(ProducerConfig.ACKS_CONFIG, "1");
         *//*
        发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在
        接收者那边做好消息接收的幂等性处理
        *//*
        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从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送消息value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        Producer<String, String> producer = new KafkaProducer<String, String>(props);

        int msgNum = 5;
        final CountDownLatch countDownLatch = new CountDownLatch(msgNum);
        for (int i = 1; i <= msgNum; i++) {
            Order order = new Order(i, 100 + i, 1, 1000.00);
            //指定发送分区
            /*ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME
                    , 0, order.getOrderId().toString(), JSON.toJSONString(order));*/
            //未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum
            ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME
                    , order.getOrderId().toString(), JSON.toJSONString(order));

            //等待消息发送成功的同步阻塞方法
            /*RecordMetadata metadata = producer.send(producerRecord).get();
            System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
                    + metadata.partition() + "|offset-" + metadata.offset());*/

            //异步回调方式发送消息
            producer.send(producerRecord, new Callback() {
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception != null) {
                        System.err.println("发送消息失败:" + exception.getStackTrace());

                    }
                    if (metadata != null) {
                        System.out.println("异步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
                                + metadata.partition() + "|offset-" + metadata.offset());
                    }
                    countDownLatch.countDown();
                }
            });

            //送积分 TODO

        }

        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.close();
    }

}
java 复制代码
public class MsgConsumer {
    private final static String TOPIC_NAME = "my-replicated-topic";
    private final static String CONSUMER_GROUP_NAME = "testGroup";

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.190.129:9092,192.168.190.130:9092,192.168.190.131:9092");
        // 消费分组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        /*
        当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
        latest(默认) :只消费自己启动之后发送到主题的消息
        earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
        */
        //props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
      /*
      consumer给broker发送心跳的间隔时间,broker接收到心跳如果此时有rebalance发生会通过心跳响应将
      rebalance方案下发给consumer,这个时间可以稍微短一点
      */
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
        /*
        服务端broker多久感知不到一个consumer心跳就认为他故障了,会将其踢出消费组,
        对应的Partition也会被重新分配给其他consumer,默认是10秒
        */
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
        //一次poll最大拉取消息的条数,如果消费者处理速度很快,可以设置大点,如果处理速度一般,可以设置小点
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
        /*
        如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,
        会将其踢出消费组,将分区分配给别的consumer消费
        */
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
        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<String, String>(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), 10);*/

        //从指定时间点开始消费
        /*List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
        //从1小时前开始消费
        long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
        Map<TopicPartition, Long> map = new HashMap<>();
        for (PartitionInfo par : topicPartitions) {
            map.put(new TopicPartition(topicName, 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() + "|offset-" + offset);
            System.out.println();
            //根据消费里的timestamp确定offset
            if (value != null) {
                consumer.assign(Arrays.asList(key));
                consumer.seek(key, offset);
            }
        }*/

        while (true) {
            /*
             * poll() API 是拉取消息的长轮询
             */
            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) {
                // 手动同步提交offset,当前线程会阻塞直到offset提交成功
                // 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
                consumer.commitSync();

                // 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
                consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                        if (exception != null) {
                            System.err.println("Commit failed for " + offsets);
                            System.err.println("Commit failed exception: " + exception.getStackTrace());
                        }
                    }
                });

            }*/
        }


    }
相关推荐
驭渊的小故事8 分钟前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客32 分钟前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18844 分钟前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn1 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su2 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123453 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
苜蓿muxu3 小时前
Java 字符串拼接:+、StringBuilder、StringBuffer的区别
java
摇滚侠3 小时前
《Java并发编程之美》第 4 章 Java 并发包中原子操作类原理剖析 阅读笔记 7
java·笔记·python
旋律4144 小时前
同义方法区别(增加类与删除类)
java·后端