1,窗口(window) 概念
窗口, 就是把无界的数据流, 依据一定规则划分成一段一段的有界数据流来计算;
既然划分成有界数据段, 通常都是为了"聚合";
Keyedwindow 重要特性: 任何一个窗口, 都绑定在自己所属的 key 上; 不同 key 的数据肯定不会划分到相同窗口中去!
2、窗口细分类型
2.1 滑动创建
window size: 表示窗口的长度,比如1小时
window slide: 表示窗口滑动的时间间隔,或者就叫滑动长度,也就是每隔多长时间(或者多少条数据)滑动一次
上面的图,可以理解为:窗口大小为1小时,滑动步长为30分钟。
上面的例子中同一条数据,最多同时属于2个窗口(窗口大小除以窗口滑动步长)
2.2 滚动窗口
window size : 表示窗口的大小,比如1小时。
滚动窗口的应用场景:比如每隔1小时,统计上一个小时的订单总量。
滚动窗口实际上就是滑动窗口的一个特例:也就是滑动步长等于窗口大小
2.3 会话窗口
没有固定的窗口长度, 也没有固定的滑动步长, 而是根据数据流中前后两个事件的时间
间隔是否超出阈值(session gap) 来划分;
比如上图中,对于user1,window1这个窗口中收到2条数据(假设窗口时间间隔是5秒),在收到第2条数据后,超过5秒没有收到数据,这时候前面的2条数据就会被划分为一个窗口。比如过了8秒,又收到了4条数据,又过了5秒没收到数据,那么这4条数据就被划分为一个 窗口。 以此类推。
3、窗口计算基本API代码示例
keyed window(带key的窗口),具体的API是 ds.window(....)
NonKeyed Window(不带key的窗口),具体的API是 ds.windowAll(....)
Flink有两类窗口聚合算子:
窗口聚合算子, 整体上分为两类
增量聚合算子, 如 min、 max、 minBy、 maxBy、 sum、 reduce、 aggregate
全量聚合算子, 如 apply、 process
两类聚合算子的底层区别
增量聚合: 一次取一条数据, 用聚合函数对中间累加器更新; 窗口触发时, 取累加器输出结果;
全量聚合: 数据“攒” 在状态容器中, 窗口触发时, 把整个窗口的数据交给聚合函数;
import java.util.Properties
import com.sunzm.flink.demo.datastream.window.agg.CountAggregateFunction
import com.sunzm.flink.demo.datastream.window.process.CountProcessWindowFunction
import org.apache.flink.api.common.eventtime.WatermarkStrategy
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.typeinfo.BasicTypeInfo
import org.apache.flink.connector.kafka.source.KafkaSource
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.kafka.clients.consumer.{ConsumerConfig, OffsetResetStrategy}
object WindowDemo {
def main(args: Array[String]): Unit = {
//创建一个flink执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 为了方便测试,设置一个并行度
env.setParallelism(1)
val properties = new Properties()
properties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
val kafkaSource = KafkaSource.builder[String]()
//设置连接的broker地址
.setBootstrapServers("192.168.1.184:9092,192.168.1.185:9092,192.168.1.186:9092")
//设置反序列化器 (字符串)
.setValueOnlyDeserializer(new SimpleStringSchema)
//设置消费的组Id
.setGroupId("flink-demo-group")
// 设置消费的主题
.setTopics("test")
//设置从哪里开始消费
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))
.build()
val kafkaDataStream: DataStream[String] = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(),
"kafkaSource")(BasicTypeInfo.STRING_TYPE_INFO)
//滚动窗口 10秒滚动一次 计算收到 的数据条数
//如果直接调用 .aggregate(new CountProcessWindowFunction) 方法,输入数据的类型就是 kafkaDataStream 的数据类型
//如果调用 .aggregate(new CountAggregateFunction, new CountProcessWindowFunction) 方法, 那么 CountProcessWindowFunction 输入数据的类型是 CountAggregateFunction 输出数据的的类型
//如果调用 .aggregate(new CountAggregateFunction) 方法, 那么最终输出数据的类型就是 CountAggregateFunction 的输出类型
//如果调用 .aggregate(new CountAggregateFunction, new CountProcessWindowFunction) 方法, 那么最终输出数据的类型就是 CountProcessWindowFunction 的输出类型
val result = kafkaDataStream.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(10)))
.aggregate(new CountAggregateFunction, new CountProcessWindowFunction)
val keyedDS = kafkaDataStream.keyBy(line => {
val jSONObject = JSON.parseObject(line)
jSONObject.getString("userId")
})
//基于事件时间语义的滚动窗口 (窗口长度为10秒)
keyedDS.window(TumblingEventTimeWindows.of(Time.seconds(10)))
//.aggregate()
//基于处理时间语义的滚动窗口 (窗口长度为10秒)
keyedDS.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
//基于事件时间语义的滑动窗口 (窗口长度为10秒, 滑动长度为5秒)
keyedDS.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
//基于处理时间语义的滑动窗口 (窗口长度为10秒, 滑动长度为5秒)
keyedDS.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
//基于事件时间语义的会话窗口 (时间间隔为10分钟)
keyedDS.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
//基于处理时间语义的会话窗口 (时间间隔为10分钟)
keyedDS.window(ProcessingTimeSessionWindows.withGap(Time.minutes(10)))
result.print()
//5.执行 ProcessFunctionDemo$
env.execute(this.getClass.getSimpleName.stripSuffix("$"))
}
}
import org.apache.flink.api.common.functions.AggregateFunction
/**
* 第一个泛型:收到的数据类型,也就是调用这个算子的数据类型
* 第二个泛型:中间聚合结果的类型,这个例子中计算窗口收到的数据条数,所以是long类型
* 第三个泛型:输出结果的类型
*/
class CountAggregateFunction extends AggregateFunction[String, Long, Long]{
override def createAccumulator(): Long = {
//初始化一个中间结果累加器对象, 这个方法只会在每隔个窗口创建的时候调用一次
0L
}
override def add(value: String, accumulator: Long): Long = {
//每收到一条数据调用一次, 更新中间累加结果
//value是新收到的数据, accumulator是上一次的累加结果
accumulator + 1
}
override def getResult(accumulator: Long): Long = {
//窗口触发计算的时候执行一次,如果窗口10秒触发一次,那么这个方法10秒就会调用一次
accumulator
}
override def merge(a: Long, b: Long): Long = {
//合并中间结果
a + b
}
}
import org.apache.commons.lang.time.DateFormatUtils
import org.apache.flink.streaming.api.scala.function.ProcessAllWindowFunction
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
/**
* 第一个泛型: 输入数据的类型
* 第二个泛型: 输出数据的类型
* 第三个泛型: 窗口的类型
*/
class CountProcessWindowFunction extends ProcessAllWindowFunction[Long, String, TimeWindow]{
override def process(context: Context, elements: Iterable[Long],
out: Collector[String]): Unit = {
val window = context.window
//窗口开始时间
val start = window.getStart
//窗口结束时间
val end = window.getEnd
val startTimeStr = DateFormatUtils.format(start, "yyyy-MM-dd HH:mm:ss")
val endTimeStr = DateFormatUtils.format(end, "yyyy-MM-dd HH:mm:ss")
var sum = 0L
elements.foreach(count => {
sum += count
})
val res = s"窗口开始时间: ${startTimeStr}, 窗口结束时间: ${endTimeStr}, 本窗口收到的数据条数: ${sum}"
out.collect(res)
}
}