Apache Kafka
一、概述
Apache Kafka是一个分布式的流数据平台,代表三层含义:
- Publish/Subscribe: 消息队列系统 MQ(Message Queue)
- Process: 流数据的实时处理(Stream Process)
- Store: 流数据会以一种安全、容错冗余存储机制存放到分布式集群中
架构
应用场景
1.两大类:
- 构建实时的流数据管道,在系统和应用之间进行可靠的流数据传输
- 构建实时的流数据处理应用,对流数据进行转换和加工处理
2.实际场景举例:
- 活动跟踪:
跟踪网站用户和前端应用发生的交互,比如页面访问次数和点击,将这些信息作为消息发布到一个或者多个主题上,这样就可以根据这些数据为机器学习提供数据,更新搜素结果等等(头条、淘宝等总会推送你感兴趣的内容,其实在数据分析之前就已经做了活动跟踪)。 - 应用解耦:
消息队列将消息生产者和订阅者分离,实现应用解耦。如电商的订单系统与库存系统。用户下单,将消息写入消息队列,返回用户订单下单成功。库存操作根据订阅的下单消息进行,下单时库存系统不能正常使用也不影响下单。实现解耦。 - 流量削峰:
在应用前端以消息队列接收请求,当请求超过队列长度,直接不处理重定向至一个静态页面,来达到削峰的目的,此场景一般用于秒杀活动。 - 流处理:
现在非常流行的框架(如Storm,Spark Streaming、Flink)从topic中读取数据,实时对其进行处理,并将处理后的数据写入新topic中,供用户和应用程序使用。
核心概念
- Cluster: kafka支持一到多个服务构成的分布式集群,每一个服务实例成为
Broker
- Topic: 某一个分类的消息的集合,如:订单的topic、商品的topic等
- Partition: 一个Topic有若干个分区(Partition)构成,分区的数量在创建Topic时手动指定
- Replication: 分区副本,是Partition的冗余备份分区,当Partition不可用时,ZooKeeper会自动将Replication(Follower)分区升级为Partition(Leader)分区
- Offset: 分区中的Record的位置标示,每一个消费者都会记录自己的消费位置(offset)
- Producer:主题生产者。发布消息的对象
- Consumer:主题消费者。订阅并处理消息的对象
Topic和Log
Each partition is an ordered, immutable sequence of records that is continually appended to—a structured commit log
Kafka的每一个分区(Partition),都是一个有序、不可变的持续追加的记录序列,Kafka会以一种结构化的提交日志保存分区中的数据。
注意:在分区中写入数据时,会在队列的末尾进行追加,每一个消费者都维护的有一个自己的消费位置(offset)
二、基础使用
命令行操作
Topic使用
-
新建Topic
[root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic tt1 --partitions 3 --replication-factor 1 --create
-
展示Topic列表
[root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --list
-
删除Topic
[root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --delete --topic tt1
发布和订阅
-
发布消息
[root@spark kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --broker-list spark:9092 --topic tt1 >hello >五月 >hello >hello kafka
-
订阅消息
[root@spark kafka_2.11-2.2.0]# bin/kafka-console-consumer.sh --topic tt1 --bootstrap-server spark:9092 hello 五月 hello hello kafka
JAVA Driver
Maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.2.0</version>
</dependency>
生产者
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.UUID;
/**
* kafka 生产者的测试类
*/
public class ProducerDemo {
public static void main(String[] args) {
//1. 准备Kafka生产者配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"spark:9092");
// string 序列化(Object ---> byte[])器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
//2. 创建kafka生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//3. 生产记录并将其发布
ProducerRecord<String, String> record = new ProducerRecord<String, String>("tt1", UUID.randomUUID().toString(),"Hello Kafka");
producer.send(record);
//4. 释放资源
producer.flush();
producer.close();
}
}
1) Kafka的消息生产者,负责生产数据(Record K\V\Timestamp),最终发布(Publish)保存到Kafka集群
2)数据的保存策略:
- 如果 Record的 Key不为 Null,采用哈希算法: key.hashCode % numPartitions = 余数(分区序号)
- 如果 Record的 Key为 Null, 采用轮询策略
- 手动指定存放的分区
3) 数据会以一种分布式的方式保存在 Kafka集群中,每一个分区都会维护一个队列的数据结构,新产生的数据会追加到队列的末尾,并且分配 offset
4)数据在 Kafka集群中默认最多保留 7天(168Hours),不论是否消费,在保留周期到达后都会自动被删除。
5)数据在 Kafka中可以进行重复消费,重置消费 offset即可
消费者
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
/**
* kafka消费者测试类
* 1. 订阅 subscribe
* 2. 拉取 pull
*/
public class ConsumerDemo {
public static void main(String[] args) {
//1. 指定kafka消费者的配置信息
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
// 反序列化器 byte[] ---> Object
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 消费组必须得指定
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group1");
//2. 创建kafka消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//3. 订阅主体topic
consumer.subscribe(Arrays.asList("t2"));
//4. 拉取新产生的记录
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.key() + "\t" + record.value() + "\t"
+ record.topic() + "\t" + record.offset()
+ "\t" + record.timestamp() + "\t" + record.partition());
}
}
}
}
1)消费者并不是独立存在,kafka中消费者会以消费组的方式进行组织和管理
2)消费组符合特征: 组外广播、组内负载均衡
- 组外广播: 保证不同的消费组,能够独立消费新产生的数据
- 组内负载均衡: 消息只会被消费组中的一个消费者进行处理,多个消费组提高了Kafka并行处理能力
3)消费者可以订阅一个到多个感兴趣的Topic,一旦这些Topic有新的数据产生,消费者会自动拉取新产生的数据,进行相应的业务处理
4)消费者在消费消息时,会维护一个消费的位置(offset),下一次消费时会自动从offset向后进行消费。
在kafka中数据会有一个默认的保留周期(7天),在保留期内数据是可以进行重复消费的,只需要重置消费者消费的offset即可。
5)
__consumer_offsets
是一个特殊topic,主要记录了Kafka消费组的消费位置。
三、其他使用
偏移量控制
Kafka消费者在订阅Topic时,会自动拉取Topic中新产生的数据。首次消费时使用默认的偏移量消费策lastest
偏移量消费策略:
-
lastest(默认):如果有已提交的offset,从已提交的offset之后消费消息。如果无提交的offset,从最后的offset之后消费数据
-
earliest:如果有已提交的offset,从已提交的offset之后消费消息。如果无提交的offset,从最早的offset消费消息
// 注意:此配置项 修改偏移量消费策略的默认行为 properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
Kafka消费者消费位置offset,默认采用自动提交的方式,将消费位置提交保存到特殊Topic_consumer_offsets中
自动提交策略:
// 默认自动提交消费的位置offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
// 默认每隔5秒提交一次消费位置
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,5000);
通常情况需要手动提交消费位置:
为什么需要手动提交消费位置(offset)的原因?
原因:如果自动提交消费位置,有可能在进行业务处理时出现错误,会造成数据没有被正确处理。
手动提交消费位置,可以保证数据一定能够被完整的正确处理。
// 关闭消费位置offset的自动提交功能
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
// 手动提交消费位置
consumer.commitSync();
消费方式
订阅(Subscribe)
消费者订阅1到N个感兴趣的Topic,一旦Topic中有新的数据产生,会自动拉取Topic分区内的所有数据
// 订阅(消费)Topic所有的分区
consumer.subscribe(Arrays.asList("t3"));
指定消费分区
消费者在消费数据时,可以只消费某个Topic特定分区内的数据
// 指定消费Topic的特定分区
consumer.assign(Arrays.asList(new TopicPartition("t3",0)));
重置消费位置
消费者在消费数据时,可以重置消费的offset,消费已消费的数据或者跳过不感兴趣的数据
consumer.assign(Arrays.asList(new TopicPartition("t3",0)));
// 重置消费位置
consumer.seek(new TopicPartition("t3",0),1);
生产者的批量发送
kafka生产者产生的多条数据共享同一个连接,发送保存到Kafka集群,这种操作方式称为:Batch(批处理)。
批处理相比于传统的发送方式,资源利用率更为高效,是一种比较常用的生产者优化策略。
使用方法
# 生产者方 添加如下配置项即可
# 两个条件 满足其一即可
batch.size = 16384Bytes 16kb// 缓冲区大小
linger.ms = 毫秒值 // 缓冲区中数据的驻留时长
具体使用方法
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
properties.put(ProducerConfig.LINGER_MS_CONFIG,2000);
Kafka和Spring Boot整合
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件
spring.kafka.bootstrap-servers= HadoopNode01:9092,HadoopNode02:9092,HadoopNode03:9092
spring.kafka.consumer.group-id= g1
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
生产者API
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.UUID;
@Component
public class KafkaProducerDemo {
@Autowired
private KafkaTemplate<String,String> template;
// 计划任务,定时发送数据
// cron 秒 分 时 日 月 周 年(省略)
@Scheduled(cron = "0/10 * * * * ?")
public void send(){
template.send("tt1", UUID.randomUUID().toString(),"Hello Kafka");
//System.out.println(new Date());
}
}
消费者API
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class KafkaConsumerDemo {
@KafkaListener(topics = "tt1")
public void receive(ConsumerRecord<String, String> record) {
System.out.println(record.key() + "\t" + record.value());
}
}
生产者幂等操作
幂等: 指的多次操作,影响结果是一致的,这种操作方式就被成为幂等操作
结论:使用Kafka生产者幂等操作原因,kafka生产者在重试发送生产数据时,多次重试操作只会在Kafka的分区队列的末尾写入一条记录
使用方法
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等操作支持
// ack时机 -1或者all 所有 1 leader 0 立即应答
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,5); // 重复次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000); // 请求超时时间
Kafka事务
数据库事务: 一个连接中多个操作不可分割,是一个整体,要么同时成功,同时失败。
Kafka的事务类似于数据库事务,每一个事务操作都需要一个唯一的事务ID(Transaction-ID
),并且事务默认的隔离级别为READ_UNCOMMITTED
和READ_COMMITTED
生产者事务
生产者事务: Kakfka生产者生产的多条数据是一个整体,不可分割,要么同时写入要么同时放弃
要求
- kafka生产者提供唯一的事务ID
- 必须开启kafka的幂等性支持
事务操作
- 初始化事务
- 开启事务
- 正确操作 提交事务
- 操作失败 回滚事务
生产者API
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.UUID;
/**
* kafka 生产者的测试类
*/
public class ProducerDemo {
public static void main(String[] args) {
//1. 准备Kafka生产者配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"spark:9092");
// string 序列化(Object ---> byte[])器
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);
// 事务ID, 唯一不可重复
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,UUID.randomUUID().toString());
// 开启幂等操作支持
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
// ack时机 -1或者all 所有 1 leader 0 立即应答
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,5); // 重复次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000); // 请求超时时间
//2. 创建kafka生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
// 初始化事务
producer.initTransactions();
// 开启事务
producer.beginTransaction();
try {
//3. 生产记录并将其发布
for (int i = 50; i < 60; i++) {
if(i == 56) {
int m = 1/0; //人为制造错误
}
// key不为null 第一种策略
ProducerRecord<String, String> record = new ProducerRecord<String, String>("tt1", UUID.randomUUID().toString(),"Hello Kafka"+i);
// key为null 轮询策略
producer.send(record);
}
// 提交事务
producer.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
// 取消事务
producer.abortTransaction();
} finally {
//4. 释放资源
producer.flush();
producer.close();
}
}
}
消费者API
// 修改消费者默认的事务隔离级别,consumer只能读取已成功提交事务的消息
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");
消费生产并存事务
consume-transform-produce:指消费和生产处于同一个事务环境中,要么消费生产同时成功,要么同时失败
要求
- kafka生产者提供唯一的事务ID
- 必须开启kafka的幂等性支持
- 关闭offset的自动提交功能
- 不能调用手动提交的方法,如: consumer.commitSync()
创建消费Topic,以及发布的Topic
[root@HadoopNode01 kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic t6 --partitions 3 --replication-factor 1 --create
[root@HadoopNode01 kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic t7 --partitions 3 --replication-factor 1 --create
核心代码
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.*;
/**
* 消费生产并存事务
*/
public class ConsumeTransformProduceDemo {
public static void main(String[] args) {
//1. 初始化生产者和消费者的配置对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(consumerConfig());
KafkaProducer<String, String> producer = new KafkaProducer<>(producerConfig());
//2. 消费者订阅topic
consumer.subscribe(Arrays.asList("t6"));
//3. 事务操作
producer.initTransactions();
while (true) {
producer.beginTransaction();
try {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
// 需要业务处理的内容
System.out.println(record.key() + "--->" + record.value());
producer.send(new ProducerRecord<String,String>("t7","t7:"+record.value()));
// 将消费位置记录到map集合中
offsets.put(new TopicPartition("t6",record.partition()),new OffsetAndMetadata(record.offset()+1));
}
// 维护消费位置 将事务内的消费位置信息 提交到kafka中
producer.sendOffsetsToTransaction(offsets,"g1");
// 正确操作 提交事务
producer.commitTransaction();
} catch (Exception e) {
e.printStackTrace();
producer.abortTransaction();
}
}
}
public static Properties producerConfig() {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString());
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, Boolean.TRUE);
properties.put(ProducerConfig.RETRIES_CONFIG, 5);
properties.put(ProducerConfig.ACKS_CONFIG, "all");
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000);
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
properties.put(ProducerConfig.LINGER_MS_CONFIG, 2000);
return properties;
}
public static Properties consumerConfig() {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
return properties;
}
}