在之前两篇文章当中,我们简单了解了kafka的基本概念和安装、集群搭建,那么接下来这一章我们就开始试着使用一下kafka,看看到底是怎么消费数据的。
1.在kafka的目录中存在着很多的脚本文件,在kafka中对应模块的对应操作脚本
topic
- kafka-topics.sh --[参数名1] [值] [参数名2] [值]... ,注意参数可以为多个,而值根据参数的需要而变化例:
# 查看所有主题,因为我们还没有创建任何主题,所以没有东西
[root@node1-zookeeper bin]# kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --list
# 我们可以指定要连接哪个节点的客户端,只需要在--bootstrap-server后指定IP或映射名+端口号即可
# 这里可以指定一个节点或多个节点。
kafka-topics.sh --bootstrap-server node1-zookeeper:9092,node2-zookeeper:9092 --list
- 常用参数
- 通过kafka命令创建一个topic
[root@node1-zookeeper bin]# kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --create --topic demo01 --partitions 1 --replication-factor 3
图中描述了各个命令的含义:
- 查看所有topic(所有主题)
[root@node1-zookeeper bin]# kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --list
demo01
- 查看我们刚才创建好的demo01主题的详细信息
[root@node1-zookeeper bin]# kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --topic demo01 --describe
Topic: demo01
TopicId: JP_DT173QCGOvfd7NjAJFw
PartitionCount: 1 #分区数量
ReplicationFactor: 3 #副本数
Configs: segment.bytes=1073741824
Topic: demo01 #主题名
Partition: 0 #当前分区
Leader: 0 #集群中的节点代理ID(borker.id)
Replicas: 0,2,1
Isr: 0,2,1
- 修改主题
# 将demo01的一个分区修改为三个分区
[root@node1-zookeeper kafka_2.12-3.0.0]# bin/kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --topic demo01 --alter --partitions 3
# 查看demo01主题的信息,此时就能看到三个分区了
[root@node1-zookeeper kafka_2.12-3.0.0]# bin/kafka-topics.sh --bootstrap-server node1-zookeeper:9092 --topic demo01 --describe
Topic: demo01 TopicId: JP_DT173QCGOvfd7NjAJFw PartitionCount: 3 ReplicationFactor: 3 Configs: segment.bytes=1073741824
Topic: demo01 Partition: 0 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: demo01 Partition: 1 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
Topic: demo01 Partition: 2 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
# 修改副本:副本不能通过命令行的方式进行修改,这里后续介绍
- 注意在修改已经创建好的主题分区时,分区数量只能增加不能减少,如:某个主题原来有两个分区,修改的时候就不能改为一个分区,分区数只能比原来的大。因为如果原来有两个消费者分别读取两个分区,如果改为一个,那么这两个消费者就不知道要去读取哪一个分区的数据。
生产和消费消息操作
- 发送消息到broker
[root@node1-zookeeper kafka_2.12-3.0.0]# bin/kafka-console-producer.sh --bootstrap-server node1-zookeeper:9092 --topic demo01
> 张三
> 李四
- 这里使用节点二和节点三消费消息
[root@node2-zookeeper kafka_2.12-3.0.0]# bin/kafka-console-consumer.sh --bootstrap-server node1-zookeeper:9092 --topic demo01
- 此时生产者发送消息时消费者客户端还未启动,当客户端启动完成后就不能读取到之前的历史消息,只能读取到生产者最新发送的消息
- 如果想要同样消费到历史数据,需要在消费者客户端添加参数 --from-beginning
所以读取方式有两种
比如读取demo01的数据,包含历史数据
[root@node2-zookeeper kafka_2.12-3.0.0]# bin/kafka-console-consumer.sh --bootstrap-server node1-zookeeper:9092 --topic demo01 --from-beginning
消息发送流程
- 生产者在main线程中创建一个Producer对象,通过一个send() 方法发送到拦截器(根据自己业务需求来定),然后到kafka自带的序列化器对数据进行序列化,通过分区器来规划数据发送往哪一个分区,其实就是缓存队列,缓存队列大小时32MB,其中每个批次的队列大小时16KB,其实这个是一个双端队列,当消息发送批次数据时创建该批次的大小,创建的时候会从内存池中取出内存,发送到kafka集群完成后将内存释放回收到内存池,接着Sender线程主动拉取队列,拉取数据的条件有两个:
- batch.size: 只有数据累计到batch.size之后,sender才会发送数据。默认为16KB
- linger.ms:如果数据迟迟未达到batch.size的大小,sender等待linger.ms设置的时间到了,也会发送数据,单位ms,默认是是0ms,表示没有延迟
- 当然这两个条件是‘或’的关系,要么数据满16KB,要么等待时间到了,就会发送,发送的队列数据会以节点的方式进行发送,在发送请求时如果没有及时应答,最多等待5个请求,只有其中某些请求响应回来了,等待队列不满五个了,才能发送新的请求。以此类推,通过Selector疏通,类似一个数据流通道,打通之后,开始将数据发送到borker(集群节点),集群收到之后会执行副本的同步,同步完成进行acks应答,应答有三种级别:
- 0:生产者发送过来的数据,不需要等待数据落盘应答。
- 1:生产者发送过来的数据,Leader收到数据之后应答。
- -1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据之后应答。-1和ALL等价
- 应答回来如果成功,则清除对应请求和分区的数据,如果失败则会进行重试上述发送步骤,重试的次数为int的最大值(该值可设置),直到成功为止。
JavaAPI的使用
1.导入测试项目依赖
<!--导入Kafka客户端对应依赖,与kafka版本对应即可-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.0.0</version>
</dependency>
2.kafka同步/异步发送,这里的异步发送和同步发送场景时是发生在外部数据发送到缓冲区队列中的过程
- Java示例:简单的消息发送和同步异步的区别
package com.moyuwanjia.kafka_study;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.Test;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
/**
* CustomProducer 生产者测试(同步/异步),这里的异步发送和同步发送场景时是发生在外部数据发送到缓冲区中的过程
* 外部数据 ——生产者——> 缓冲区 ——缓冲队列——> send线程 ——> kafka集群
* 调用get()方法:当使用get()方法发送消息时,它会阻塞当前线程,程序会等待get()方法方法的返回结果(成功或失败的响应)。
* 这意味着外部数据进入缓冲区队列时,等待上一批数据发送到kafka集群(borker节点)之后,才会将外部数据
* 继续送入缓冲区队列中。
*
* 不调用get()方法:不调用get()方法时,消息发送会是一个异步操作,并且当前线程不会进行阻塞,不会等待发送结果。
* 这时外部数据进入缓冲区队列时,不会等待上一批数据是否发送到kafka集群中,会直接将数据继续送入缓冲区队列中。
*
*/
public class demo01_CustomProducer {
/**
* 异步发送一条简单的消息
*/
@Test
public void test1(){
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定参数
ProducerRecord producerRecord = new ProducerRecord("demo01","第一条数据...");
// 4.发送数据
kafkaProducer.send(producerRecord);
// 5.关闭资源
kafkaProducer.close();
}
/**
* 异步发送一条携带回调的简单消息
*/
@Test
public void test2(){
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定参数
ProducerRecord producerRecord = new ProducerRecord("demo01","第一条数据...");
// 4.发送数据,添加回调方式一
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(null == e){
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
});
// 5.关闭资源
kafkaProducer.close();
}
/**
* 同步发送一条携带回调的简单消息
* 同步:在外部同一批次数据全部发送完毕之后,才能再次发送下一批数据
* 实现方式:直接在发送数据时调用get()方法即可,与异步发送方式执行原理不一致。
*/
@Test
public void test3() throws ExecutionException, InterruptedException {
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定参数
ProducerRecord producerRecord = new ProducerRecord("demo01","第一条数据...");
// 4.发送数据,添加回调方式一
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(null == e){
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
}).get();
// 5.关闭资源
kafkaProducer.close();
}
}
kafka中的分区器
- 拦截器:在实际应用中并不常见,这里先忽略
- 序列化器:我们在数据处理过程中常见的都是为String类型,其他自定义类型自行百度
- 分区器:
- ①:便于合理使用存储资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块块数据存储在多台Broker上。合理控制分区的任务,实现负载均衡的效果。
- ②:提高并行度,生产者可以以分区为单位发送数据,消费者可以以分区为单位进行消费。
分区策略
- ①指明partition的情况下,直接将指明的值作为partition的值,例如partition=0,所有数据写入分区0。
- ②没有指明partition值的情况下,将key的hash值与topic的partition进行取余数得到partition值,例如分区总数为2:key1的hash值=5,key2的hash值=6,topic的partition数=2,那么key1对应的value1写入1号分区(5%2=1),key2对应的value2写入0号分区(6%2=0)。
- ③即没有指定分区值又没有key值的情况下,kafka采用Sticky Partition(粘性分区器),会随机选择一个分区,并且尽可能一直使用该分区,等待该分区的batch已满(16KB)或者linger.ms时间到了,kafka会再次随机一个分区进行使用(和上一次的分区不同)例如:第一次随机选择0号分区,等待0号分区当前一批次的满了或时间到了,kafka会再随机一个分区进行使用,如果此时随机到了0号分区仍然不会使用,还会继续随机
package com.moyuwanjia.kafka_study;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.Test;
import java.util.Properties;
/**
* 默认分区器:
* @see org.apache.kafka.clients.producer.internals.DefaultPartitioner
*
* 分区策略:
* ①指明partition的情况下,直接将指明的值作为partition的值,例如partition=0,所有数据写入分区0。
* ②没有指明partition值的情况下,将key的hash值与topic的partition进行取余数得到partition值,例如,分区总数为2:
* key1的hash值=5,key2的hash值=6,topic的partition数=2,那么key1对应的value1写入1号分区(5%2=1),key2
* 对应的value2写入0号分区(6%2=0)。
* ③即没有指定分区值又没有key值的情况下,kafka采用Sticky Partition(粘性分区器),会随机选择一个分区,并且尽可能一直使用
* 该分区,等待该分区的batch已满(16KB)或者linger.ms时间到了,kafka会再次随机一个分区进行使用(和上一次的分区不同)
* 例如:第一次随机选择0号分区,等待0号分区当前一批次的满了或时间到了,kafka会再随机一个分区进行使用,如果此时随机
* 到了0号分区仍然不会使用,还会继续随机
*/
public class demo02_TestPartitioner{
/**
* 测试 指定分区策略
*/
@Test
public void test1(){
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定分区等参数
ProducerRecord producerRecord = new ProducerRecord("demo01",2,"","第一条数据...");
// 4.发送数据,添加回调方式一
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(null == e){
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
});
// 5.关闭资源
kafkaProducer.close();
}
/**
* 测试 指定不指定分区,指定key策略
*/
@Test
public void test2(){
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定key等参数
ProducerRecord producerRecord = new ProducerRecord("demo01","ThisIsAKeyValue","第一条数据...");
// 4.发送数据,添加回调方式一
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(null == e){
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
});
// 5.关闭资源
kafkaProducer.close();
}
/**
* 测试 既不指定分区,也不指定key策略
*/
@Test
public void test3() throws InterruptedException {
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定参数
for (int i = 0; i < 50; i++) {
// 4.发送数据,添加回调方式一
kafkaProducer.send(new ProducerRecord("demo01","第"+i+"条数据..."), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(null == e){
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
});
// 每次发送完成后等待
Thread.sleep(2);
}
// 5.关闭资源
kafkaProducer.close();
}
}
自定义分区器
- 测试:如研发人员根据企业需求,自己重新实现分区器。
- 需求:实现一个分区器,发送过来的数据中如果包含“张三”,就发送往0号分区,不包含“张三”就发往1号分区
- 用法:实现Partitioner类,并重写其中的方法
package com.moyuwanjia.kafka_study;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.Test;
import java.util.Map;
import java.util.Properties;
/**
* 自定义分区器
* 如研发人员根据企业需求,自己重新实现分区器。
* 需求:实现一个分区器,发送过来的数据中如果包含“张三”,就发送往0号分区,不包含“张三”就发往1号分区
*
* 实现Partitioner类,并重写其中的方法
*/
public class demo03_TestMyPartitioner implements Partitioner {
/**
* @param topic 主题名称
* @param key key值
* @param keyBytes 序列化后的key值
* @param value value值
* @param valueBytes 序列化后的value值
* @param cluster The current cluster metadata
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
return value.toString().contains("张三") ? 0 : 1;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
/**
* 测试 自定义分区策略
*/
@Test
public void test3() throws InterruptedException {
// 1.拼接和配置参数
Properties properties = new Properties();
// --bootstrap-servers参数
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1-zookeeper:9092,node2-zookeeper:9092,node3-zookeeper:9092");
// 指定key和value的序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 使用自定义分区器
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, demo03_TestMyPartitioner.class.getName());
// 2.创建kafka生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);
// 3.指定参数
for (int i = 0; i < 50; i++) {
// 4.发送数据,添加回调方式一
kafkaProducer.send(new ProducerRecord("demo01", "第" + (i % 2 > 0 ? "张三" : "李四") + "条数据"), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (null == e) {
System.out.println("主题:" + recordMetadata.topic() + "\n" +
"分区:" + recordMetadata.partition() + "\n" +
"偏移量:" + recordMetadata.offset() + "\n"
);
}
}
});
// 每次发送完成后等待
Thread.sleep(2);
}
// 5.关闭资源
kafkaProducer.close();
}
}
以上就是kafka的使用、主题、分区等概念