官方介绍:
Kafka Streams是一个用于构建应用程序和微服务的客户端库,其中输入和输出数据存储在Kafka集群中。它结合了在客户端编写和部署标准Java和Scala应用程序的简单性,以及Kafka服务器端集群技术的优势。
DSL (Domain Specific Language) 领域特定语言
它是建立在流处理器API之上的。推荐大多数用户使用,尤其是初学者。大多数数据处理操作可以只用几行DSL代码表示。
👇 调试时可以用offsetexplorer,是一款可视化的kafka UI 👇
offsetexplorer
stream
创造一个stream流
final StreamsBuilder builder = new StreamsBuilder();
// key序列化,value序列化均为String序列化器
KStream<String, String> stream = builder.stream("delay-test", Consumed.with(Serdes.String(), Serdes.String()));
创建出stream流并不意味着它就开启了,需要手动开启。
Topology topology = builder.build();// 流的拓扑结果
System.out.println(topology.describe());
KafkaStreams kafkaStreams = new KafkaStreams(topology, props);
kafkaStreams.start();
Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
@Override
public void run() {
kafkaStreams.close(); // 最好正确的关闭它
}
});
Peek
查看流
stream.peek((key, value) -> System.out.println("key=" + key + ", value=" + value));
Branch
分支,按照特定规则划分流
Map<String, KStream<String, String>> map = stream.split(Named.as("Branch-"))
.branch((k, v) -> v.startsWith("A"))
.branch((k, v) -> v.startsWith("B"))
.defaultBranch(Branched.as("C"));
// KStream branches.get("Branch-A") 包含了全部value以"A"开头的流
// KStream branches.get("Branch-B") 包含了全部value以"B"开头的流
// KStream branches.get("Branch-C") 其他记录流
Filter
过滤,只处理想要的流(此功能可以实现基于Kafka的死信队列)
stream.filter((k, v) -> {
long time = Long.parseLong(v);
// 假设value存的是时间,当前时间 <= 存的时间,意味着该处理消息了
// 没有过滤到的会自动等待。
return System.currentTimeMillis() <= time;
}).to("handle-topic"); // 将消息发送到handle-topic,此时有监听器监听这个topic的话将被立刻执行
Inverse Filter
反向过滤
KStream<String, Long> onlyPositives = stream.filterNot((key, value) -> value <= 0);// 只要value > 0的
FlatMap
根据收到的消息,变成其他类型的消息(0条、1条、多条)
stream.flatMap(
// 例如: (345, "Hello") -> ("HELLO", 1000), ("hello", 9000)
(key, value) -> {
List<KeyValue<String, Integer>> result = new LinkedList<>();
result.add(KeyValue.pair(value.toUpperCase(), 1000));
result.add(KeyValue.pair(value.toLowerCase(), 9000));
return result;
}
FlatMapValues
类似FlatMap,但是不能改变数据类型
stream.flatMapValues((k, v) -> List.of(v + "666"))
.peek((k, v) -> System.out.println(v)); //(key,bEfw_EPb) -> (key,bEfw_EPb666)
Foreach
单纯的遍历吧,不能改数据
stream.foreach((key, value) -> System.out.println(key + " => " + value));
GroupByKey
根据key分组,后续可以进行聚合函数的操作
KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
// 指定序列化
KGroupedStream<byte[], String> groupedStream = stream.groupByKey(
Grouped.with(
Serdes.String(), /* key */
Serdes.String()) /* value */
);
GroupBy
自己制定分组方式
KGroupedStream<String, String> groupedStream = stream.groupBy(
(key, value) -> value,
Grouped.with(
Serdes.String(), /* key */
Serdes.String()) /* value */
);
// 分组的时候可以改值的类型
KGroupedTable<String, Integer> groupedTable = table.groupBy(
(key, value) -> KeyValue.pair(value, value.length()),
Grouped.with(
Serdes.String(), /* key */
Serdes.Integer()) /* value */
);
Cogroup
聚合多个groupStream
Map
遍历消息,可以更改消息
stream.map(
(key, value) -> KeyValue.pair(value.toLowerCase(), value.length())); // (KEY1,value1) -> (key1,6)
Map (values only)
只能改value,不能改key
stream.mapValues(value -> value.toUpperCase());
SelectKey
只能改key,感觉这个东西和Map (values only)结合起来是map的完整功能吧。
stream.selectKey((k, v) -> {
return "key-init";
}).peek(((key, value) -> {
System.out.println(key + ":" + value);
}));
// (key,YnfYWNoR) -> (key-init,YnfYWNoR)
Merge
流的合并
KStream<byte[], String> stream1 = ...;
KStream<byte[], String> stream2 = ...;
KStream<byte[], String> merged = stream1.merge(stream2);
Aggregate
滚动聚合
key是null将被忽略,state store name用于存储之前的结果,再下一次运行时继续聚合。
看一个例子,往Topic中一次放入三条数据(对象的形式)
{“from”:“ivk1Cfa6”,“to”:“0dNonPAg”,“body”:“p0_YbUJS”} |
---|
{“from”:“9QLtz40V”,“to”:“07Nrgwo9”,“body”:“BmLmJ6cd”} |
{“from”:“eYZI_yA_”,“to”:“vD9cqTXg”,“body”:“YRXZ_7JJ”} |
有记忆化功能,因为聚合的数据被存储到了aggregated-table-store中。
KStream<String, String> stream = builder.stream("my-topic", Consumed.with(Serdes.String(), Serdes.String()));
stream.groupByKey().aggregate(
() -> new String("".getBytes(), StandardCharsets.UTF_8),// 给定初始值
// aggKey消息key、newValue当前收到的消息、aggValue当前聚合的结果
(aggKey, newValue, aggValue) -> {
System.out.println("aggKey=" + aggKey);
System.out.println("newValue=" + newValue);
System.out.println("aggValue=" + aggValue);
Message message = JSONObject.parseObject(newValue, Message.class);
return aggValue + message.getBody();
},
Materialized.<String, String, KeyValueStore<Bytes, byte[]>>as("aggregated-table-store") /* state store name */
.withValueSerde(Serdes.String())
).toStream().peek((k, v) -> {
System.out.println(k);
System.out.println(v);
});
// 最后一次输出
aggKey=key2
newValue={"from":"eYZI_yA_","to":"vD9cqTXg","body":"YRXZ_7JJ"}
aggValue=p0_YbUJSBmLmJ6cd
key2
p0_YbUJSBmLmJ6cdYRXZ_7JJ
Aggregate (windowed)
窗口聚合,和上面说到的类似,只不过有一个窗口期,过了这个窗口期数据清空。
{“from”:“_rBU2X4n”,“to”:“ef6_UDSq”,“body”:“uxTl6m1j”} |
---|
{“from”:“81jx6zd_”,“to”:“PdyJdG3P”,“body”:“CAa69VG6”} |
先发送第一条,过了五分钟发送第二条,你会发现这两条消息没有合并在一起,因为过了窗口期
stream.groupByKey().windowedBy(SlidingWindows.ofTimeDifferenceWithNoGrace(Duration.ofMinutes(5))).aggregate(
() -> new String("".getBytes(), StandardCharsets.UTF_8),
(aggKey, newValue, aggValue) -> {
System.out.println("aggKey=" + aggKey);
System.out.println("newValue=" + newValue);
System.out.println("aggValue=" + aggValue);
Message message = JSONObject.parseObject(newValue, Message.class);
return aggValue + message.getBody();
},
Materialized.<String, String, WindowStore<Bytes, byte[]>>as("aggregated-table-store") /* state store name */
.withValueSerde(Serdes.String())
).toStream().peek((k, v) -> {
System.out.println(k);
System.out.println(v);
});
count
按照分组key统计数量
key是null将被忽略
KStream<String, String> stream = builder.stream("my-topic", Consumed.with(Serdes.String(), Serdes.String()));
stream.groupByKey().count().toStream().peek((k, v) -> {
System.out.println(k);
System.out.println(v);
});
/**
* 第一次 key 1
* 第二次 key 2
* 第三次 key2 1 ...
*/
count(window)
一样,多了个窗口期
reduce
滚动聚合,和Aggregate 功能类似
key是null将被忽略
KStream<String, String> stream = builder.stream("my-topic", Consumed.with(Serdes.String(), Serdes.String()));
stream.groupByKey().reduce((oldValue, newValue) -> {
return oldValue + newValue;
}).toStream().peek((k, v) -> {
System.out.println(k);
System.out.println(v);
});
reduce (windowed)
滚动聚合,和Aggregate 功能类似,多了窗口期
key是null将被忽略