目录
7. 全窗口函数 ProcessWindowFunction
package window
import java.text.SimpleDateFormat
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, WatermarkStrategy}
import org.apache.flink.api.common.functions.{AggregateFunction, ReduceFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.{ProcessWindowFunction, WindowFunction}
import org.apache.flink.streaming.api.windowing.assigners.{EventTimeSessionWindows, SlidingEventTimeWindows, TumblingEventTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
import source.ClickSource
case class UrlViewCount(url:String,count:Long,widowStart:String,windowEnd:String)
/**
*
* @PROJECT_NAME: flink1.13
* @PACKAGE_NAME: window
* @author: 赵嘉盟-HONOR
* @data: 2023-05-23 0:21
* @DESCRIPTION
*
*/
object windowFunction {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val data = env.addSource(new ClickSource)
.assignTimestampsAndWatermarks(WatermarkStrategy.forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner[source.Event] {
override def extractTimestamp(t: source.Event, l: Long): Long = t.timestamp
}))
//TODO 增量聚合函数 Reduce(流处理)
data.map(data => (data.user,1)).keyBy(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce((state,data)=>(data._1,data._2+state._2)).print("reduceLmd")
data.map(data => (data.user, 1)).keyBy(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce(new ReduceFunction[(String, Int)] {
override def reduce(t: (String, Int), t1: (String, Int)): (String, Int) = (t._1,t._2+t1._2)
}).print("reduceFunction")
val data2 = env.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp) //升序数据时间戳提取
.keyBy(data => "kry").window(TumblingEventTimeWindows.of(Time.seconds(10)))
//TODO 增量聚合函数 Aggregate(统计PV,UV,输出PV/UV)
data2
.aggregate(new AggregateFunction[source.Event,(Long,Set[String]),Double] {
override def createAccumulator(): (Long, Set[String]) = (0L,Set[String]())
override def add(in: source.Event, acc: (Long, Set[String])): (Long, Set[String]) = (acc._1+1,acc._2+in.user)
override def getResult(acc: (Long, Set[String])): Double = acc._1.toDouble/acc._2.size
override def merge(acc: (Long, Set[String]), acc1: (Long, Set[String])): (Long, Set[String]) = ???
}).print("aggregate")
//TODO 全窗口函数 windowFunction(apply)(批处理)
data2.apply(new WindowFunction[source.Event,String,String,TimeWindow] {
override def apply(key: String, window: TimeWindow, input: Iterable[source.Event], out: Collector[String]): Unit = {
var userSet=Set[String]()
input.foreach(userSet+=_.user)
val uv=userSet.size
val windowStart=window.getStart
val windowEnd=window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的UV值为 $uv")
}
}).print("windowFunction")
data2.apply((key,window,input,out:Collector[String])=>{
var userSet = Set[String]()
input.foreach(userSet += _.user)
val uv = userSet.size
val windowStart = window.getStart
val windowEnd = window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的UV值为 $uv")
})
//TODO 全窗口函数 processWindowFunction(process)
data2.process(new ProcessWindowFunction[source.Event,String,String,TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[source.Event], out: Collector[String]): Unit = {
var userSet = Set[String]()
elements.foreach(userSet += _.user)
val uv = userSet.size
val windowStart = context.window.getStart
val windowEnd = context.window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的UV值为 $uv")
}
}).print("processWindowFunction")
//TODO 增量聚合函数与券窗口函数的结合使用:分组计算PV
data2.aggregate(new AggregateFunction[source.Event,Long,Long] {
override def createAccumulator(): Long = 0L
override def add(in: source.Event, acc: Long): Long = acc+1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long = ???
},new ProcessWindowFunction[Long,UrlViewCount,String,TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Long], out: Collector[UrlViewCount]): Unit = {
out.collect(UrlViewCount(key,
elements.iterator.next(),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getStart),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getEnd)
))
}
}).print("windowFunctionPlusMax")
env.execute("windowFunction")
}
}
这段代码是一个使用 Apache Flink 进行窗口计算的示例,展示了如何使用不同的窗口函数和聚合函数来处理流数据。以下是代码各部分的详细解释以及相关原理知识的拓展。
代码各部分详细解释
1. 包导入与数据模型定义
package window
import java.text.SimpleDateFormat
import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, WatermarkStrategy}
import org.apache.flink.api.common.functions.{AggregateFunction, ReduceFunction}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.{ProcessWindowFunction, WindowFunction}
import org.apache.flink.streaming.api.windowing.assigners.{EventTimeSessionWindows, SlidingEventTimeWindows, TumblingEventTimeWindows, TumblingProcessingTimeWindows}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector
import source.ClickSource
case class UrlViewCount(url: String, count: Long, widowStart: String, windowEnd: String)
- 包导入:引入了 Flink 相关的各种类和接口,包括窗口分配器、时间语义、聚合函数、窗口函数等。
- UrlViewCount 样例类:定义了一个数据模型,用于存储 URL 的访问次数、窗口开始时间和结束时间。
2. 执行环境设置
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
- 创建执行环境:获取 Flink 的流执行环境。
- 设置并行度:将任务的并行度设置为 1,方便调试和观察结果。
3. 数据源与时间戳分配
val data = env.addSource(new ClickSource)
.assignTimestampsAndWatermarks(WatermarkStrategy.forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner[source.Event] {
override def extractTimestamp(t: source.Event, l: Long): Long = t.timestamp
}))
- 添加数据源:从自定义的
ClickSource
中获取数据。 - 分配时间戳和水位线:使用
WatermarkStrategy.forMonotonousTimestamps()
为单调递增的时间戳生成水位线,并通过withTimestampAssigner
从事件中提取时间戳。
4. 增量聚合函数 Reduce
data.map(data => (data.user, 1)).keyBy(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce((state, data) => (data._1, data._2 + state._2)).print("reduceLmd")
data.map(data => (data.user, 1)).keyBy(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce(new ReduceFunction[(String, Int)] {
override def reduce(t: (String, Int), t1: (String, Int)): (String, Int) = (t._1, t._2 + t1._2)
}).print("reduceFunction")
- 第一种方式:使用匿名函数进行增量聚合,计算每个用户在 5 秒滚动窗口内的点击次数。
- 第二种方式:实现
ReduceFunction
接口进行增量聚合,功能与第一种方式相同。
5. 增量聚合函数 Aggregate
val data2 = env.addSource(new ClickSource)
.assignAscendingTimestamps(_.timestamp)
.keyBy(data => "kry").window(TumblingEventTimeWindows.of(Time.seconds(10)))
data2
.aggregate(new AggregateFunction[source.Event, (Long, Set[String]), Double] {
override def createAccumulator(): (Long, Set[String]) = (0L, Set[String]())
override def add(in: source.Event, acc: (Long, Set[String])): (Long, Set[String]) = (acc._1 + 1, acc._2 + in.user)
override def getResult(acc: (Long, Set[String])): Double = acc._1.toDouble / acc._2.size
override def merge(acc: (Long, Set[String]), acc1: (Long, Set[String])): (Long, Set[String]) =???
}).print("aggregate")
- 数据源与窗口设置:从
ClickSource
获取数据,设置升序时间戳,按 "kry" 分组,使用 10 秒滚动窗口。 - 聚合函数:计算每个窗口内的平均 UV(Unique Visitor)。
6. 全窗口函数 WindowFunction
data2.apply(new WindowFunction[source.Event, String, String, TimeWindow] {
override def apply(key: String, window: TimeWindow, input: Iterable[source.Event], out: Collector[String]): Unit = {
var userSet = Set[String]()
input.foreach(userSet += _.user)
val uv = userSet.size
val windowStart = window.getStart
val windowEnd = window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的 UV 值为 $uv")
}
}).print("windowFunction")
data2.apply((key, window, input, out: Collector[String]) => {
var userSet = Set[String]()
input.foreach(userSet += _.user)
val uv = userSet.size
val windowStart = window.getStart
val windowEnd = window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的 UV 值为 $uv")
})
- 第一种方式:实现
WindowFunction
接口,计算每个窗口内的 UV 值并输出。 - 第二种方式:使用匿名函数实现相同的功能。
7. 全窗口函数 ProcessWindowFunction
data2.process(new ProcessWindowFunction[source.Event, String, String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[source.Event], out: Collector[String]): Unit = {
var userSet = Set[String]()
elements.foreach(userSet += _.user)
val uv = userSet.size
val windowStart = context.window.getStart
val windowEnd = context.window.getEnd
out.collect(s"窗口 $windowStart ~ $windowEnd 的 UV 值为 $uv")
}
}).print("processWindowFunction")
- 实现 ProcessWindowFunction:计算每个窗口内的 UV 值并输出,与
WindowFunction
类似,但提供了更多的上下文信息。
8. 增量聚合函数与全窗口函数的结合使用
data2.aggregate(new AggregateFunction[source.Event, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(in: source.Event, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long =???
}, new ProcessWindowFunction[Long, UrlViewCount, String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[Long], out: Collector[UrlViewCount]): Unit = {
out.collect(UrlViewCount(key,
elements.iterator.next(),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getStart),
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(context.window.getEnd)
))
}
}).print("windowFunctionPlusMax")
- 增量聚合:计算每个窗口内的点击次数。
- 全窗口函数:将点击次数、窗口开始时间和结束时间封装成
UrlViewCount
对象并输出。
代码原理知识背景拓展
1. 窗口计算基础
在流处理中,窗口计算是一种常用的技术,用于对一段时间内的数据进行聚合操作。Flink 提供了多种窗口类型,包括滚动窗口(Tumbling Windows)、滑动窗口(Sliding Windows)和会话窗口(Session Windows)。
2. 增量聚合函数
- ReduceFunction:对窗口内的数据进行逐步聚合,每次处理一个新元素时更新聚合结果。
- AggregateFunction:更灵活的增量聚合函数,允许定义初始值、累加器和合并逻辑。
3. 全窗口函数
- WindowFunction:在窗口关闭时对整个窗口的数据进行处理,适用于需要访问整个窗口数据的场景。
- ProcessWindowFunction:与
WindowFunction
类似,但提供了更多的上下文信息,如当前窗口的开始时间、结束时间和水位线等。
4. 时间语义
Flink 支持三种时间语义:事件时间(Event Time)、摄入时间(Ingestion Time)和处理时间(Processing Time)。事件时间是指事件实际发生的时间,通过水位线(Watermark)来处理乱序数据。
5. 水位线(Watermark)
水位线是 Flink 中处理乱序数据的关键机制,它表示一个时间戳,在这个时间戳之前的数据都已经到达。水位线的生成可以是单调递增的,也可以是有界乱序的。
6. 窗口分配器
Flink 提供了多种窗口分配器,用于将数据流划分为不同的窗口。滚动窗口是固定大小的窗口,滑动窗口可以重叠,会话窗口根据事件之间的时间间隔来划分。
7. 状态管理
在窗口计算中,Flink 需要管理窗口的状态,包括聚合结果、时间戳等。状态管理对于保证计算的准确性和容错性非常重要。
8. 容错机制
Flink 提供了强大的容错机制,通过检查点(Checkpoint)和重启策略来保证在故障发生时能够恢复计算。
通过以上解释和拓展,你应该对 Flink 的窗口计算有了更深入的理解。希望这段代码示例和原理知识对你有所帮助。