kafka(三)kafka steaming high-level api

接上一篇文章 https://blog.csdn.net/qq_44962429/article/details/113809911

1、 high level api

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相同。
在这里插入图片描述
此时如果对该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编程

public static void main(String[] args) {
        //1.创建kafka streaming的配置对象
        Properties properties = new Properties();
        properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.139.156: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");
        properties.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 2);  // 线程数量 默认为1


        //2.dsl编程
        StreamsBuilder streamsBuilder = new StreamsBuilder();
        KStream<String, String> kStream = streamsBuilder.stream("Atopic");

        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<>(k, word); //k: record k v: Hello
                        list.add(keyValue);
                    }
                    return list;
                })
                // 将v相同的键值对归为一类
                .groupBy((k, v) -> v)
                // 统计k相同的v的数量
                .count();
        
        // 将计算的结果输出保存到Btopic的topic中 k: word【string】 v: count【long】
        kTable.toStream().to("Btopic", 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详解
在这里插入图片描述

2、kafka 转换算子

2.1 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

// 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并且将多条记录展开

// 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));
Peek

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

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

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

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

最终操作,将每一个record进行输出打印,也可以Printed.toFile()

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

2.2 statful transformation(有状态转换算子)

在这里插入图片描述
aggragate

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

// 指定状态存储的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()));

3、kafka window

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

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

3.3 session window

Session 窗口的大小动态 无重叠 数据驱动的窗口

Session Window该窗口用于对Key做Group后的聚合操作中。它需要对Key做分组,然后对组内的数据根据业务需求定义一个窗口的起始点和结束点。一个典型的案例是,希望通过Session Window计算某个用户访问网站的时间。对于一个特定的用户(用Key表示)而言,当发生登录操作时,该用户(Key)的窗口即开始,当发生退出操作或者超时时,该用户(Key)的窗口即结束。窗口结束时,可计算该用户的访问时间或者点击次数等。

Session Windows用于将基于key的事件聚合到所谓的会话中,其过程称为session化。会话表示由定义的不活动间隔(或“空闲”)分隔的活动时段。处理的任何事件都处于任何现有会话的不活动间隙内,并合并到现有会话中。如果事件超出会话间隙,则将创建新会话。会话窗口的主要应用领域是用户行为分析。基于会话的分析可以包括简单的指标。

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);
});
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值