接上一篇文章 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 中有这么几个概念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的计算结果是<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])
最终操作,将每一个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);
});