目录
1、Java编程操作Kafka
1.1、同步生产消息到Kafka中
1.1.1、需求
编写Java程序,将1-100的数字消息写入到Kafka中。
1.1.2、准备工作
1、导入Maven Kafka POM依赖
<!-- 代码库 -->
<repositories>
<repository>
<id>central</id>
<url>http://maven.aliyun.com/nexus/content/groups/public//</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
</repository>
</repositories>
...
<dependencies>
<!-- Kafka -->
<!-- Kafka客户端工具 -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<!-- SLF桥接Log4J日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.6</version>
</dependency>
<!-- slog4j日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
2、导入log4j.properties
将log4j.properties配置文件放入到resource文件件中。
log4j.rootLogger=INFO,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%Sp - %m%n
3、创建包和类
创建cn.itcast.kafka,并创建KafkaProducerTest类。
1.1.3、生产者程序开发
- 创建用于连接Kafka的Properties配置;
创建连接: bootstrap.servers:Kafka的服务器地址 acks:表示当生产者生产数据到kafka中,Kafka中会以什么样的策略返回 key.serializer:Kafka中的消息是以key、value键值对存储的,而且生产者生产的消息是需要在网络上传递的,这里指定的是StringSerializer方式,就是以字符串方式发送(将来还可以使用其他的一些序列化框架:Google Protobuf、Avro) value.serializer:同上 Properties props = new Properties(); props.put("bootstrap.servers", "192.168.88.100:9092"); props.put("acks", "all"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
- 创建一个生产者对象KafkaProducer;
- 调用send()发送1-100消息(ProducerRecord,封装的是key-value键值对)到指定Topic test,并获取返回值Future,该对象封装了返回值;
- 再调用一个Future.get()方法表示等待服务端的响应;
- 关闭生产者。
参考代码:
/**
* Kafka的生产者程序
* 会将消息创建出来,并发送到Kafka集群中
*/
public class KafkaProducerTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、创建用于连接Kafka的Properties配置
Properties props = new Properties();
props.put("bootstrap.servers", "127.0.0.1:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 2、创建一个生产者对象KafkaProducer
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(props);
// 3、发送1-100的消息到指定的topic中
for (int i = 0; i < 100; i++) {
// 构建一条消息 ProducerRecord
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test-topic", null, i + "");
Future<RecordMetadata> future = kafkaProducer.send(producerRecord);
// 调用Future.get()方法等待响应
future.get();
System.out.println("第" + i + "条消息写入成功!");
}
// 4、关闭生产者
kafkaProducer.close();
}
}
1.2、从Kafka的topic中消费消息
1.2.1、需求
从test-topic中,将消息都消费,并将记录的offset、key、value打印出来。
1.2.2、准备工作
在cn.itcast包下创建KafkaConsumerTest类。
1.2.3、消费者程序开发
- group.id:消费者组的概念,可以在一个消费组中包含多个消费者。如果若干个消费者的group.id是一样的,表示它们就在一个组中,一个组中的消费者是共同消费Kafka中topic的数据。
- Kafka是一种拉消息模式的消息队列,在消费者中会有一个offset,表示从哪条消息开始拉取数据。
- KafkaConsumer.poll:Kafka的消费者API是一批一批数据的拉取。
- 关闭consumer一直打印debug日志的方式,创建资源文件logback.xml,添加如下内容:
<configuration scan="true" scanPeriod="10 seconds">
<include resource="org/springframework/boot/logging/logback/base.xml" />
<!-- 屏蔽kafka debug -->
<logger name="org.apache.kafka.clients" level="INFO" />
</configuration>
开发步骤:
参考代码:
/**
* 消费者程序
*/
public class KafkaConsumerTest {
public static void main(String[] args) {
// 1、创建Kafka消费者配置
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "127.0.0.1:9092");
// 消费者组(可以使用消费者组,将若干个消费者组织到一起,共同消费Kafka中topic的数据)
// 每一个消费者需要指定一个消费者组,如果消费者的组名一样,就表明这几个消费者是一个组的
properties.setProperty("group.id", "test");
// 自动提交offset
properties.setProperty("enable.auto.commit", "true");
// 自动提交offset的时间间隔
properties.setProperty("auto.commit.interval.ms", "1000");
// 拉取的key、value数据的反序列化方式
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 2、创建Kafka的消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 3、订阅要消费的主题
// 指定消费者从哪个topic中拉取数据
kafkaConsumer.subscribe(Arrays.asList("test-topic"));
// 4、使用一个while循环,不断从Kafka的topic中拉取消息
while(true) {
// Kafka的消费者一次拉取一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
// 5、将记录(ConsumerRecords)的offset、key、value都打印出来
for(ConsumerRecord<String, String> consumerRecord : consumerRecords) {
// 主题名字
String topic = consumerRecord.topic();
// offset
long offset = consumerRecord.offset();
// key / value
String key = consumerRecord.key();
String value = consumerRecord.value();
System.out.println("topic:" + topic + " offset:" + offset + " key:" + key + " value" + value);
}
}
}
}
1.3、异步使用带有回调函数方法生产消息
如果我们想获取生产者消息是否成功,或者成功生产消息到Kafka中后,执行一些其它动作。此时,可以很方便地使用带有回调函数来发送消息。
需求:
- 在发送消息出现异常时,能够及时打印出异常消息;
- 在发送消息成功时,打印Kafka到的topic名字、分区id、offset。
使用匿名内部类实现Callback接口,该接口中表示Kafka服务器响应给客户端,会自动调用onCompletion()方法:
- metadata:消息的元数据(属于哪个topic、属于哪个partition、对应的offset是什么);
- exception:这个对象Kafka生产消息封装了出现的异常,如果为null,表示发送成功,如果不为空,标识出现异常。
/**
* Kafka的生产者程序
* 会将消息创建出来,并发送到Kafka集群中
*/
public class KafkaProducerTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、创建用于连接Kafka的Properties配置
Properties props = new Properties();
props.put("bootstrap.servers", "127.0.0.1:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 2、创建一个生产者对象KafkaProducer
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(props);
// 3、发送1-100的消息到指定的topic中
for (int i = 0; i < 100; i++) {
// 方式1:使用同步等待方式发送消息
// // 构建一条消息 ProducerRecord
// ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test-topic", null, i + "");
// Future<RecordMetadata> future = kafkaProducer.send(producerRecord);
// // 调用Future.get()方法等待响应
// future.get();
// System.out.println("第" + i + "条消息写入成功!");
// 方式2:使用异步回调的方式发送消息
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test-topic", null, i + "");
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
// 1、判断发送消息是否成功
if (e == null) {
// 发送成功
// 主题
String topic = recordMetadata.topic();
// 分区id
int partition = recordMetadata.partition();
// 偏移量
long offset = recordMetadata.offset();
System.out.println("topic:" + topic + " 分区id:" + partition + " 偏移量:" + offset);
} else {
// 发送失败
System.out.println("生产消息出现异常!");
// 打印异常消息
System.out.println(e.getMessage());
// 打印调用栈
System.out.println(e.getStackTrace());
}
}
});
}
// 4、关闭生产者
kafkaProducer.close();
}
}
2、Kafka架构
2.1、Kafka重要概念
2.1.1、broker
- Kafka服务器进程,生产者、消费者都要连接broker;
- 一个Kafka的集群通常由多个braker组成,这样才能实现负载均衡,以及容错;
- braker是无状态(Stateless)的,它们是通过Zookeeper来维护集群状态;
- 一个Kafka的broker每秒可以处理数十万次读写,每个broker都可以处理TB消息而不影响性能。
2.1.2、zookeeper
- ZK用来管理和协调broker,并且存储了Kafka的元数据(例如:有多少topic、partition、consumer);
- ZK服务主要用于通知生产者和消费者Kafka急群众有新的broker加入、或者Kafka集群中出现故障的broker。
Kafka正在逐步想办法将Zookeeper剥离,维护两套集群成本较高,社区提出KIP-500就是要替换掉ZooKeeper的依赖。“Kafka on Kafka”——Kafka自己来管理自己的元数据。
2.1.3、producer(生产者)
生产者负责将数据推送给broker的topic。
2.1.4、consumer(消费者)
消费者负责从broker的topic中拉取数据,并自己进行处理。
2.1.5、consumer group(消费者组)
- consumer group是kafka提供的可扩展且具有容错性的消费者机制;
- 一个消费者组可以包含多个消费者;
- 一个消费者组有一个唯一的ID(group id);
- 组内的消费者一起消费主题的所有分区数据。
2.1.6、分区(Partitions)
在Kafka集群中,主题topic被分为多个分区partition。Kafka集群的分布式就是由分区来实现的,一个topic中的消息可以分布在topic的不同partition中。
2.1.7、副本(replicas)
副本可以确保某个服务出现故障时,确保数据依然可用。在Kafka中,一般都会设计副本的个数>1。
2.1.8、主题(topic)
- 主题是一个逻辑概念,用于生产者发布数据,消费者拉取数据;
- Kafka中的主题必须要有标识符,而且是唯一的,Kafka中可以有任意数量的主题,没有数量上的限制;
- 在主题中的消息是有结构的,一般一个主题包含某一类消息;
- 一旦生产者发送消息到主题中,这些消息就不能被更新(更改)。
2.1.9、偏移量(offset)
- offset记录着下一条将要发送给Consumer的消息的序号,相对消费者来说,可以通过offset来拉取数据;
- 默认Kafka将offset存储在ZooKeeper中;
- 在一个分区中,消息是有顺序的方式存储着,在每个分区的消费都是有一个递增的id。这个就是偏移量offset;
- 偏移量在分区中才是有意义的。在分区之间,offset是没有任何意义的。
2.2、消费者组
- 一个消费者组中可以包含多个消费者,共同来消费topic中的数据;
- 一个topic中如果只有一个分区,那么这个分区只能被某个组中的一个消费者消费;
- 有多少个分区,那么就可以被同一个组的多少个消费者消费。
3、Kafka生产者幂等性与事务
3.1、幂等性
3.1.1、简介
拿http举例来说,一次或多次请求,得到的响应是一致的(网络超时等问题除外),换句话说,就是执行多次操作与执行一次操作的影响是一样的。
3.1.2、Kafka生产者幂等性
在生产者生产消息时,如果出现retry时,有可能会一条消息发送了多次,如果Kafka不具备幂等性的话,就有可能会在partition中保存多条一模一样的消息。
3.1.3、配置幂等性
props.setProperty("enable.idempotence", true);
3.1.4、幂等性原理
为了实现生产者的幂等性,Kafka引入了Producer ID(PID)和Sequence Number的概念。
- PID:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的;
- Sequence Number:针对每个生产者(对应PID)发送到指定主题分区的消息都是对应一个从0开始递增的Sequence Number。
生产者消息重复问题:Kafka生产者生产消息到partition,如果直接发送消息,Kafka会将消息保存到分区中,但Kafka会返回一个ack给生产者,表示当前操作是否成功,是否已经保存了这条消息,如果ack响应的过程中失败了,此时生产者会重试,继续发送没有发送成功的消息,Kafka又会保存一条一模一样的消息。
在Kafka中可以开启幂等性:当Kafka的生产者生产消息时,会增加一个pid(生产者的唯一编号)和sequence number(针对消息的一个递增序列);发送消息,会连着pid和sequence number一并保存下来;如果ack响应失败,生产者重试,Kafka会根据pid、sequence number是否需要再保存一条消息;判断条件:生产者发送过来的sequence number是否小于等于partition中消息对应的sequence。