kafka学习资料

Kafka笔记

一、概述

http://kafka.apache.org/

Apache Kafka® is a distributed streaming platform——分布式的流数据平台

Kafka具备三项关键能力:

  • 发布订阅记录流(Record),类似于消息队列(MQ)或者企业级消息系统
  • 存储记录流,以一种容错持久化方式
  • 实时处理加工流数据

Kafka的应用场景:

  • 构建实时的流数据管道,可靠的在系统和应用间获取数据(MQ)
  • 构建实时的流数据应用,传输或者处理加工流数据

Kafka中的核心概念:

  • Broker(中间人或者代理人): 一个kafka服务实例
  • Topic(主题): 某一个类别的记录
  • Partition(分区): 对主题进行数据分区,一个主题有多个数据分区构成
  • Replication(复制): 主题主分区中的数据备份,故障恢复
  • Leader(主分区): 主题的主分区 读写操作默认使用主分区
  • Follower(复制分区): 同步主分区中的数据(冗余备份),并且当主分区不可用时,某个follower会自动升级为主分区
  • Offset(偏移量): 标识读写操作的位置,读offset <= 写offset

Kafka的四大核心API:

  • The Producer API:允许应用发布记录流给一个或者多个Topic
  • The Consumer API : 允许应用订阅一个或者多个主题,并且对主题中新产生的记录进行处理
  • The Streams API : 允许应用扮演流处理器,消费来自于一个或者多个主题流数据记录,并且将处理的结果输出到一个或者多个主题中,Streams API可以高效的处理传输数据。
  • The Connector API: 连接外部的存储系统

架构图

在这里插入图片描述

工作原理
  • 首先Producer产生Record发送给指定的Kafka Topic(Topic实质是有多个分区构成,每一个分区都会相应的复制分区),在真正存放到Kafka集群时会进行计算key.hashCode%topicPartitionNums等于要存放的分区序号。
  • Leader分区中的数据会自动同步到Follower分区中,ZooKeeper会实时监控服务健康信息,一旦发生故障,会立即进行故障转移操作(将一个Follower复制分区自动升级为Leader主分区)
  • Kafka一个分区实际上是一个有序的Record的Queue(符合队列的数据结构,先进先出), 分区中新增的数据,会添加到队列的末尾,在处理时,会从队列的头部开始消费数据。Queue在标识读写操作位置时,会使用一个offset(读的offset <= 写的offset)
  • 最后Consumer会订阅一个Kafka Topic,一旦Topic中有新的数据产生,Conumser立即拉取最新的记录,进行相应的业务处理。

二、环境搭建

kafka完全分布式集群

准备工作

  • 3个节点
  • 确保ZooKeeper集群运行正常

安装

[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

配置

broker.id=0 | 1 | 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

三、基本操作

Topic管理

创建Topic
[root@node1 kafka_2.11-2.2.0]#  bin/kafka-topics.sh --create --topic dingl --partitions 3 --replication-factor 3 --bootstrap-server node1:9092,node2:9092,node3:9092

注意:

--create : 创建动作

--topic: 操作的主题名

--partitions: Leader分区的数量

--replication-factor: 复制因子,包含Leader分区本身

--bootstrap-server: Kafka集群地址列表

展示Topic列表
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --list  --bootstrap-server node1:9092,node2:9092,node3:9092
dingl

注意:

--list: 展示主题列表

查看指定的Topic描述
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic dingl --bootstrap-server node1:9092,node2:9092,node3:9092
Topic:dingl    PartitionCount:3        ReplicationFactor:3     Configs:segment.bytes=1073741824
        Topic: dingl   Partition: 0    Leader: 1       Replicas: 1,0,2 Isr: 1,0,2
        Topic: dingl   Partition: 1    Leader: 0       Replicas: 0,2,1 Isr: 0,2,1
        Topic: dingl   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 dingl --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:dingl    PartitionCount:3        ReplicationFactor:3     Configs:segment.bytes=1073741824
        Topic: dingl   Partition: 0    Leader: 0       Replicas: 1,0,2 Isr: 0,2
        Topic: dingl   Partition: 1    Leader: 0       Replicas: 0,2,1 Isr: 0,2
        Topic: dingl   Partition: 2    Leader: 2       Replicas: 2,1,0 Isr: 2,0

在这里插入图片描述

删除Topic

Kafka早期版本是不允许删除Topic,需要在配置文件 server.properties 添加一行配置delete.topic.enable=true

[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --delete --topic dingl --bootstrap-server node1:9092,node2:9092,node3:9092
修改Topic
# dingl topic 分区数量修改为2个

[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic dingl --bootstrap-server node1:9092,node2:9092,node3:9092
Topic:dingl    PartitionCount:1        ReplicationFactor:1     Configs:segment.bytes=1073741824
        Topic: dingl   Partition: 0    Leader: 2       Replicas: 2     Isr: 2
[root@node1 kafka_2.11-2.2.0]#
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --alter --topic dingl --patitions 2 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --alter --topic dingl --partitions 2 --bootstrap-server node1:9092,node2:9092,node3:9092
[root@node1 kafka_2.11-2.2.0]# bin/kafka-topics.sh --describe --topic dingl --bootstrap-server node1:9092,node2:9092,node3:9092  Topic:dingl    PartitionCount:2        ReplicationFactor:1     Configs:segment.bytes=1073741824
        Topic: dingl   Partition: 0    Leader: 2       Replicas: 2     Isr: 2
        Topic: dingl   Partition: 1    Leader: 0       Replicas: 0     Isr: 0

发布订阅记录

发布消息
[root@node1 kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --topic dingl --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 dingl --bootstrap-server node1:9092,node2:9092,node3:9092 --from-beginning
Hello Kafka
Hello World

四、Kafka中的基本概念

Topic和日志

​ 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处理的功能

生产者(Producer)

Kafka Producer(生产者)是用来产生持续的Record,并发布到kafka某一个或者多个Topic中.

一个Record是由key,value,timestamp构成

发布Record时的策略:

  • key = null: 轮询Partition
  • key != null : key.hashCode % partitionNum = 存放Record分区的序号
  • 手动指定Partition序号: Producer一方在发布Record时手动指定要存放的分区序号

消费者(Consumer)

Kafka Comsumer(消费者)订阅一个或者多个感兴趣的Topic,一旦这些Topic中有新的数据产生,会立即拉取到本地(Consumer)一方,进行相应的业务处理。

Kafka使用Consumer Group组织管理Conumser,Consumer Group特点: 组外广播,组内负载均衡

`

五、Kafka Java API

发布/订阅

Maven依赖
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.2.0</version>
</dependency>
配置windows kafka集群的hosts映射
192.168.12.130	node1
192.168.12.131	node2
192.168.12.132	node3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx8I4QZy-1570241847324)(assets/1566962215457.png)]

生产者API
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();

    }
}
消费者API
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 同组负载均衡 不同组广播

        //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());
            }
        }
    }
}

Topic管理

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<String> 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<String, KafkaFuture<TopicDescription>> values = result.values();
        // values.forEach((k,v) -> System.out.println(k +" | " +v));
        for (Map.Entry<String,KafkaFuture<TopicDescription>> 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();
            }
        }
    }
    

在这里插入图片描述

七、指定分区消费

订阅Topic

subscribe订阅一个或者多个主题,一旦主题中有新的数据产生,consumer会收到topic内所有分区内的消息

//3. 订阅主题
consumer.subscribe(Arrays.asList("t1"));

指定消费Topic特定的分区

指定消费的topic的分区序号,一旦分区序号内有新的数据产生,consumer会收到特定分区的新产生的记录

// 分配方法: 只消费t1主题0号分区内的消息
consumer.assign(Arrays.asList(new TopicPartition("t1",0)));

手动控制消费offset

手动控制消费的offset,可以重置offset重复处理已经处理过的消息,设定offset跳过不感兴趣的消息

// 分配方法: 只消费t1主题0号分区内的消息
consumer.assign(Arrays.asList(new TopicPartition("t1",0)));
// 手动指定消费的offset:从t1主题的0号分区offset=0的位置开始消费消息
consumer.seek(new TopicPartition("t1",0),0);

NOTE:

​ 如果手动控制消费位置,offset提交策略就没有作用了,因为每次都是从指定的offfset位置开始向后消费消息

八、自定义对象类型传输与接收

kafka Serializer接口

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();
}

kafka Deserializer接口


    void configure(Map<String, ?> configs, boolean isKey);
    
	// 反序列化方法
	// byte[] ---> T
    T deserialize(String topic, byte[] data);

    @Override
    void close();

添加JDK序列化和反序列化的工具类依赖

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</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());
            }
        }
    }
}

九、Kafka和Spring Boot整合

创建SpringBoot工程

(略)

修改application.properties

# =====================生产者配置信息==========================
spring.kafka.producer.bootstrap-servers=node1:9092,node2:9092,node3:9092
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.IntegerSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer

# =====================消费者配置信息==========================
spring.kafka.consumer.bootstrap-servers=node1:9092,node2:9092,node3:9092
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.group-id=g1

定时任务

spring框架中有一个特殊的工具包spring-task

定时任务需要满足两个条件:

  • 触发时机: cron表达式 秒 分 时 日 月 周 年

    ​ 1 * 10 * * * *

    ​ 固定数值 特殊字符: *(任意值) ?(不关心内容) / (增幅)

    ​ # 每秒执行一次 * * * * * ?

  • 任务内容

生产者

package com.dingl.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();
        ListenableFuture<SendResult<Integer, String>> future = kafkaTemplate.send("t4", num, "Hello World: " + num);
        // 函数式编程
        future.addCallback((t) -> {
            System.out.println("发送成功:" + t);
        }, (e) -> {
            System.out.println("发送失败:" + e.getMessage());
        });
    }
}

消费者

package com.dingl.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一个或者多个分区内数据处理

十二、生产者的幂等性(idempotence)

幂等:多次操作的结果和一次操作的结果相同,就称为幂等性操作。读操作一定是幂等性操作,写操作一定不是幂等性操作。

Restful: 微服务系统设计架构

  • GET: 查询

  • POST: 新增

  • PUT: 修改

  • DELETE:删除

幂等性操作: GET \ Delete \Put
在这里插入图片描述

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);  // 开启幂等性支持

十三、Kafka事务

数据库事务回顾

数据库事务:多个操作是一个原子操作,不可分割,同时成功或者同时失败。

事务问题: 脏读、不重复读、幻影读

事务隔离级别(解决事务问题):读未提交、读提交、可重复读、序列化读

Kafka事务

kafka事务指的是在一个事务内多个Record发送或者处理是一个原子操作,不可分割,同时成功,或者同时失败。

kafka事务的隔离级别:读未提交(默认:read_uncommitted)、读提交(read_commmitted

Kafka Producer对象中的事务方法

// 初始化事务
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<Integer, String> record = new ProducerRecord<Integer, String>("t1",1,"Hello World");
        // ProducerRecord<Integer, String> record = new ProducerRecord<Integer, String>("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();
    }
}

Consume-Transfer-Produce

消费和生产并存事务: 消费和生产在一个事务内完成,同时成功或者同时失败
在这里插入图片描述

准备测试使用的topic
[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的应用场景

异步通信

系统解耦

流量削峰

日志处理

MQ的应用场景:https://www.jianshu.com/p/a4186a6b51b4

项目:Kafka异步通信

用户注册系统

  • 用户信息保存到MySQL中
  • 发送邮件通知(email):java mail
  • 发送短信通知(可选): 免费的短信服务接口

技术选型:springboot + html + mysql + kafka 生产者和消费者API

Kafka Streaming

一、流处理和批处理

在这里插入图片描述

批处理 Batch Processing

​ 在批处理中,新到达的数据元素被收集到一个组中。整个组在未来的时间进行处理(作为批处理,因此称为“批处理”)。确切地说,何时处理每个组可以用多种方式来确定。例如,它可以基于预定的时间间隔(例如,每五分钟,处理任何新的数据已被收集)或在某些触发的条件下(例如,处理只要它包含五个数据元素或一旦它拥有超过1MB的数据)

批处理模式中使用的数据集通常符合下列特征

  • 有界:批处理数据集代表数据的有限集合
  • 持久:数据通常始终存储在某种类型的持久存储位置中
  • 大量:批处理操作通常是处理极为海量数据集的唯一方法
  • 高延迟:大量数据的处理需要付出大量时间,因此批处理不适合对处理时间要求较高的场合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-krS0cZxV-1570241847329)(assets/1567157583412.png)]

批处理架构的应用场景:日志分析、计费应用程序、数据仓库等

相关的开源项目(由Google MapReduce衍生):Apache Hadoop、Apache Spark、Apache Flink等

流处理 Stream Processing

在流处理中,每一条新数据都会在到达时进行处理。与批处理不同,在下一批处理间隔之前不会等待,数据将作为单独的碎片进行处理,而不是一次处理批量。尽管每个新的数据都是单独处理的,但许多流处理系统也支持“窗口”操作,这些操作允许处理也引用在当前数据到达之前和/或之后在指定时间间隔内到达的数据。
在这里插入图片描述

流处理模式中使用的数据集通常符合下列特征

  • 无界:流处理的输入数据基本上都是无边界数据,而流处理系统将依据具体的应用场景来关注数据的事件时间还是处理时间
  • 高吞吐:大多数的流处理框架都支持分布式并行处理流数据
  • 低延迟:流处理所需的响应时间应该以毫秒(或微秒)来进行计算

流处理的应用场景:实时监控、风险评估、实时商业智能(如智能汽车)、实时分析等

开源项目:Apache Kafka、Apache Flink、Apache Storm、Apache Samza等。

二、Kafka Streaming概述

Kafka Streams是一个用于构建应用程序和微服务的客户端库,其中的输入和输出数据存储在Kafka集群中。它结合了在客户端编写和部署标准Java和Scala应用程序的简单性,以及Kafka服务器端集群技术的优点。

Topology(拓扑):表示一个流计算任务,等价于MapReduce中的job。不同的是MapReduce的job作业最终会停止,但是Topology会一直运行在内存中,除非人工关闭该Topology

Stream:它代表了一个无限的,不断更新的Record数据集。流是有序,可重放和容错的不可变数据记录序列,其中数据记录被定义为键值对

States:用以持久化存放流计算状态结果,可以用以容错和故障恢复

Time

  • Event time(事件时间)
  • Processing time(处理时间)
  • Ingestion time(摄入时间)

注意:所谓的流处理就是通过Topology编织程序对Stream中Record元素的处理的逻辑/流程。

架构

Kafka Streams通过构建Kafka生产者和消费者库并利用Kafka的本机功能来提供数据并行性,分布式协调,容错和操作简便性,从而简化了应用程序开发。
在这里插入图片描述

Kafka的消息分区用于存储和传递消息, Kafka Streams对数据进行分区以进行处理。 Kafka Streams使用Partition和Task的概念作为基于Kafka Topic分区的并行模型的逻辑单元。在并行化的背景下,Kafka Streams和Kafka之间有着密切的联系:

  • 每个stream分区都是完全有序的数据记录序列,并映射到Kafka Topic分区。

  • Stream中的数据记录映射到该Topic的Kafka消息。

  • 数据记录的key决定了Kafka和Kafka Streams中数据的分区,即数据如何路由到Topic的特定分区。

任务的并行度

Kafka Streams基于应用程序的输入流分区创建固定数量的Task,每个任务(Task)分配来自输入流的分区列表(即Kafka主题)。分区到任务的分配永远不会改变,因此每个任务都是应用程序的固定平行单元。然后,任务可以根据分配的分区实例化自己的处理器拓扑; 它们还为每个分配的分区维护一个缓冲区,并从这些记录缓冲区一次一个地处理消息。因此,流任务可以独立并行地处理,无需人工干预。

img

用户可以启动多个KafkaStream实例,这样等价启动了多个Stream Tread,每个Thread处理1~n个Task。一个Task对应一个分区,因此Kafka Stream流处理的并行度不会超越Topic的分区数。需要值得注意的是Kafka的每个Task都维护这自身的一些状态,线程之间不存在状态共享和通信。因此Kafka在实现流处理的过程中扩展是非常高效的。

img

容错

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等)

low-level:Processor API

创建Maven应用
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-streams</artifactId>
    <version>2.2.0</version>
</dependency>
编写stream应用
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() {

    }
}
测试

上面案列存在的问题:

  1. 宕机则计算的状态丢失
  2. 并没有考虑状态中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.

关联StateStore和Processor
// 创建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");
在自定义Processor实现类中使用state
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() {

    }
}

high-level

Kafka Streams DSL(Domain Specific Language)构建于Streams Processor API之上。它是大多数用户推荐的,特别是初学者。大多数数据处理操作只能用几行DSL代码表示。在 Kafka Streams DSL 中有这么几个概念KTableKStreamGlobalKTable

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相同。

img

此时如果对该KStream和KTable分别基于key做Group,对Value进行Sum,得到的结果将会不同。对KStream的计算结果是<Jack,4>,<Lily,7>,<Mike,4>。而对Ktable的计算结果是<Mike,4>,<Jack,3>,<Lily,5>。

GlobalKTable: 和KTable类似,不同点在于KTable只能表示一个分区的信息,但是GlobalKTable表示的是全局的状态信息。

基于DSL风格的WordCount

编写应用
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();
    }
}
DSL自动创建的Topoloy详解

在这里插入图片描述

Kafka Streams转换算子
stateless transformation

无状态的转换算子:流的处理器不涉及状态的处理和存储

branch

分支 将一个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));
filter

过滤 将一个stream进过boolean函数处理,保留符合条件的结果

// filter 过滤  保留record value为Hello开头的结果
kStream.filter((k,v) -> v.startsWith("Hello")).foreach((k,v) -> System.out.println(k+"\t"+v));
filterNot

翻转过滤 将一个stream经过Boolean函数处理 保留不符合条件的结果

kStream.filterNot((k,v) -> v.startsWith("Hello")).foreach((k,v) -> System.out.println(k+"\t"+v));
flatMap

将一个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));
flatMapValues

将一条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));
foreach

终止操作, 为每一个record提供一种无状态的操作

.foreach((k,v) -> System.out.println(k+"\t"+v));
GroupByKey | GroupBy

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());

map | mapValues

将一条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);
Peek

作为程序执行的探针,一般用于debug调试,因为peek并不会对后续的流数据带来任何影响。

KStream<byte[], String> unmodifiedStream = stream.peek((key, value) -> System.out.println("key=" + key + ", value=" + value));
Print

最终操作,将每一个record进行输出打印

stream.print(Printed.toSysOut());
stream.print(Printed.toFile("streams.out").withLabel("streams"));
SelectKey

修改记录中key (k,v)—>(newkey,v)

KStream<String, String> rekeyed = stream.selectKey((key, value) -> value.split(" ")[0])
statful transformation

有状态的转换算子,处理器【Processor】在进行处理时需要更新状态或者从历史状态中恢复数据

img

聚合(Aggregating
  • 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.<String,Long,KeyValueStore<Bytes,byte[]>>as("c160")
       			 .withKeySerde(Serdes.String())
        		.withValueSerde(Serdes.Long()));  // kout: String vout: Long
    
  • Count

    统计key相同的 record的出现次数

    // 指定状态存储的k v的结构类型
    .count(Materialized.<String, 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.<String, Long, KeyValueStore<Bytes, byte[]>>as("1902")
                .withKeySerde(Serdes.String())
                .withValueSerde(Serdes.Long()));
    //===========================================================================
    

    窗口操作

    micro batch(微批),时间维度数据范围的计算

    Tumbling(翻滚) 固定大小 无重叠

    翻滚窗口将流元素按照固定的时间间隔,拆分成指定的窗口,窗口和窗口间元素之间没有重叠。在下图不同颜色的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.<String, Long, WindowStore<Bytes,byte[]>>as("AA").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
    //===========================================================================
    
    Hopping (跳跃) 固定大小 有重叠

    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.<String, Long, WindowStore<Bytes,byte[]>>as("BB").withKeySerde(Serdes.String()).withValueSerde(Serdes.Long()));
//===========================================================================
session window

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时开始和结束蓝键的新会话。

img

//==================================会话窗口=========================================
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.<String, 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-ymuxm4wy-1570241847333)]

如果我们接收到另外三条记录(包括两条迟到的记录),那么绿色记录key的两个现有会话将合并为一个会话,从时间0开始到结束时间6,包括共有三条记录。蓝色记录key的现有会话将延长到时间5结束,共包含两个记录。最后,将在11时开始和结束蓝键的新会话。

[外链图片转存中...(img-EamnytU0-1570241847333)]

```java
//==================================会话窗口=========================================
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.<String, 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);
});
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值