场景描述:统计某个商品(商品id)在10min,30min,1hour内的购买量。我们把商品的每一次购买事件作为流,对其加窗,统计各需求时间段内的购买次数。
1.先创建一个读取数据的topic:window-count,再创建一个统计结果输出的topic:window-count-out。
2.编写代码。
3.启动程序WindowCount。
package teststreams;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.KGroupedStream;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KeyValueMapper;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.TimeWindows;
/**
* @description count 10min,30min,1hour groupBy itemId
* @author wyhui
*
*/
public class WindowCount {
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(StreamsConfig.APPLICATION_ID_CONFIG, "mywindowcount");
prop.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.184.128:9092");//kafka服务器IP
prop.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
prop.put(StreamsConfig.STATE_DIR_CONFIG, "C:\\IT\\tool\\kafka-state-store");//设置状态仓库的存储路径
StreamsBuilder builder = new StreamsBuilder();
//从topic中读入数据
KStream input = builder.stream("window-count");
//使用map()将数据的Key设置为与value相同。
KGroupedStream groupBy = input
.map(new KeyValueMapper>(){
@Override
public KeyValue apply(String key, String value) {
return new KeyValue(value, value);
}
})
//gorupBy()可以指定任意想要group的值
.groupBy(new KeyValueMapper(){
@Override
public String apply(String key, String value) {
//这里我们根据itemId来进行group,此方法要返回的就是我们要group的值,也就是group之后的key。
//假设我们的topic读入的数据格式是:itemId="0001",itemName="华硕电脑",userId="1003"
return value.substring(8, 12);//截取itemId
}
});
/*
* 对数据进行group之后,我们该对不同的itemId加窗统计,由于这里我们是想求得同一个Id在不同时间段内的购买次数,
* 就需要施加多个window,所以在这里我们需要分成多个分流进行不同的加窗,然后把最终的结果都输出到window-count-out这个topic中。
* 要对上面的同一个group结果进行不同的操作,我们就需要将其复制多份来进行不同的操作。
*/
KGroupedStream groupBy10min = groupBy;
KGroupedStream groupBy30min = groupBy;
KGroupedStream groupBy1hour = groupBy;
groupBy10min.windowedBy(TimeWindows.of(TimeUnit.MINUTES.toMillis(1)))//以Min为单位,转换为毫秒
.count(Materialized.as("itemGroupBy1min-state-store"))//指定存储中间状态的state-store,存储在上面配置的STATE_DIR_CONFIG路径下
.toStream()//经过上面的count之后得到的是KTable,输出到topic中需要转为流
//经过上面的操作之后,会自动地在key的后面加上一个时间戳,而我们在输出时并不需要这些数据,所以使用map()将key中的时间戳处理掉
/*
* .map((key, count) -> KeyValue.pair(key.toString(), count.toString()+"10min"))
* 如果使用这样的方法,得到的输出结果就是:offset:0,key:[0001@1550313600000/1550314200000],value:110min
* 所以我们需要对Key进行处理
*/
//上面统计的count是Long类型的,此处需要转为String。为了在输出count时能区分出是哪个时间段统计的,在这里我们加上"10min"来指示
.map((key, count) -> KeyValue.pair(key.toString().substring(1, key.toString().indexOf("@")), count.toString()+"******1min"))
.to("window-count-out");//输出到topic中
groupBy30min.windowedBy(TimeWindows.of(TimeUnit.MINUTES.toMillis(3)))//以Min为单位,转换为毫秒
.count(Materialized.as("itemGroupBy3min-state-store"))//指定存储中间状态的state-store
.toStream()
.map((key, count) -> KeyValue.pair(key.toString().substring(1, key.toString().indexOf("@")), count.toString()+"******3min"))
.to("window-count-out");
groupBy1hour.windowedBy(TimeWindows.of(TimeUnit.HOURS.toMillis(1)))
.count(Materialized.as("itemGroupBy1hour-state-store"))
.toStream()
.map((key, count) -> KeyValue.pair(key.toString().substring(1, key.toString().indexOf("@")), count.toString()+"*******1hour"))
.to("window-count-out");
KafkaStreams streams = new KafkaStreams(builder.build(), prop);
streams.cleanUp();
streams.start();
Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
}
}
在多次运行该程序时可能会出现Exception in thread "main" org.apache.kafka.streams.errors.StreamsException: java.nio.file.DirectoryNotEmptyException,解决方案:https://blog.csdn.net/QYHuiiQ/article/details/87467981
4.启动cosumer:
package mykafka;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
/**
* 消息的消費者
* @author wyhui
*
*/
public class MyConsumer {
public static void main(String[] args) {
Properties consumerProp = new Properties();
consumerProp.put("bootstrap.servers","192.168.184.128:9092");
consumerProp.put("group.id", "wyhtest");
consumerProp.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProp.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProp.put("enable.auto.commit", "true");
consumerProp.put("auto.commit.interval.ms", "30000");
KafkaConsumer consumer = new KafkaConsumer(consumerProp);
consumer.subscribe(Arrays.asList("window-count-out"));
while(true) {
ConsumerRecords records = consumer.poll(1000);
for(ConsumerRecord record : records) {
long offset = record.offset();
Object key = record.key();
Object value = record.value();
System.out.println("offset:"+offset+",key:"+key+",value:"+value);
}
}
}
}
5.向window-count中发送数据:
package mykafka;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
/**
* 消息生产者实体类
* @author wyhui
*/
public class MyProducer {
private final static String TOPIC = "window-count";
private static KafkaProducer producer = null;
public static void main(String[] args) {
Properties producerProp = new Properties();
producerProp.put("bootstrap.servers","192.168.184.128:9092");
producerProp.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProp.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer(producerProp);
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1115\",itemName=\"联想电脑\",userId=\"1003\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1116\",itemName=\"华硕电脑\",userId=\"1023\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1118\",itemName=\"惠普电脑\",userId=\"1031\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1117\",itemName=\"戴尔电脑\",userId=\"1026\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1116\",itemName=\"华硕电脑\",userId=\"1036\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1117\",itemName=\"戴尔电脑\",userId=\"1053\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1115\",itemName=\"联想电脑\",userId=\"1065\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1116\",itemName=\"华硕电脑\",userId=\"1077\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1118\",itemName=\"惠普电脑\",userId=\"1046\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1115\",itemName=\"联想电脑\",userId=\"1076\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1117\",itemName=\"戴尔电脑\",userId=\"1084\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1116\",itemName=\"华硕电脑\",userId=\"1027\""));//key为空
producer.send(new ProducerRecord<>(TOPIC, null, "itemId=\"1115\",itemName=\"联想电脑\",userId=\"1099\""));//key为空
System.out.println("消息发送完毕");
producer.close();
}
}
由于我们在测试时使用的是1min,3min,1hour,所以我们实行以下发送数据计划:
7:21分-------启动windowCount程序,启动cosumer程序。
7:22分-------启动producer程序,第一次发送数据,在每次的数据中,itemId分别出现的次数为:1115(4),1116(4),1117(3),1118(2)。
此时的消费输出结果为:
7:22分------在这一分钟内第二次发送数据,是为了验证1min的时间窗口计数是否正确,输出的结果为:
可以看到在这1min内每个id的count还是在刚才1min的基础上累加的,说明数据计时正确。
7:23分发送数据,此时已经超过1min,所有id在1min的计算窗口内都是重新计算了,而3min,1hour的计数还是在之前的结果上累加,说明数据计数正确:
7:24发送数据,此时距离WindowCount程序启动已经超过3min,所以3min的数据重新计算(在计时过程中由于我没有精确到秒,所以可能会出现误差):
以上就是对同一数据流进行不同时间的加窗统计count。
项目源码:https://github.com/wyhuiii/KafkaStreams-count