三天半
http://kafka.apache.org/
所有集合类型都是Iterable
流计算原理都是一样的,这个学会了,spark,flink就好学了
分布式流数据平台(流数据:连续的,源源不断的;对数据实时的)
MapperReducer批处理(数据是有限的)
Kafka具备三项关键能力:
发布和订阅(Record),类似于消息队列(MQ)或者企业级消息系统
流数据的存储
流数据的实时处理和加工
Kafka的应用场景:
Producer:(A系统)生产数据的一方,其生产的数据叫做record【k.hashCode%topicPartitionNum=分区序号】,发布到一个或多个topic中
record:(由key,value,时间戳构成)每一个record都有一个offset(偏移量),代表其在分区内的位置标示
Offset:标示读写操作的位置,读的offset<=写的offset
Consumer:(B系统)订阅数据(subscribe Topic),拉去数据(poll record)
Broker:中间人或者代理人,一个kafka服务实例,里面管理0到n个主题
Topic:主题,某一个记录流的类别,可以人为的进行数据分区,一个主题实际上是由多个数据分区组成。
Partition:分区,针对于主题,一个主题由多个数据分区构成,一个分区有序不可变的recorder序列
Replication:复制,每一个主题的分区都有一个复制分区,主要是将主题主分区中的数据备份,主分区和复制分区一定不在同一个Broker。故障恢复
replication-factor:复制因子(包含主分区本身)
Leader :主分区称为leader ,读写操作默认使用主分区
Follower复制分区称为follower,会实时同步主分区中的数据(数据的冗余备份),当主分区不可用时,某个follower分区,会自动升级为主分区
zookeeper:管理kafka元数据,kafka中的实例都会和zookeeper进行会话,都是zookeeper中的临时节点
分区结构:类似于队列Queue(先进先出FIFO),根据时间戳有序的进行排序
Kafka的四大核心API:
Producer
产生Record
发送给指定的Kafka Topic(Topic实质是有多个分区构成,每一个分区都会相应的复制分区),在真正存放到Kafka集群时会进行计算key.hashCode%topicPartitionNums等于要存放的分区序号。Leader
分区中的数据会自动同步到Follower
分区中,ZooKeeper
会实时监控服务健康信息,一旦发生故障,会立即进行故障转移操作(将一个Follower复制分区自动升级为Leader主分区)Queue
(符合队列的数据结构,先进先出), 分区中新增的数据,会添加到队列的末尾,在处理时,会从队列的头部开始消费数据。Queue
在标识读写操作位置时,会使用一个offset
(读的offset <= 写的offset)Consumer
会订阅一个Kafka Topic,一旦Topic中有新的数据产生,Conumser立即拉取最新的记录,进行相应的业务处理。kafka完全分布式集群
[root@node1 ~]# scp kafka_2.11-2.2.0.tgz root@node2:~
kafka_2.11-2.2.0.tgz 100% 61MB 100.2MB/s 00:00
[root@node1 ~]# scp kafka_2.11-2.2.0.tgz root@node3:~
kafka_2.11-2.2.0.tgz
[root@nodex ~]# tar -zxf kafka_2.11-2.2.0.tgz -C /usr
[root@node3 ~]# cd /usr/kafka_2.11-2.2.0/
[root@node1 kafka_2.11-2.2.0]# vi config/server.properties
@node1 broker.id=0
@node2 broker.id=1
@node3 broker.id=2
# 第二个虚拟机使用 PLAINTEXT://node2:9092
# 第三个虚拟机使用 PLAINTEXT://node3:9092
listeners=PLAINTEXT://node1:9092
# kafka服务数据存放目录
log.dirs=/usr/kafka_2.11-2.2.0/data
# kafka集群中的数据最多保留7天 默认的就是不用修改
log.retention.hours=168
zookeeper.connect=node1:2181,node2:2181,node3:2181
[root@nodex kafka_2.11-2.2.0]# bin/kafka-server-start.sh -daemon config/server.properties
[root@node1 kafka_2.11-2.2.0]# jps
1777 QuorumPeerMain
96294 Kafka # kafka的服务实例
[root@nodex kafka_2.11-2.2.0]# bin/kafka-server-stop.sh config/server.properties
[root@node1 kafka_2.11-2.2.0]#
注意:
--create
: 创建动作
--topic
: 操作的主题名
--partitions
: Leader分区的数量
--replication-factor
: 复制因子,包含Leader分区本身
--bootstrap-server
: Kafka集群地址列表
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --list --bootstrap-server node1:9092,node2:9092,node3:9092
baizhi
注意:
--list
: 展示主题列表
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092
Topic:baizhi PartitionCount:3 ReplicationFactor:3 Configs:segment.bytes=1073741824
Topic: baizhi Partition: 0 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: baizhi Partition: 1 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: baizhi Partition: 2 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092
[2019-08-22 17:49:40,572] WARN [AdminClient clientId=adminclient-1] Connection to node -2 (node2/192.168.12.131:9092) could not be established. Broker may not be available. (org.apache.kafka.clients.NetworkClient)
Topic:baizhi PartitionCount:3 ReplicationFactor:3 Configs:segment.bytes=1073741824
Topic: baizhi Partition: 0 Leader: 0 Replicas: 1,0,2 Isr: 0,2
Topic: baizhi Partition: 1 Leader: 0 Replicas: 0,2,1 Isr: 0,2
Topic: baizhi Partition: 2 Leader: 2 Replicas: 2,1,0 Isr: 2,0
Kafka早期版本是不允许删除Topic,需要在配置文件 server.properties 添加一行配置
delete.topic.enable=true
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --delete --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092
注意:分区数量只能从小改到大,从大修改到小会报错
[root@node3 kafka_2.11-2.2.0]# bin/kafka-topics.sh --alter --topic baizhi --partitions 1 --bootstrap-server node1:9092,node2:9092,node3:9092
Error while executing topic command : org.apache.kafka.common.errors.InvalidPartitionsException: Topic currently has 2 partitions, which is higher than the requested 1.
[2019-08-23 00:25:04,070] ERROR java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.InvalidPartitionsException: Topic currently has 2 partitions, which is higher than the requested 1.
at org.apache.kafka.common.internals.KafkaFutureImpl.wrapAndThrow(KafkaFutureImpl.java:45)
at org.apache.kafka.common.internals.KafkaFutureImpl.access$000(KafkaFutureImpl.java:32)
at org.apache.kafka.common.internals.KafkaFutureImpl$SingleWaiter.await(KafkaFutureImpl.java:89)
at org.apache.kafka.common.internals.KafkaFutureImpl.get(KafkaFutureImpl.java:260)
at kafka.admin.TopicCommand$AdminClientTopicService.alterTopic(TopicCommand.scala:200)
at kafka.admin.TopicCommand$.main(TopicCommand.scala:62)
at kafka.admin.TopicCommand.main(TopicCommand.scala)
Caused by: org.apache.kafka.common.errors.InvalidPartitionsException: Topic currently has 2 partitions, which is higher than the requested 1.
(kafka.admin.TopicCommand$)
# baizhi topic 分区数量修改为2个
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092
Topic:baizhi PartitionCount:1 ReplicationFactor:1 Configs:segment.bytes=1073741824
Topic: baizhi Partition: 0 Leader: 2 Replicas: 2 Isr: 2
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --alter --topic baizhi --partitions 2 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092 Topic:baizhi PartitionCount:2 ReplicationFactor:1 Configs:segment.bytes=1073741824
Topic: baizhi Partition: 0 Leader: 2 Replicas: 2 Isr: 2
Topic: baizhi Partition: 1 Leader: 0 Replicas: 0 Isr: 0
[root@node1 kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --topic baizhi --broker-list node1:9092,node2:9092,node3:9092
>Hello Kafka
>Hello World
[root@node2 kafka_2.11-2.2.0]# bin/kafka-console-consumer.sh --topic baizhi --bootstrap-server node1:9092,node2:9092,node3:9092 --from-beginning
Hello Kafka
Hello World
Kafka的一个Topic(主题)在集群内部实际是有1到N个Partition(分区)构成,每一个Partition都是一个有序的,持序追加的Record队列,一个Partition是一个structured commit log(结构化的提交日志)。
Record持久化存放在Kafka集群中的,Record会有一个保留周期(默认是168小时),如果超过保留期,不论Record是否消费,Kafka都会丢弃。理论上来说,Kafka容量为维持在一个合理的范围区间之内(不断的产生新数据,不断丢失过期的数据)
kafka中的每一个Record在分区中都一个唯一的offset,offset会随着分区不断写入数据有序递增。并且Consumer会保留一个元数据(offset | position,记录了Consumer消费的位置),Kafka这种机制为我们提供Record重复处理或者跳过不感兴趣Record处理的功能
Kafka Producer(生产者)是用来产生持续的Record,并发布到kafka某一个或者多个Topic中.
一个Record是由key,value,timestamp
构成
发布Record时的策略:
key = null
: 轮询Partitionkey != null
: key.hashCode % partitionNum = 存放Record分区的序号手动指定Partition序号
: Producer一方在发布Record时手动指定要存放的分区序号Kafka Comsumer(消费者)订阅一个或者多个感兴趣的Topic,一旦这些Topic中有新的数据产生,会立即拉取到本地(Consumer)一方,进行相应的业务处理。
Kafka使用Consumer Group
组织管理Conumser,Consumer Group特点: 组外广播,组内负载均衡
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-clientsartifactId>
<version>2.2.0version>
dependency>
192.168.12.130 node1
192.168.12.131 node2
192.168.12.132 node3
package pubsub;
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.IntegerSerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaProducerDemo {
public static void main(String[] args) {
//1. 创建配置对象 指定Producer的信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); // 对record的key进行序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
//2. 创建Producer对象
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(properties);
//3. 发布消息
ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>("t1",1,"Hello World");
producer.send(record);
//4. 提交
producer.flush();
producer.close();
}
}
package pubsub;
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.IntegerDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1. 创建配置对象 指定Consumer信息
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); // k v按照反序列化进行解析
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1"); // 消费组的ID 同组负载均衡 不同组广播
/*k v必须按照反序列化进行解析 不然会报如下错误
Caused by: org.apache.kafka.common.KafkaException: org.apache.kafka.common.serialization.IntegerSerializer is not an instance of org.apache.kafka.common.serialization.Deserializer*/
//2. 创建消费者
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<Integer, String>(properties);
//3. 订阅主题
consumer.subscribe(Arrays.asList("t1"));
//4. 拉取主题内新增的数据
while (true) {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(5));// 拉取的超时时间
for (ConsumerRecord<Integer, String> record : records) {
// 1 HelloWorld xxxx 0
System.out.println(record.key() + " | " + record.value() + " | " + record.timestamp() + " | " + record.offset() +" | "+ record.partition());
}
}
}
}
package topic;
import org.apache.kafka.clients.admin.*;
import org.apache.kafka.common.KafkaFuture;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
/**
* topic: 创建 删除 展示列表 查看详情 修改(不支持)
*/
public class TopicManagerDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建配置对象 指定Topic管理信息
Properties properties = new Properties();
properties.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,"node1:9092,node2:9092,node3:9092");
AdminClient client = AdminClient.create(properties);
//2. 创建Topic
//client.createTopics(Arrays.asList(new NewTopic("t2",3,(short)3)));
//3. 展示Topic列表
/*
ListTopicsResult result = client.listTopics();
Set topics = result.names().get();
// topics.forEach(topic -> System.out.println(topic));
for (String topic : topics) {
System.out.println(topic);
}
*/
//4. 查看topic详情
/*
DescribeTopicsResult result = client.describeTopics(Arrays.asList("t1"));
Map> values = result.values();
// values.forEach((k,v) -> System.out.println(k +" | " +v));
for (Map.Entry> entry: values.entrySet()) {
System.out.println(entry.getKey() + " | "+ entry.getValue().get());
}
*/
//5. 删除topic
client.deleteTopics(Arrays.asList("t2"));
// 关闭连接
client.close();
}
}
Kafka的消费者在消费Record时会通过offset
记录消费位置。
Kafka会使用一个特殊的topic __consumer_offsets
记录消费组的读的offset,__consumer_offsets
由50个分区构成,每一个分区包含它本身有3个冗余备份。消费者在消费消息时,会根据所属的消费组的ID,去__consumer_offsets
的特殊Topic中查找上一个提交的offset,然后从offset +1的位置继续消费。
Kafka偏移量的提交策略有两种:
自动提交(默认):默认情况下kafka consumer在拉取完分区内的数据后,会自动提交读的offset
手动提交:大多数情况自动提交offset可以满足我们的需求,但是在一些特殊情况,我们需要手动提交offset。保证分区内数据都能够得到正确的处理。
# 关闭kafka consumer的offset自动提交
enable.auto.commit = false
package 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.IntegerDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class KafkaConsumerDemo {
public static void main(String[] args) {
//1. 创建配置对象 指定Consumer信息
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class); // k v按照反序列化进行解析
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1"); // 消费组的ID 同组负载均衡 不同组广播
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false); // 关闭kafka consumer的offset自动提交功能
//2. 创建消费者
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<Integer, String>(properties);
//3. 订阅主题
consumer.subscribe(Arrays.asList("t3"));
//4. 拉取主题内新增的数据
while (true) {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(5));// 拉取的超时时间
for (ConsumerRecord<Integer, String> record : records) {
// 业务处理
// 支付 加积分 减去优惠券
// ...
}
// 手动提交offset偏移量
consumer.commitAsync();
}
}
}
subscribe订阅一个或者多个主题,一旦主题中有新的数据产生,consumer会收到topic内所有分区内的消息
//3. 订阅主题
consumer.subscribe(Arrays.asList("t1"));
指定消费的topic的分区序号,一旦分区序号内有新的数据产生,consumer会收到特定分区的新产生的记录
// 分配方法: 只消费t1主题0号分区内的消息
consumer.assign(Arrays.asList(new TopicPartition("t1",0)));
手动控制消费的offset,可以重置offset重复处理已经处理过的消息,设定offset跳过不感兴趣的消息
// 分配方法: 只消费t1主题0号分区内的消息
consumer.assign(Arrays.asList(new TopicPartition("t1",0)));
// 手动指定消费的offset:从t1主题的0号分区offset=0的位置开始消费消息
// 自动读offset失效
consumer.seek(new TopicPartition("t1",0),0);
NOTE:
如果手动控制消费位置,offset提交策略就没有作用了,因为每次都是从指定的offfset位置开始向后消费消息
网络中传输的都是字节数组
public interface Serializer<T> extends Closeable {
void configure(Map<String, ?> configs, boolean isKey);
// 序列化方法
// topic: 操作主题名 T: 操作数据
// T ---> byte[]
byte[] serialize(String topic, T data);
@Override
void close();
}
void configure(Map<String, ?> configs, boolean isKey);
// 反序列化方法
// byte[] ---> T
T deserialize(String topic, byte[] data);
@Override
void close();
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
public class User implements Serializable {
private String name;
private Boolean sex;
private Double salary;
//...
}
package transfer;
import org.apache.commons.lang.SerializationUtils;
import org.apache.kafka.common.serialization.Serializer;
import java.io.Serializable;
import java.util.Map;
/**
* User ---> byte[]
*/
public class UserSerializer implements Serializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
// user ---> json --->byte[]
// byte[] ---> json ---> user
@Override
public byte[] serialize(String topic, User user) {
byte[] bytes = SerializationUtils.serialize((Serializable) user);
return bytes;
}
@Override
public void close() {
}
}
package transfer;
import org.apache.commons.lang.SerializationUtils;
import org.apache.kafka.common.serialization.Deserializer;
import java.util.Map;
public class UserDeserializer implements Deserializer<User> {
@Override
public void configure(Map<String, ?> configs, boolean isKey) {
}
@Override
public User deserialize(String topic, byte[] bytes) {
User user = (User) SerializationUtils.deserialize(bytes);
return user;
}
@Override
public void close() {
}
}
package transfer;
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.IntegerSerializer;
import java.util.Properties;
public class UserProducerDemo {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, UserSerializer.class);
KafkaProducer<Integer, User> producer = new KafkaProducer<Integer, User>(properties);
producer.send(new ProducerRecord<Integer, User>("t4", 1, new User("zs", true, 100D)));
producer.flush();
producer.close();
}
}
//--------------------------------------------------------------------------------------------
package transfer;
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.IntegerDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class UserConsumerDemo {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"node1:9092,node2:9092,node3:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,UserDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"g2");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false); // 关闭offset自动提交
// properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,3000); // 自动提交offset间隔时间 单位毫秒
KafkaConsumer<Integer, User> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(Arrays.asList("t4"));
while(true){
ConsumerRecords<Integer, User> records = consumer.poll(Duration.ofSeconds(5));
for (ConsumerRecord<Integer, User> record : records) {
System.out.println(record.key() +" " + record.value().getName() + " "+ record.offset());
}
}
}
}
(略)
application.properties
#======================生产者配置信息====================
spring:
kafka:
producer:
bootstrap-servers: node1:9092,node2:9092,node3:9092
key-serializer: org.apache.kafka.common.serialization.IntegerSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
#======================消费者配置信息==========================
consumer:
bootstrap-servers: node1:9092,node2:9092,node3:9092
key-deserializer: org.apache.kafka.common.serialization.IntegerDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
group-id: g1
server:
port: 8989 #修改内嵌服务器端口号
#必须指定端口号,不然springboot默认端口号8080和Oracle数据库
spring框架中有一个特殊的工具包
spring-task
定时任务需要满足两个条件:
- 触发时机: cron表达式 秒 分 时 日 月 周 年
1 * 10 * * * *
固定数值 特殊字符: *(任意值) ?(不关心内容,占位符,日周不能同时出现,可以使用?) / (增幅)
# 每秒执行一次 * * * * * ?
spring中不支持年,所以只有6位
Lunix中5位
- 任务内容
package com.baizhi.kafka;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.FailureCallback;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.SuccessCallback;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
@Component // 当前类自动注册为spring容器中的bean
public class ProducerDemo {
// 从10点23分0秒开始 每隔10秒触发一次
// @Scheduled(cron = "0/10 26 10 * * ?")
// public void m1(){
// System.out.println(new Date());
// }
@Autowired
public KafkaTemplate<Integer, String> kafkaTemplate;
private AtomicInteger atomicInteger = new AtomicInteger();
//复杂的任务解析不了 有专业的定时任务框架可使用
// 每隔5秒发送一条数据
@Scheduled(cron = "0/5 * * * * ?")//调度注解
public void send() {
int num = atomicInteger.getAndIncrement();
//future 异步处理的结果对象
ListenableFuture<SendResult<Integer, String>> future = kafkaTemplate.send("t4", num, "Hello World: " + num);
// 函数式编程 callBack回调方法
future.addCallback((t) -> {
System.out.println("发送成功:" + t);
}, (e) -> {
System.out.println("发送失败:" + e.getMessage());
});
}
}
package com.baizhi.kafka;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class ConsumerDemo {
/**
* 接受方法 消费消息方法
*/
@KafkaListener(topics = "t4") //订阅指定主题
public void receive(ConsumerRecord<Integer,String> record){
System.out.println(record.key() +" | " + record.value() + " | " +record.offset());
}
}
生产者会尝试缓冲record,实现批量发送,通过以下配置控制发送时机
batch.size # 当多条消息发送到一个分区时,生产者会进行批量发送,这个参数指定了批量消息的大小上限(以字节为单位)。
linger.ms # 这个参数指定生产者在发送批量消息前等待的时间,当设置此参数后,即便没有达到批量消息的指定大小,到达时间后生产者也会发送批量消息到broker
# 生产者一方的配置
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,2048); // 批量发送数据上限为 2kb
properties.put(ProducerConfig.LINGER_MS_CONFIG,1000); // 批量发送最大等待时间 1s
消费组实际上是用来管理组织消费者,原则:组外广播,组内负载均衡
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
// 或
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g2");
# 注意: 多个消费者的消费组必须得一样
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
结论:
消费组组内负载均衡是针对于分区,组内的消费者在负载均衡时,一个消费者负责Topic一个或者多个分区内数据处理
幂等:多次操作的结果和一次操作的结果相同,就称为幂等性操作。读操作一定是幂等性操作,写操作一定不是幂等性操作。
Restful: 微服务系统设计架构
GET
: 查询POST
: 新增PUT
: 修改DELETE
:删除
Kafka的producer和broker之间默认有应答(ack)机制,当kafka的producer发送数据给broker,如果在规定的时间没有收到应答,生产者会自动重发数据,这样的操作可能造成重复数据(at least onnce语义)的产生。
开启kafka生产者的幂等性支持
acks = all // 0 无需应答 1 只写入Leader分区后立即ack -1(all) 写入到leader和follower分区后再进行应答
retries = 3 // 表示重试次数
request.timeout.ms = 3000 //等待应答超时时间
enable.idempotence = true //开启幂等性
properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等性支持
数据库就俩知识点:1、事务 2、索引
数据库事务:多个操作是一个原子操作,不可分割,同时成功或者同时失败。
事务问题: 脏读、不重复读、幻影读
事务隔离级别(解决事务问题):读未提交、读提交、可重复读、序列化读
kafka事务指的是在一个事务内多个Record发送或者处理是一个原子操作,不可分割,同时成功,或者同时失败。
kafka事务的隔离级别:读未提交(默认:read_uncommitted
)、读提交(read_commmitted
)
// 初始化事务
public void initTransactions() {
throwIfNoTransactionManager();
if (initTransactionsResult == null) {
initTransactionsResult = transactionManager.initializeTransactions();
sender.wakeup();
}
try {
if (initTransactionsResult.await(maxBlockTimeMs, TimeUnit.MILLISECONDS)) {
initTransactionsResult = null;
} else {
throw new TimeoutException("Timeout expired while initializing transactional state in " + maxBlockTimeMs + "ms.");
}
} catch (InterruptedException e) {
throw new InterruptException("Initialize transactions interrupted.", e);
}
}
// 开启事务
public void beginTransaction() throws ProducerFencedException {
throwIfNoTransactionManager();
transactionManager.beginTransaction();
}
// 提交事务
public void commitTransaction() throws ProducerFencedException {
throwIfNoTransactionManager();
TransactionalRequestResult result = transactionManager.beginCommit();
sender.wakeup();
result.await();
}
// 将事务内的offset发送kafka集群
public void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
String consumerGroupId) throws ProducerFencedException {
throwIfNoTransactionManager();
TransactionalRequestResult result = transactionManager.sendOffsetsToTransaction(offsets, consumerGroupId);
sender.wakeup();
result.await();
}
// 取消事务
public void abortTransaction() throws ProducerFencedException {
throwIfNoTransactionManager();
TransactionalRequestResult result = transactionManager.beginAbort();
sender.wakeup();
result.await();
}
Kafka生产者在一个事务内生产的Record是一个不可分割的整体,要么同时写入Kafka集群,或者某个出错,回滚撤销所有的写操作
开启生产者事务:
ENABLE_IDEMPOTENCE_CONFIG=true
每一个生产者,需要唯一的事务编号:TransactionID
事务的超时时间(可选):TRANSACTION_TIMEOUT_CONFIG
package transaction;
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.errors.ProducerFencedException;
import org.apache.kafka.common.serialization.IntegerSerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.UUID;
public class KafkaProducerDemo {
public static void main(String[] args) {
//1. 创建配置对象 指定Producer的信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); // 对record的key进行序列化
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.ACKS_CONFIG,"all"); // ack的机制
properties.put(ProducerConfig.RETRIES_CONFIG,3); // 重试发送record的次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,3000); // 请求的超时时间 单位毫秒
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等性支持
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,2048); // 批处理的数据上限
properties.put(ProducerConfig.LINGER_MS_CONFIG,1000); // 批处理的时间上限
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 生产者事务的ID
properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG,1000); // 事务的超时时间
//2. 创建Producer对象
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(properties);
// 初始化kafka的事务
producer.initTransactions();
// 开启Kafka的事务
producer.beginTransaction();
//3. 发布消息
// ProducerRecord record = new ProducerRecord("t1",1,"Hello World");
// ProducerRecord record = new ProducerRecord("t1",2,"Hello World2");
try {
for (int i = 80; i < 100; i++) {
ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>("t1",i,"Hello World"+i);
producer.send(record);
}
// 正常操作 提交Kafka事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
e.printStackTrace();
// 异常操作 回滚kafka事务
producer.abortTransaction();
}
//4. 提交
producer.flush();
producer.close();
}
}
消费和生产并存事务: 消费和生产在一个事务内完成,同时成功或者同时失败
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t5 --partitions 3 --replication-factor 3 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t6 --partitions 3 --replication-factor 3 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --list --bootstrap-server node1:9092,node2:9092,node3:9092
t5
t6
特点:消费kafka的A主题进行业务操作,将操作的结果写入到B主题中。如果开启
consume-transfer-produce
事务,读A和写B在一个事务环境中,不可分割,要么同时成功,要么同时失败。
package transaction;
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.errors.ProducerFencedException;
import org.apache.kafka.common.serialization.IntegerDeserializer;
import org.apache.kafka.common.serialization.IntegerSerializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.*;
/**
* 消费生产并存事务
*/
public class ConsumeTransferProduce {
public static void main(String[] args) {
KafkaProducer<Integer, String> producer = initProducer();
KafkaConsumer<Integer, String> consumer = initConsumer();
//1. 消费者订阅
consumer.subscribe(Arrays.asList("t5"));
//2. 初始化kafka的事务
producer.initTransactions();
while (true) {
//3. 开启kafka事务环境
producer.beginTransaction();
try {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofSeconds(5));
Map<TopicPartition, OffsetAndMetadata> commitOffset = new HashMap<>();
for (ConsumerRecord<Integer, String> record : records) {
// 业务操作
System.out.println(record.key() + " ---> " + record.value());
// 人为模拟业务错误
//==============================================
/*
if (record.value().equals("AA")){
int m = 1/0;
}
*/
//==============================================
// 手动维护消费的偏移量信息
commitOffset.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1));
// 消费啥内容 发送啥内容
producer.send(new ProducerRecord<Integer, String>("t6", record.key(), record.value()));
}
//4. 将事务内的消费的偏移量提交
producer.sendOffsetsToTransaction(commitOffset, "g1");
//5. 提交kafka的事务
producer.commitTransaction();
} catch (ProducerFencedException e) {
e.printStackTrace();
//6. 回滚事务
producer.abortTransaction();
}
}
}
/**
* 初始化 生产者实例
*
* @return
*/
public static KafkaProducer<Integer, String> initProducer() {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 2048);
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1000);
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
properties.put(ProducerConfig.ACKS_CONFIG, "all");
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 2000);
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString()); // 事务的ID
properties.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 1000);
return new KafkaProducer<Integer, String>(properties);
}
/**
* 初始化 消费者实例
*
* @return
*/
public static KafkaConsumer<Integer, String> initConsumer() {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交消费的offset
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed"); // 读已提交 不会读到别的事务未提交的记录
return new KafkaConsumer<Integer, String>(properties);
}
}
MQ的应用场景:https://www.jianshu.com/p/a4186a6b51b4
用户注册系统
java mail
技术选型:springboot + html + mysql + kafka 生产者和消费者API
在批处理中,新到达的数据元素被收集到一个组中。整个组在未来的时间进行处理(作为批处理,因此称为“批处理”)。确切地说,何时处理每个组可以用多种方式来确定。例如,它可以基于预定的时间间隔(例如,每五分钟,处理任何新的数据已被收集)或在某些触发的条件下(例如,处理只要它包含五个数据元素或一旦它拥有超过1MB的数据)
批处理模式中使用的数据集通常符合下列特征
批处理架构的应用场景:日志分析、计费应用程序、数据仓库等
相关的开源项目(由Google MapReduce衍生):Apache Hadoop、Apache Spark、Apache Flink等
在流处理中,每一条新数据都会在到达时进行处理。与批处理不同,在下一批处理间隔之前不会等待,数据将作为单独的碎片进行处理,而不是一次处理批量。尽管每个新的数据都是单独处理的,但许多流处理系统也支持“窗口”操作,这些操作允许处理也引用在当前数据到达之前和/或之后在指定时间间隔内到达的数据。
流处理模式中使用的数据集通常符合下列特征
流处理的应用场景:实时监控、风险评估、实时商业智能(如智能汽车)、实时分析等
开源项目:Apache Kafka、Apache Flink、Apache Storm、Apache Samza等。
Kafka Streams是一个用于构建应用程序和微服务的客户端库,其中的输入和输出数据存储在Kafka集群中。它结合了在客户端编写和部署标准Java和Scala应用程序的简单性,以及Kafka服务器端集群技术的优点。
Topology(拓扑):表示一个流计算任务(task),等价于MapReduce中的job。不同的是MapReduce的job作业最终会停止,但是Topology会一直运行在内存中,除非人工关闭该Topology(一个圆圈就是一个对数据的处理加工动作,一个计算逻辑)
Stream:它代表了一个无限的,不断更新的Record数据集。流是有序,可重放和容错的不可变数据记录序列,其中数据记录被定义为键值对
States:用以持久化存放流计算状态结果,可以用以容错和故障恢复(zook)
Time:
注意:所谓的流处理就是通过Topology编织程序对Stream中Record元素的处理的逻辑/流程。
Kafka Streams通过构建Kafka生产者和消费者库并利用Kafka的本机功能来提供数据并行性,分布式协调,容错和操作简便性,从而简化了应用程序开发。
Kafka的消息分区用于存储和传递消息, Kafka Streams对数据进行分区以进行处理。 Kafka Streams使用Partition和Task的概念作为基于Kafka Topic分区的并行模型的逻辑单元。在并行化的背景下,Kafka Streams和Kafka之间有着密切的联系:
Kafka Streams基于应用程序的输入流分区创建固定数量的Task,每个任务(Task)分配来自输入流的分区列表(即Kafka主题)。分区到任务的分配永远不会改变,因此每个任务都是应用程序的固定平行单元。然后,任务可以根据分配的分区实例化自己的处理器拓扑; 它们还为每个分配的分区维护一个缓冲区,并从这些记录缓冲区一次一个地处理消息。因此,流任务可以独立并行地处理,无需人工干预。
用户可以启动多个KafkaStream实例,这样等价启动了多个Stream Tread,每个Thread处理1~n个Task。一个Task对应一个分区,因此Kafka Stream流处理的并行度不会超越Topic的分区数。需要值得注意的是Kafka的每个Task都维护这自身的一些状态,线程之间不存在状态共享和通信。因此Kafka在实现流处理的过程中扩展是非常高效的。
Kafka Streams构建于Kafka本地集成的容错功能之上。 Kafka分区具有高可用性和复制性;因此当流数据持久保存到Kafka时,即使应用程序失败并需要重新处理它也可用。 Kafka Streams中的任务利用Kafka消费者客户端提供的容错功能来处理故障。如果任务运行的计算机故障了,Kafka Streams会自动在其余一个正在运行的应用程序实例中重新启动该任务。
此外,Kafka Streams还确保local state store也很有力处理故障容错。对于每个state store,Kafka Stream维护一个带有副本changelog的Topic,在该Topic中跟踪任何状态更新。这些changelog Topic也是分区的,该分区和Task是一一对应的。如果Task在运行失败并Kafka Stream会在另一台计算机上重新启动该任务,Kafka Streams会保证在重新启动对新启动的任务的处理之前,通过重播相应的更改日志主题,将其关联的状态存储恢复到故障之前的内容。
注:创建Kafka Streaming Topology有两种方式
- low-level:Processor API
- high-level:Kafka Streams DSL(DSL:提供了通用的数据操作算子,如:map, filter, join, and aggregations等)
#t7必须是一个分区
[root@node3 ~]# cd /usr/kafka_2.11-2.2.0/
[root@node3 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t7 --partitions 1 --replication-factor 1 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node3 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t8 --partitions 1 --replication-factor 1 --bootstrap-server node1:9092,node2:9092,node3:9092
启动t7生产者主题
[root@node3 kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --topic t7 --broker-list node1:9092,node2:9092,node3:9092
启动t8消费者主题
[root@node2 kafka_2.11-2.2.0]# bin/kafka-console-consumer.sh --bootstrap-server node1:9092,node2:9092,node3:9092 \
--topic t8 \
--from-beginning \
--formatter kafka.tools.DefaultMessageFormatter \
--property print.key=true \
--property print.value=true \
--property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer \
--property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer
<dependency>
<groupId>org.apache.kafkagroupId>
<artifactId>kafka-streamsartifactId>
<version>2.2.0version>
dependency>
package lowlevel;
import org.apache.kafka.common.serialization.LongSerializer;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.common.serialization.StringSerializer;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import java.util.Properties;
public class WortCountWithProcessorAPI {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "gaozhy:9092");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-processor-application");
// 创建Topology
Topology topology = new Topology();
topology.addSource("input", "input");
topology.addProcessor("wordCountProcessor", () -> new WordCountProcessor(), "input");
topology.addSink("output", "output", new StringSerializer(), new LongSerializer(), "wordCountProcessor");
KafkaStreams streams = new KafkaStreams(topology, properties);
streams.start();
}
}
package lowlevel;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;
import org.apache.kafka.streams.processor.PunctuationType;
import java.time.Duration;
import java.util.HashMap;
public class WordCountProcessor implements Processor<String, String> {
private HashMap<String, Long> wordPair;
private ProcessorContext context;
@Override
public void init(ProcessorContext context) {
wordPair = new HashMap<>();
this.context = context;
// 每隔1秒将处理的结果向下游传递
this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
System.out.println("----------- " + timestamp + " ----------- ");
wordPair.forEach((k,v) -> {
System.out.println(k +" | "+v);
this.context.forward(k,v);
});
});
}
@Override
public void process(String key, String value) {
String[] words = value.split(" ");
for (String word : words) {
Long num = wordPair.getOrDefault(word, 0L);
num++;
wordPair.put(word, num);
}
context.commit();
}
@Override
public void close() {
}
}
上面案列存在的问题:
- 宕机则计算的状态丢失
- 并没有考虑状态中keys的数目,一旦数目过大,会导致流计算服务内存溢出。
Map<String,String> changLog = new HashMap<>();
changLog.put("min.insync.replicas","1");
// changlog数据清除策略
// 一、默认策略(delete) 删除超过保留期的过期数据
// 二、compact(压实) 多个key相同的数据 使用新值覆盖旧值
changLog.put("cleanup.policy","compact");
// p1处理器添加状态管理
StoreBuilder<KeyValueStore<String, Long>> storeBuilder = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("Counts"),
Serdes.String(),
Serdes.Long()).withLoggingEnabled(changLog);// 开启远程状态副本 容错故障恢复
注意:事实StateStore本质是一个Topic,但是改topic的清除策略不在是delete,而是compact.
// 创建Topology
Topology topology = new Topology();
topology.addSource("input", "input");
topology.addProcessor("wordCountProcessor", () -> new WordCountProcessor(), "input");
// 创建state,存放状态信息
Map<String, String> changelogConfig = new HashMap();
// override min.insync.replicas
changelogConfig.put("min.insyc.replicas", "1");
changelogConfig.put("cleanup.policy","compact");
StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("Counts"),
Serdes.String(),
Serdes.Long()).withLoggingEnabled(changelogConfig);
topology.addStateStore(countStoreSupplier,"wordCountProcessor");
topology.addSink("output", "output", new StringSerializer(), new LongSerializer(), "wordCountProcessor");
package lowlevel;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;
import org.apache.kafka.streams.processor.PunctuationType;
import org.apache.kafka.streams.state.KeyValueIterator;
import org.apache.kafka.streams.state.KeyValueStore;
import java.time.Duration;
import java.util.HashMap;
public class WordCountProcessor implements Processor<String, String> {
private KeyValueStore<String, Long> keyValueStore;
private ProcessorContext context;
@Override
public void init(ProcessorContext context) {
keyValueStore = (KeyValueStore<String, Long>) context.getStateStore("Counts");
this.context = context;
// 定期向下游输出计算结果
this.context.schedule(Duration.ofSeconds(1), PunctuationType.STREAM_TIME, timestamp -> {
System.out.println("----------- " + timestamp + " ----------- ");
KeyValueIterator<String, Long> iterator = keyValueStore.all();
while (iterator.hasNext()) {
KeyValue<String, Long> entry = iterator.next();
this.context.forward(entry.key, entry.value);
}
iterator.close();
});
}
@Override
public void process(String key, String value) {
String[] words = value.split(" ");
for (String word : words) {
Long oldValue = keyValueStore.get(word);
if (oldValue == null) {
keyValueStore.put(word, 1L);
} else {
keyValueStore.put(word, oldValue + 1L);
}
}
context.commit();
}
@Override
public void close() {
}
}
Kafka Streams DSL(Domain Specific Language)构建于Streams Processor API之上。它是大多数用户推荐的,特别是初学者。大多数数据处理操作只能用几行DSL代码表示。在 Kafka Streams DSL 中有这么几个概念KTable
、KStream
和GlobalKTable
KStream是一个数据流,可以认为所有记录都通过Insert only的方式插入进这个数据流里。而KTable代表一个完整的数据集,可以理解为数据库中的表。由于每条记录都是Key-Value对,这里可以将Key理解为数据库中的Primary Key,而Value可以理解为一行记录。可以认为KTable中的数据都是通过Update only的方式进入的。也就意味着,如果KTable对应的Topic中新进入的数据的Key已经存在,那么从KTable只会取出同一Key对应的最后一条数据,相当于新的数据更新了旧的数据。
以下图为例,假设有一个KStream和KTable,基于同一个Topic创建,并且该Topic中包含如下图所示5条数据。此时遍历KStream将得到与Topic内数据完全一样的所有5条数据,且顺序不变。而此时遍历KTable时,因为这5条记录中有3个不同的Key,所以将得到3条记录,每个Key对应最新的值,并且这三条数据之间的顺序与原来在Topic中的顺序保持一致。这一点与Kafka的日志compact相同。
此时如果对该KStream和KTable分别基于key做Group,对Value进行Sum,得到的结果将会不同。对KStream的计算结果是
GlobalKTable: 和KTable类似,不同点在于KTable只能表示一个分区的信息,但是GlobalKTable表示的是全局的状态信息。
基于DSL风格的WordCount
创建t9,t10 topic
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t9 --partitions 3 --replication-factor 3 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --create --topic t10 --partitions 3 --replication-factor 3 --bootstrap-server node1:9092,node2:9092,node3:9092
开启t9生产者模式
[root@node1 kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --topic t9 --broker-list node1:9092,node2:9092,node3:9092
开启t10消费者模式
[root@node2 kafka_2.11-2.2.0]# bin/kafka-console-consumer.sh --bootstrap-server node1:9092,node2:9092,node3:9092 \
--topic t10 \
--from-beginning \
--formatter kafka.tools.DefaultMessageFormatter \
--property print.key=true \
--property print.value=true \
--property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer \
--property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer
package highlevel;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.KeyValueMapper;
import org.apache.kafka.streams.kstream.Produced;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* dsl api 高级api
* jdk8 lambda
*/
public class WordCountApplication {
public static void main(String[] args) {
//1.创建kafka streaming的配置对象
Properties properties = new Properties();
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass()); // 指定key默认的序列化器和反序列化器
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-dsl");
properties.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 2); // 线程数量 默认为1
//2.dsl编程
StreamsBuilder streamsBuilder = new StreamsBuilder();
KStream<String, String> kStream = streamsBuilder.stream("t9");// kstream【数据流】反映了t9 topic中的record
// kstream k: record k v: record v
// flatMap展开或者铺开 v ---> word[]
KTable<String, Long> kTable = kStream
.flatMap((String k, String v) -> {
String[] words = v.split(" ");
List<KeyValue<String, String>> list = new ArrayList<>();
for (String word : words) {
KeyValue<String, String> keyValue = new KeyValue<String, String>(k, word); //k: record k v: Hello
list.add(keyValue);
}
return list;
})
// 将v相同的键值对归为一类
.groupBy((k, v) -> v)
// 统计k相同的v的数量
.count();
// 将计算的结果输出保存到t10的topic中 k: word【string】 v: count【long】
kTable.toStream().to("t10", Produced.with(Serdes.String(), Serdes.Long()));
//3. 创建kafka Streaming应用
Topology topology = streamsBuilder.build();
// 打印输出topology的关系图
System.out.println(topology.describe());
KafkaStreams kafkaStreams = new KafkaStreams(topology, properties);
//4. 启动
kafkaStreams.start();
}
}
无状态的转换算子:流的处理器不涉及状态的处理和存储
分支 将一个stream转换为1到多个stream
stream ---> stream[]
// branch 分流
KStream<String, String>[] streams = kStream.branch((k, v) -> v.startsWith("A"), (k, v) -> v.startsWith("B"), (k, v) -> true);
streams[0].foreach((k, v) -> System.out.println(k + "\t" + v));
[0]"A"
[1]"B"
[2]除了A B
过滤 将一个stream进过boolean函数处理,保留符合条件的结果
// filter 过滤 保留record value为Hello开头的结果
kStream.filter((k,v) -> v.startsWith("Hello")).foreach((k,v) -> System.out.println(k+"\t"+v));
翻转过滤 将一个stream经过Boolean函数处理 保留不符合条件的结果
kStream.filterNot((k,v) -> v.startsWith("Hello")).foreach((k,v) -> System.out.println(k+"\t"+v));
将一个record展开,产生0到多个record
record —> record1,record2…
// flatMap 展开
// Hello World ---> r1(k,Hello) r2(k,World)
kStream.flatMap((k,v) -> {
List<KeyValue<String, String>> keyValues = new ArrayList<>();
String[] words = v.split(" ");
for (String word : words) {
keyValues.add(new KeyValue<String,String>(k,word));
}
return keyValues;
}).foreach((k,v) -> System.out.println(k+"\t"+v));
>a b c d
null a b c d
null a
null b
null c
null d
>Hello A
null Hello
null A
将一条record变为多条record并且将多条记录展开
(k,v) -> (k,v1),(k,v2)….
// flatMapValues
kStream
.flatMapValues((v) -> Arrays.asList(v.split(" ")))
.foreach((k,v) -> System.out.println(k+"\t"+v));
终止操作, 为每一个record提供一种无状态的操作
.foreach((k,v) -> System.out.println(k+"\t"+v));
GroupByKey : 根据key进行分组
GroupBy: 根据自定义的信息进行分组
// groupByKey | groupBy
kStream
.flatMap((k, v) -> {
String[] words = v.split(" ");
List<KeyValue<String, String>> keyValues = new ArrayList<>();
for (String word : words) {
keyValues.add(new KeyValue<String, String>(word, word));
}
return keyValues;
})
.groupByKey()
.count()
.toStream()
.print(Printed.toSysOut());
将一条record映射为另外的一条record
// map | mapValues
// map: 将一个record转换为另外一个record
// k = null v: Hello
kStream.map((k,v) -> new KeyValue<String,Long>(k,(long) v.length())).foreach((k,v) -> System.out.println(k +"\t"+v));
Merge
将两个流合并为一个
KStream<byte[], String> stream1 = ...;
KStream<byte[], String> stream2 = ...;
KStream<byte[], String> merged = stream1.merge(stream2);
作为程序执行的探针,一般用于debug调试,因为peek并不会对后续的流数据带来任何影响。
KStream<byte[], String> unmodifiedStream = stream.peek((key, value) -> System.out.println("key=" + key + ", value=" + value));
最终操作,将每一个record进行输出打印
stream.print(Printed.toSysOut());
stream.print(Printed.toFile("streams.out").withLabel("streams"));
修改记录中key (k,v)—>(newkey,v)
KStream<String, String> rekeyed = stream.selectKey((key, value) -> value.split(" ")[0])
状态存储,以防重启数据丢失
有状态的转换算子,处理器【Processor】在进行处理时需要更新状态或者从历史状态中恢复数据
Aggregate
聚合 有状态的转换算子
KTable<String, Long> kTable = kStream
.flatMapValues(value -> Arrays.asList(value.split(" ")))
.groupBy((k, v) -> v)
// 第一参数:聚合的初始值 第二参数:聚合逻辑 第三个参数:【必须】指定状态存储的KV数据类型
.aggregate(
()-> 0L,
(k,v,agg) -> 1L+agg,
Materialized.,Long,KeyValueStore<Bytes,byte[]>>as("c160")
.withKeySerde(Serdes.String())
.withValueSerde(Serdes.Long())); // kout: String vout: Long
Count
统计key相同的 record的出现次数
// 指定状态存储的k v的结构类型
.count(Materialized., Long, KeyValueStore<Bytes,byte[]>>as("c158").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
Reduce
规约 计算 有状态的转换算子
//===========================================================================
KTable<String, Long> kTable = kStream
// k=null v= Hello World
.flatMapValues(value -> Arrays.asList(value.split(" ")))
// k=null v= Hello
// k=null v= World
.map((String k,String v) -> new KeyValue<String,Long>(v,1L))
// Hello 1L
// World 1L
// K:String V:Long 手动指定reparation topic的kv类型
.groupByKey(Grouped.with(Serdes.String(),Serdes.Long())) // 替换默认的String kv类型
//.groupByKey() // 注意:ERROR
// k: String v:Long
.reduce((v1,v2) -> v1+v2,Materialized., Long, KeyValueStore<Bytes, byte[]>>as("1902")
.withKeySerde(Serdes.String())
.withValueSerde(Serdes.Long()));
//===========================================================================
micro batch(微批),时间维度数据范围的计算
翻滚窗口将流元素按照固定的时间间隔,拆分成指定的窗口,窗口和窗口间元素之间没有重叠。在下图不同颜色的record表示不同的key。可以看是在时间窗口内,每个key对应一个窗口。前闭后开
//=================================翻滚窗口==========================================
KTable<Windowed<String>, Long> kTable = kStream
.flatMapValues(value -> Arrays.asList(value.split(" ")))
.groupBy((k, v) -> v)
// 将分组后的数据按照窗口进行划分
// 翻滚窗口 时间间隔10s
// now:0 - 10s 计算
// 10s - 20s 计算
// ...
.windowedBy(TimeWindows.of(Duration.ofSeconds(10)))
// 指定状态存储的k v的结构类型
.count(Materialized., Long, WindowStore<Bytes,byte[]>>as("AA").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
//===========================================================================
Hopping time windows是基于时间间隔的窗口。他们模拟固定大小的(可能)重叠窗口。跳跃窗口由两个属性定义:窗口大小和其提前间隔(又名“hop”)。
//=================================跳跃窗口==========================================
KTable<Windowed<String>, Long> kTable = kStream
.flatMapValues(value -> Arrays.asList(value.split(" ")))
.groupBy((k, v) -> v)
// 将分组后的数据按照窗口进行划分
// 翻滚窗口 时间间隔10s
// 第一个窗口:now:0 - 10s 计算
// 第二个窗口:5-15 计算 (5-10)归属于第一个和第二个窗口
// 10-20
// ...
.windowedBy(TimeWindows.of(Duration.ofSeconds(10)).advanceBy(Duration.ofSeconds(5)))
// 指定状态存储的k v的结构类型
.count(Materialized., Long, WindowStore<Bytes,byte[]>>as("BB").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
//===========================================================================
Session 窗口的大小动态 无重叠 数据驱动的窗口
回顾:Servelt Session 会话对象,一旦使用Session,会话会自动延长30min,Session超时策略(服务器自动删除30min未使用的Session)
Session Window该窗口用于对Key做Group后的聚合操作中。它需要对Key做分组,然后对组内的数据根据业务需求定义一个窗口的起始点和结束点。一个典型的案例是,希望通过Session Window计算某个用户访问网站的时间。对于一个特定的用户(用Key表示)而言,当发生登录操作时,该用户(Key)的窗口即开始,当发生退出操作或者超时时,该用户(Key)的窗口即结束。窗口结束时,可计算该用户的访问时间或者点击次数等。
Session Windows用于将基于key的事件聚合到所谓的会话中,其过程称为session化。会话表示由定义的不活动间隔(或“空闲”)分隔的活动时段。处理的任何事件都处于任何现有会话的不活动间隙内,并合并到现有会话中。如果事件超出会话间隙,则将创建新会话。会话窗口的主要应用领域是用户行为分析。基于会话的分析可以包括简单的指标.
如果我们接收到另外三条记录(包括两条迟到的记录),那么绿色记录key的两个现有会话将合并为一个会话,从时间0开始到结束时间6,包括共有三条记录。蓝色记录key的现有会话将延长到时间5结束,共包含两个记录。最后,将在11时开始和结束蓝键的新会话。
//==================================会话窗口=========================================
KTable<Windowed<String>, Long> kTable = kStream
.flatMapValues(value -> Arrays.asList(value.split(" ")))
.groupBy((k, v) -> v)
// 将分组后的数据按照窗口进行划分
// 翻滚窗口 时间间隔10s
// 第一个窗口:now:0 - 10s 计算
// 第二个窗口:5-15 计算 (5-10)归属于第一个和第二个窗口
// 10-20
// ...
.windowedBy(SessionWindows.with(Duration.ofSeconds(10)))
// 指定状态存储的k v的结构类型
.count(Materialized., Long, SessionStore<Bytes, byte[]>>as("CC").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
//===========================================================================
kTable.toStream().foreach((k, v) -> { // 窗口计算指的是对窗口内的数据进行计算
long start = k.window().start();
long end = k.window().end();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String d1 = sdf.format(new Date(start));
String d2 = sdf.format(new Date(end));
System.out.println(d1 + "\t" + d2 + "\t" + k.key() + "\t" + v);
});
.
//===========================================================================
#### session window
> Session 窗口的大小动态 无重叠 数据驱动的窗口
>
> 回顾:Servelt Session 会话对象,一旦使用Session,会话会自动延长30min,Session超时策略(服务器自动删除30min未使用的Session)
Session Window该窗口用于对Key做Group后的聚合操作中。它需要对Key做分组,然后对组内的数据根据业务需求定义一个窗口的起始点和结束点。一个典型的案例是,希望通过Session Window计算某个用户访问网站的时间。对于一个特定的用户(用Key表示)而言,当发生登录操作时,该用户(Key)的窗口即开始,当发生退出操作或者超时时,该用户(Key)的窗口即结束。窗口结束时,可计算该用户的访问时间或者点击次数等。
Session Windows用于将基于key的事件聚合到所谓的会话中,其过程称为session化。会话表示由定义的不活动间隔(或“空闲”)分隔的活动时段。处理的任何事件都处于任何现有会话的不活动间隙内,并合并到现有会话中。如果事件超出会话间隙,则将创建新会话。会话窗口的主要应用领域是用户行为分析。基于会话的分析可以包括简单的指标.
[外链图片转存中...(img-XJl0y2F9-1569078377533)]
如果我们接收到另外三条记录(包括两条迟到的记录),那么绿色记录key的两个现有会话将合并为一个会话,从时间0开始到结束时间6,包括共有三条记录。蓝色记录key的现有会话将延长到时间5结束,共包含两个记录。最后,将在11时开始和结束蓝键的新会话。
[外链图片转存中...(img-CcggCzf3-1569078377534)]
```java
//==================================会话窗口=========================================
KTable, Long> kTable = kStream
.flatMapValues(value -> Arrays.asList(value.split(" ")))
.groupBy((k, v) -> v)
// 将分组后的数据按照窗口进行划分
// 翻滚窗口 时间间隔10s
// 第一个窗口:now:0 - 10s 计算
// 第二个窗口:5-15 计算 (5-10)归属于第一个和第二个窗口
// 10-20
// ...
.windowedBy(SessionWindows.with(Duration.ofSeconds(10)))
// 指定状态存储的k v的结构类型
.count(Materialized.>as("CC").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
//===========================================================================
kTable.toStream().foreach((k, v) -> { // 窗口计算指的是对窗口内的数据进行计算
long start = k.window().start();
long end = k.window().end();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String d1 = sdf.format(new Date(start));
String d2 = sdf.format(new Date(end));
System.out.println(d1 + "\t" + d2 + "\t" + k.key() + "\t" + v);
});