Kafka streams的相关中文资料非常少,笔者希望借该代码讲述一下自己对kafka streams API的用法。
kafka streams从0.10.0开始引入,现在已经更新到0.11.0。首先它的使用成本非常低廉,仅需在代码中依赖streams lib,编写计算逻辑,启动APP即可。其次它的负载均衡也非常简单暴力,增加或者减少运行实例就可以动态调整,无需人工干预。最后还有一个大杀器(0.11开始支持),提供 Exactly-once消息传递特性,它包含了producer幂等性,不会重复发送消息到broker;consumer exactly once,不会重复消费也不会丢失。在运算失败的时候,重启运算实例即可恢复。
下面用demo讲解streams api用法。
WordCount:
KStreamBuilder builder = new KStreamBuilder();
KStream<String, String> textLines = builder.stream(stringSerde, stringSerde, textLinesTopic);
KStream<String, Long> wordCounts = textLines
.flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
.map((key, word) -> new KeyValue<>(word, word))
.groupByKey().count("counts").toStream();
wordCounts.to(stringSerde, longSerde, countTopic);
KafkaStreams streams = new KafkaStreams(builder, streamsConfiguration);
streams.start();
这段非常简短的代码包括了consumer顶阅主题并消费、统计词频、producer写入到另一个Topic。跟踪代码可以发现flatMapValues,map函数本质上是处理单元processor,在函数调用时,API会创建特定的processor加入到拓扑中。
这种消费单个主题进行运算的方式可以做一些日志的统计分析,例如网站的UV,PV等。如果需要处理更复杂的业务,那么关联操作不可避免。同样的,kafka streams 提供了join函数。
KStreamBuilder streamBuilder = new KStreamBuilder();
String userStore = "user_store";
String driverStore = "driver_Store";
KTable<String, UserOrder> userOrderKTable = streamBuilder.table(Serdes.String(),
SerdeFactory.serdeFrom(UserOrder.class), TOPIC_USER_ORER, userStore);
KTable<String, DriverOrder> driverOrderKTable= streamBuilder.table(Serdes.String(),
SerdeFactory.serdeFrom(DriverOrder.class), TOPIC_DRIVER_ORDER, driverStore);
userOrderKTable.leftJoin(driverOrderKTable,
(userOrder,driverOrder)->join(userOrder,driverOrder))
.toStream()
.map((k,v)->new KeyValue<>(k,v))
.to(Serdes.String(),SerdeFactory.serdeFrom(Travel.class),TOPIC_TRAVEL);
join的语法本质上是join by partition and key。为了得到正确的Join结果,两个不同的topic需要再同一个运行实例中被消费到。假设,Topic1 和Topic2各有4个partition,有两个实例在运行,于是一个task仅消费Topic1和Topic2的两个,这样就需要保证,topic1中的两个partition的key值在另一个topic中能被找到。
上面的逻辑看起来非常绕口,在实际开发的过程中,我们仅需保证两个topic拥有相同数量的partition,并且producer采用同样的Paritioner。如果该条件不满足,需要通过through函数完成。
KStream<K, V> through(Serde<K> keySerde, Serde<V> valSerde,
StreamPartitioner<K, V> partitioner, String topic);
假设在join过程中,我们需要最新的数据做聚合。kafka streams 提供了windowed函数,在时间窗口内,后续的记录会覆盖同一个Key的记录。窗口结束后,会触发后续的计算逻辑得到正确的结果。
KTable<Windowed<String>, MultiUserOrder> userOrderKTable = streamBuilder.stream(Serdes.String(),
SerdeFactory.serdeFrom(UserOrder.class),
TOPIC_USER_ORER, userStore)
.groupByKey()
.aggregate(
new MultiUserOrder(), (k, v, map) -> {
map.setOrderId(k);
map.getOrders().add(v);
return map;
},
TimeWindows.of(6 * 1000),
SerdeFactory.serdeFrom(MultiUserOrder.class), userAggStore);
KTable<Windowed<String>, MultiDriverOrder> driverOrderKTable = streamBuilder.stream(Serdes.String(),
SerdeFactory.serdeFrom(DriverOrder.class),
TOPIC_DRIVER_ORDER, driverStore)
.groupByKey()
.aggregate(new MultiDriverOrder(), (k, v, map) -> {
map.setOrderId(k);
map.getOrders().add(v);
return map;
}, TimeWindows.of(6 * 1000),
SerdeFactory.serdeFrom(MultiDriverOrder.class), driverAggStore);
userOrderKTable.leftJoin(driverOrderKTable,
(multiUserOrder,multiDriverOrder)->join(multiUserOrder,multiDriverOrder))
.toStream()
.map((k,v)->new KeyValue<>(k.key(),v))
.to(Serdes.String(),SerdeFactory.serdeFrom(Travel.class),TOPIC_TRAVEL);