第一章 Kafka快速实战与基本原理

第一章 Kafka快速实战与基本原理

1、介绍

Untitled

Kafka 是最初由 Linkedin 公司开发的,是一个分布式、支持分区的(partition)、多副本的(replica),基于 zookeeper 协调的分布式消息系统,它最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于 hadoop 的批处理系统、低延迟的实时系统、Storm/Spark 流式处理引擎,web/nginx 日志、访问日志,消息服务等等,用 scala 语言编写,Linkedin 于 2010 年贡献给了 Apache 基金会并成为顶级开源项目。

1.1、Kafka的使用场景

  • 日志收集:一个公司可以用 Kafka 收集各种服务的 log,通过 kafka 以统一接口服务的方式开放给各种 consumer,例如 hadoop、Hbase、Solr 等。
  • 消息系统:解耦生产者和消费者、缓存消息等。
  • 用户活动跟踪:Kafka 经常被用来记录 web 用户或者 app 用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到 kafka 的topic 中,然后订阅者通过订阅这些 topic 来做实时的监控分析,或者装载到 hadoop、数据仓库中做离线分析和挖掘。
  • 运营指标:Kafka 也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。

Untitled

1.2、基本概念

kafka是一个分布式的,分区的消息(官方称之为commit log)服务。它提供一个消息系统应该具备的功能,但是却有着独特的设计。可以这样说,Kafka 借鉴了 JMS 规范的思想,但是却并没有完全遵循 JMS 规范。

基础的消息(Message)相关术语:

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

因此,从一个较高的层面上来看,Producer 通过网络发送消息到 Kafka 集群,然后 Consumer 来进行消费,如下图:

Untitled

服务端(Brokers)和客户端(Producer、Consumer)之间的通信是通过 TCP 协议来完成的。

2、基本使用

安装前的环境准备

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

2、Kafka 依赖 zookeeper,所以需要先安装 zookeeper

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
ls /			#查看zk的根目录相关节点

2.1、安装 Zookeeper 和 Kafka

1、下载安装包

下载 2.4.1 release 版本,并解压:

wget https://archive.apache.org/dist/kafka/2.4.1/kafka_2.11-2.4.1.tgz  # 2.11是scala的版本,2.4.1是kafka的版本
tar -zxvf kafka_2.11-2.4.1.tgz
cd kafka_2.11-2.4.1/

2、修改配置

修改配置文件 config/server.properties

# broker.id属性在kafka集群中必须是唯一的
broker.id=0
# kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT://10.0.12.14:9092 # 内网地址
advertised.listeners=PLAINTEXT://119.91.21.45:9092 # 公网地址
# kafka的消息存储路径
log.dir=/usr/local/data/kafka-logs
# kafka连接zookeeper的地址
zookeeper.connect=119.91.21.45:2181

3、启动服务

启动脚本语法:kafka-server-start.sh [-daemon] server.properties

可以看到,server.properties 的配置路径是一个强制的参数,-daemon 表示以后台进程运行,否则 ssh 客户端退出后,就会停止服务。

# 启动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

Untitled

server.properties 核心配置详解:

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

2.2、创建主题

创建一个名字为 “test” 的 Topic,这个 Topic 只有一个 partition,并且备份因子也设置为1:

bin/kafka-topics.sh --create --zookeeper 119.91.21.45:2181 --replication-factor 1 --partitions 1 --topic test # test表示主题名称

可以通过以下命令来查看 kafka 中目前存在的 Topic:

bin/kafka-topics.sh --list --zookeeper 119.91.21.45:2181

Untitled

除了我们通过手动的方式创建 Topic,当 producer 发布一个消息到某个指定的 Topic 时,如果这个 Topic 不存在,那么 Kafka 也会自动创建。

2.3、删除主题

bin/kafka-topics.sh --delete --topic test --zookeeper 119.91.21.45:2181

2.4、发送消息

kafka自带了一个 producer 命令客户端,可以从本地文件中读取内容,或者我们也可以在命令行中直接输入内容,并将这些内容以消息的形式发送到 kafka 集群中。在默认情况下,每一个行会被当做一个独立的消息。

首先我们要运行发布消息的脚本,然后在命令行中输入要发送的消息内容:

bin/kafka-console-producer.sh --broker-list 119.91.21.45:9092 --topic test 
>this is a msg
>this is a another msg

Untitled

2.5、消费消息

对于 consumer,kafka 同样也携带了一个命令行客户端,会将获取到的内容在命令中进行输出,默认是消费 consumer 启动之后的消息:

bin/kafka-console-consumer.sh --bootstrap-server 119.91.21.45:9092 --topic test

Untitled

如果想要消费之前的消息可以通过 --from-beginning 参数指定,如下命令:

bin/kafka-console-consumer.sh --bootstrap-server 119.91.21.45:9092 --from-beginning --topic test

Untitled

2.6、消费多主题

bin/kafka-console-consumer.sh --bootstrap-server 119.91.21.45:9092 --whitelist "test|test-2"

2.7、单播消费

一条消息只能被某一个消费者消费的模式,类似 queue 模式(队列),只需让所有消费者在同一个消费组里即可

分别在两个客户端执行如下消费命令,然后往主题里发送消息,结果只有一个客户端能收到消息

bin/kafka-console-consumer.sh --bootstrap-server 119.91.21.45:9092 --consumer-property group.id=testGroup --topic test

2.8、多播消费

一条消息能被多个消费者消费的模式,类似 publish-subscribe 模式(发布-订阅),针对 Kafka 同一条消息只能被同一个消费组下的某一个消费者消费的特性,要实现多播只要保证这些消费者属于不同的消费组即可。我们再增加一个消费者,该消费者属于 testGroup-2 消费组,结果两个客户端都能收到消息

bin/kafka-console-consumer.sh --bootstrap-server 119.91.21.45:9092 --consumer-property group.id=testGroup-2 --topic test

2.9、查看消费组名

bin/kafka-consumer-groups.sh --bootstrap-server 119.91.21.45:9092 --list

Untitled

2.10、查看消费组的消费偏移量

bin/kafka-consumer-groups.sh --bootstrap-server 119.91.21.45:9092 --describe --group testGroup

Untitled

  • current-offset:当前消费组的已消费偏移量
  • log-end-offset:主题对应分区消息的结束偏移量(HW)
  • lag:当前消费组未消费的消息数

3、主题Topic和消息日志Log

可以理解 Topic 是一个类别的名称,同类消息发送到同一个 Topic 下面。对于每一个 Topic,下面可以有多个分区(Partition)日志文件:

Untitled

Partition 是一个有序的 message 序列,这些 message 按顺序添加到一个叫做 commit log 的文件中。每个 partition 中的消息都有一个唯一的编号,称之为offset,用来唯一标识某个分区中的 message。

每个 partition,都对应一个 commit log 文件。一个 partition 中的 message 的 offset 都是唯一的,但是不同的 partition 中的 message 的 offset 可能是相同的。

kafka 一般不会删除消息,不管这些消息有没有被消费。只会根据配置的日志保留时间(log.retention.hours)确认消息多久被删除,默认保留最近一周的日志消息。kafka 的性能与保留的消息数据量大小没有关系,因此保存大量的数据消息日志信息不会有什么影响。

每个 consumer 是基于自己在 commit log 中的消费进度(offset)来进行工作的。在 kafka 中,消费 offset 由 consumer 自己来维护;一般情况下我们按照顺序逐条消费 commit log 中的消息,当然我们也可以通过指定 offset 来重复消费某些消息,或者跳过某些消息。

这意味着 kafka 中的 consumer 对集群的影响是非常小的,添加一个或者减少一个 consumer,对于集群或者其他 consumer 来说都是没有影响的,因为每个consumer 都是维护各自的消费 offset。

1、创建多个分区的主题

bin/kafka-topics.sh --create --zookeeper 119.91.21.45:2181 --replication-factor 1 --partitions 2 --topic test1

2、查看 topic 的情况

bin/kafka-topics.sh --describe --zookeeper 119.91.21.45:2181 --topic test1

Untitled

第一行是所有分区的概要信息,之后的每一行表示每一个 partition 的信息。

  • Leader 节点负责给定 Partition 的所有读写请求,同一个主题不同分区 leader 副本一般不一样(为了容灾)
  • Replicas 表示某个 Partition 在哪几个 Broker 上存在备份。不管这个节点是不是 Leader 节点,甚至这个节点挂了,也会列出。
  • Isr 是 Replicas 的一个子集,它只列出当前还存活着的,并且已经同步备份了该 Partition 的节点。

我们可以运行相同的命令查看之前创建的名称为 “test” 的 topic:

bin/kafka-topics.sh --describe --zookeeper 119.91.21.45:2181 --topic test

Untitled

之前设置了 topic 的 partition 数量为1,备份因子为1,因此显示就如上所示了。

可以进入 kafka 的数据文件存储目录查看 test 和 test1 主题的消息日志文件:

Untitled

消息日志文件主要存放在分区文件夹里的以 log 结尾的日志文件里,如下是 test1 主题对应的分区 0 的消息日志:

Untitled

可以通过如下命令增加 topic 的分区数量(目前 kafka 不支持减少分区):

bin/kafka-topics.sh -alter --partitions 3 --zookeeper 119.91.21.45:2181 --topic test

3.1、理解 Partition

一个 topic 代表逻辑上的一个业务数据集,比如按照数据库里不同表的数据操作消息区分放入不同的 topic,订单相关操作的消息放入订单 topic,用户相关操作的消息放入用户 topic,对于大型网站来说,后端数据都是海量的,订单消息很可能是非常巨量的,比如有几百个 G 甚至达到 TB 级别,如果把这么多数据都放在一台机器上那肯定会有容量限制问题,那么就可以在 topic 内部划分多个 partition 来分片存储数据,不同的 partition 可以位于不同的机器上,每台机器上都运行一个 Kafka 的进程 Broker。

为什么要对 Topic 下的数据进行分区存储?

1、commit log 文件会受到所在机器的文件系统大小的限制,分区之后可以将不同的分区放在不同的机器上,相当于对数据做了分布式存储,理论上一个topic 可以处理任意数量的数据。

2、为了提高并行度。

4、kafka集群实战

对于 kafka 来说,一个单独的 broker 意味着 kafka 集群中只有一个节点。要想增加 kafka 集群中的节点数量,只需要多启动几个 broker 实例即可。为了有更好的理解,现在我们在一台机器上同时启动三个 broker 实例。

首先,我们需要建立好其他2个 broker 的配置文件:

cp config/server.properties config/server-1.properties
cp config/server.properties config/server-2.properties

配置文件需要修改的内容分别如下:

1、config/server-1.properties

# broker.id属性在kafka集群中必须是唯一的
broker.id=1
# kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT://10.0.12.14:9093 # 内网地址
advertised.listeners=PLAINTEXT://119.91.21.45:9093 # 公网地址
log.dir=/usr/local/data/kafka-logs-1
# kafka连接zookeeper的地址,要把多个kafka实例组成集群,对应连接的zookeeper必须相同
zookeeper.connect=119.91.21.45:2181

2、config/server-2.properties

# broker.id属性在kafka集群中必须是唯一的
broker.id=2
# kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT://10.0.12.14:9094 # 内网地址
advertised.listeners=PLAINTEXT://119.91.21.45:9094 # 公网地址
log.dir=/usr/local/data/kafka-logs-2
# kafka连接zookeeper的地址,要把多个kafka实例组成集群,对应连接的zookeeper必须相同
zookeeper.connect=119.91.21.45:2181

目前我们已经有一个 zookeeper 实例和一个 broker 实例在运行了,现在我们只需要再启动2个 broker 实例即可:

bin/kafka-server-start.sh -daemon config/server-1.properties
bin/kafka-server-start.sh -daemon config/server-2.properties

注意:如果我们的云服务器内存较小,启动多个 Kafka 会显示内存不足,这时候可以通过修改 Kafka 启动内存大小来解决!

vim /bin/kafka-server-start.sh

Untitled

查看 zookeeper 确认集群节点是否都注册成功:

ls /brokers/ids

Untitled

现在我们创建一个新的 topic,副本数设置为3,分区数设置为2:

bin/kafka-topics.sh --create --zookeeper 119.91.21.45:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic

Untitled

查看 topic 的情况:

bin/kafka-topics.sh --describe --zookeeper 119.91.21.45:2181 --topic my-replicated-topic

Untitled

  • Leader 节点负责给定 Partition 的所有读写请求,同一个主题不同分区的 leader 副本一般是不一样的(为了容灾)
  • Replicas 表示某个 Partition 在哪几个 Broker 上存在备份。不管这个节点是不是 Leader 节点,甚至这个节点挂了,也会列出。
  • Isr 是 Replicas 的一个子集,它只列出当前还存活着的,并且已经同步备份了该 Partition 的节点。

现在我们向新建的 my-replicated-topic 中发送一些 Message,Kafka 集群可以加上所有 Kafka 节点:

bin/kafka-console-producer.sh --broker-list 8.142.132.36:9092,8.142.132.36:9093,8.142.132.36:9094 --topic my-replicated-topic
>my test msg 1
>my test msg 2

4.1、集群消费

log 的 partitions 分布在 kafka 集群中的不同 broker 上,每个 broker 可以请求备份其他 broker 上的 partition 数据。kafka 集群支持配置一个 partition 备份的数量。

针对每个 partition,都有一个 broker 起到 “leader” 的作用,0个或多个其他的 broker 作为 “follwers” 的作用。leader 处理所有针对这个 partition 的读写请求,而 followers 则被动复制 leader 的结果,不提供读写(主要是为了保证多副本数据与消费的一致性)。如果这个 leader 失效了,那么其中的一个 follower 将会自动的变成新的 leader。

Producers

生产者将消息发送到 topic 中去,同时负责选择将 message 发送到 topic 的哪一个 partition 中。通过 round-robin 做简单的负载均衡,也可以根据消息中的某一个关键字来进行区分。通常第二种方式使用的更多。

Consumers

传统的消息传递模式有2种:队列(queue)和(publish-subscribe)

  • queue 模式:多个 consumer 从服务器中读取数据,消息只会到达一个 consumer。
  • publish-subscribe 模式:消息会被广播给所有的 consumer。

Kafka 基于这2种模式提供了一种 consumer 的抽象概念:consumer group

  • queue 模式:所有的 consumer 都位于同一个 consumer group 下。
  • publish-subscribe 模式:所有的 consumer 都有着自己唯一的 consumer group。

Untitled

上图说明:由2个 broker 组成的 kafka 集群,某个主题总共有4个 partition(P0-P3),分别位于不同的 broker 上。这个集群由2个 Consumer Group 进行消费, A 有 2 个 consumer instances ,B 有 4 个。

通常一个 topic 会有几个 consumer group,每个 consumer group 都是一个逻辑上的订阅者( logical subscriber )。每个 consumer group 由多个 consumer instance 组成,从而达到可扩展和容灾的功能。

顺序消费

一个 partition 同一时刻在一个 consumer group 中只能由一个 consumer instance 进行消费,从而保证消费顺序。

consumer group 中的 consumer instance 的数量不能比一个 Topic 中的 partition 数量还多,否则多出来的 consumer 消费不到消息。

Kafka 只在 partition 的范围内保证消息消费的局部顺序性,不能在同一个 topic 中的多个 partition 中保证总的消费顺序性。

如果需要在总体上保证消费的顺序,那么我们可以通过将 topic 的 partition 数量设置为1,将 consumer group 中的 consumer instance 数量也设置为1,但是这样会影响性能,所以 kafka 的顺序消费很少用。

5、Java客户端访问Kafka

5.1、引入maven依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.4.1</version>
</dependency>

5.2、消息发送端

public class MsgProducer {

    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, "119.91.21.45:9092,119.91.21.45:9093");
        /**
         * 发送消息持久化机制参数
         * 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());

        // 创建Kafka发送者
        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();
    }

}

5.3、消息接收端

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) throws Exception {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "119.91.21.45:9092,119.91.21.45:9093,119.91.21.45:9094");
        // 消费分组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        // 是否自动提交offset,默认就是true
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        // 自动提交offset的间隔时间(一般不会用自动提交)
//        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
//        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        /**
         * 当消费主题的是一个新的消费组,或者指定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, 50);
        /**
         * 如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,
         * 会将其踢出消费组,将分区分配给其他consumer消费
         */
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
        // 将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<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消费
         * 从0分区的offset等于10开始消费
         */
//        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(TOPIC_NAME, par.partition()), fetchDataTime);
//        }
//        // 获取最近一小时的offset
//        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()是拉取消息的长轮询
             * 何为长轮询?如果一秒钟之内没有拉到消息,那么客户端会一直进行拉取,直到拉取的总时间为1秒。如果某一次拉到消息,那么会立即返回
             */
            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());
//                        }
//                    }
//                });
            }
        }
    }
}

6、Spring Boot整合Kafka

6.1、引入依赖

引入spring boot kafka依赖

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

6.2、配置文件

server:
  port: 8080

spring:
  kafka:
    bootstrap-servers: 119.91.21.45:9092,119.91.21.45:9093,119.91.21.45:9094
    producer: # 生产者
      retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
      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: 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
    listener:
      # RECORD:
      # 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
      # BATCH:
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
      # TIME:
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
      # COUNT:
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
      # COUNT_TIME:
      # TIME | COUNT 有一个条件满足时提交
      # MANUAL:
      # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
      # MANUAL_IMMEDIATE:
      # 手动调用Acknowledgment.acknowledge()后立即提交
      ack-mode: MANUAL_IMMEDIATE

6.3、发送者代码

@RestController
public class KafkaController {

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

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

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

}

6.4、消费者代码

@Component
public class MyConsumer {

    /**
     * @param record
     * @KafkaListener(groupId = "testGroup", topicPartitions = {
     * @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
     * @TopicPartition(topic = "topic2", partitions = "0",
     * partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
     * },concurrency = "6")
     * //concurrency就是同组下的消费者个数,就是并发消费数,必须小于等于分区总数
     */
    @KafkaListener(topics = "my-replicated-topic", groupId = "xiexuGroup")
    public void listenZhugeGroup(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 = "tulingGroup")
    public void listenTulingGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        ack.acknowledge();
    }*/
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猿小羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值