首先要实现的是实时热门商品统计,我们将会基于 UserBehavior 数据集来进行分析。
项目主体用 Scala 编写,采用 IDEA 作为开发环境进行项目编写,采用 maven作为项目构建和管理工具。首先我们需要搭建项目框架。
创建 Maven 项目
项目框架搭建
打开 IDEA,创建一个 maven 项目,命名为 UserBehaviorAnalysis。由于包含了多 个模 块, 我 们可 以 以 UserBehaviorAnalysis 作 为父 项目 , 并在 其 下建 一个 名 为HotItemsAnalysis 的子项目,用于实时统计热门 top N 商品。
在 UserBehaviorAnalysis 下 新 建 一 个 maven module 作 为 子 项 目 , 命 名 为HotItemsAnalysis。
父项目只是为了规范化项目结构,方便依赖管理,本身是不需要代码实现的,所以 UserBehaviorAnalysis 下的 src 文件夹可以删掉。
声明项目中工具的版本信息
我们整个项目需要的工具的不同版本可能会对程序运行造成影响,所以应该在最外层的 UserBehaviorAnalysis 中声明所有子模块共用的版本信息。
在 pom.xml 中加入以下配置:
UserBehaviorAnalysis/pom.xml
<flink.version>1.10.1</flink.version>
<scala.binary.version>2.12</scala.binary.version>
<kafka.version>2.2.0</kafka.version>
更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com
编辑
添加项目依赖
对 于 整 个 项 目 而 言 , 所 有 模 块 都 会 用 到 flink 相 关 的 组 件 , 所 以 我 们 在UserBehaviorAnalysis 中引入公有依赖:
UserBehaviorAnalysis/pom.xml
org.apache.flink
flink-scala_${scala.binary.version}
${flink.version}
org.apache.flink
flink-streaming-scala_${scala.binary.version}
${flink.version}
org.apache.kafka
kafka_${scala.binary.version}
${kafka.version}
org.apache.flink
flink-connector-kafka_${scala.binary.version}
${flink.version}
更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com
同样,对于 maven 项目的构建,可以引入公有的插件:
net.alchim31.maven
scala-maven-plugin
4.4.0
compile
org.apache.maven.plugins
maven-assembly-plugin
3.3.0
jar-with-dependencies
make-assembly
package
single
在 HotItemsAnalysis 子模块中,我们并没有引入更多的依赖,所以不需要改动pom 文件。
数据准备
在 src/main/目录下,可以看到已有的默认源文件目录是 java,我们可以将其改名为 scala。将数据文件 UserBehavior.csv 复制到资源文件目录 src/main/resources 下,我们将从这里读取数据_大数据培训。
至此,我们的准备工作都已完成,接下来可以写代码了。
模块代码实现
我们将实现一个“实时热门商品”的需求,可以将“实时热门商品”翻译成程序员更好理解的需求:每隔 5 分钟输出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情:
• 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口
• 过滤出点击行为数据
• 按一小时的窗口大小,每 5 分钟统计一次,做滑动窗口聚合(Sliding Window)
• 按每个窗口聚合,输出每个窗口中点击量前 N 名的商品
更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com
程序主体
在 src/main/scala 下创建 HotItems.scala 文件,新建一个单例对象。定义样例类UserBehavior 和 ItemViewCount,在 main 函数中创建 StreamExecutionEnvironment 并做配置,然后从 UserBehavior.csv 文件中读取数据,并包装成 UserBehavior 类型。
代码如下:
HotItemsAnalysis/src/main/scala/HotItems.scala
case class UserBehavior(userId: Long, itemId: Long, categoryId: Int, behavior: String,
timestamp: Long)
case class ItemViewCount(itemId: Long, windowEnd: Long, count: Long)
object HotItems {
def main(args: Array[String]): Unit = {
// 创建一个 StreamExecutionEnvironment
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 设定 Time 类型为 EventTime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 为了打印到控制台的结果不乱序,我们配置全局的并发为 1,这里改变并发对结果正确性没有影响
env.setParallelism(1)
val stream = env
// 以 window 下为例,需替换成自己的路径
.readTextFile("YOUR_PATH\resources\UserBehavior.csv")
.map(line => {
val linearray = line.split(",")
UserBehavior(linearray(0).toLong, linearray(1).toLong, linearray(2).toInt,
linearray(3), linearray(4).toLong)
})
// 指定时间戳和 watermark
.assignAscendingTimestamps(_.timestamp * 1000)
env.execute("Hot Items Job")
}
这里注意,我们需要统计业务时间上的每小时的点击量,所以要基于 EventTime来处理。那么如果让 Flink 按照我们想要的业务时间来处理呢?这里主要有两件事情要做。
第一 件是 告诉 Flink 我们 现在 按照 EventTime 模式 进行 处理 , Flink 默认 使用ProcessingTime 处理,所以我们要显式设置如下:
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
第二件事情是指定如何获得业务时间,以及生成 Watermark。Watermark 是用来追踪业务事件的概念,可以理解成 EventTime 世界中的时钟,用来指示当前处理到什么时刻的数据了。由于我们的数据源的数据已经经过整理,没有乱序,即事件的时间戳是单调递增的,所以可以将每条数据的业务时间就当做 Watermark。这里我们用 assignAscendingTimestamps 来实现时间戳的抽取和 Watermark 的生成_大数据视频。
注:真实业务场景一般都是乱序的,所以一般不用 assignAscendingTimestamps,而是使用 BoundedOutOfOrdernessTimestampExtractor。
.assignAscendingTimestamps(_.timestamp * 1000)这样我们就得到了一个带有时间标记的数据流了,后面就能做一些窗口的操作。
过滤出点击事件
在开始窗口操作之前,先回顾下需求“每隔 5 分钟输出过去一小时内点击量最多的前 N 个商品”。由于原始数据中存在点击、购买、收藏、喜欢各种行为的数据,但是我们只需要统计点击量,所以先使用 filter 将点击行为数据过滤出来。
.filter(_.behavior == "pv")
设置滑动窗口,统计点击量
由于要每隔 5 分钟统计一次最近一小时每个商品的点击量,所以窗口大小是一小 时 , 每 隔 5 分 钟 滑 动 一 次 。 即 分 别 要 统 计 [09:00, 10:00), [09:05, 10:05), [09:10, 10:10)…等窗口的商品点击量。是一个常见的滑动窗口需求(Sliding Window)。
.keyBy("itemId")
.timeWindow(Time.minutes(60), Time.minutes(5))
.aggregate(new CountAgg(), new WindowResultFunction());
我们使用.keyBy("itemId")对商品进行分组,使用.timeWindow(Time size, Time slide)对 每 个 商 品 做 滑 动 窗 口 ( 1 小 时 窗 口 , 5 分 钟 滑 动 一 次 ) 。 然 后 我 们 使用 .aggregate(AggregateFunction af, WindowFunction wf) 做增量的聚合操作,它能使用 AggregateFunction 提 前 聚 合 掉 数 据 , 减 少 state 的 存 储 压 力 。 较之 .apply(WindowFunction wf) 会将窗口中的数据都存储下来,最后一起计算要高效地多。这里的 CountAgg 实现了 AggregateFunction 接口,功能是统计窗口中的条数,即遇到一条数据就加一。
// COUNT 统计的聚合函数实现,每出现一条记录就加一
class CountAgg extends AggregateFunction[UserBehavior, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(userBehavior: UserBehavior, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc1: Long, acc2: Long): Long = acc1 + acc2
}
聚 合 操 作 .aggregate(AggregateFunction af, WindowFunction wf) 的 第 二 个 参 数WindowFunction 将每个 key 每个窗口聚合后的结果带上其他信息进行输出。我们这里 实 现 的 WindowResultFunction 将 < 主 键 商 品 ID , 窗 口 , 点 击 量 > 封 装 成 了ItemViewCount 进行输出。
// 商品点击量(窗口操作的输出类型)
case class ItemViewCount(itemId: Long, windowEnd: Long, count: Long)
代码如下:
// 用于输出窗口的结果
class WindowResultFunction extends WindowFunction[Long, ItemViewCount, Tuple,
TimeWindow] {
override def apply(key: Tuple, window: TimeWindow, aggregateResult: Iterable[Long],
collector: Collector[ItemViewCount]) : Unit = {
val itemId: Long = key.asInstanceOf[Tuple1[Long]].f0
val count = aggregateResult.iterator.next
collector.collect(ItemViewCount(itemId, window.getEnd, count))
}
}
现在我们就得到了每个商品在每个窗口的点击量的数据流。
更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com
计算最热门 Top N 商品
为了统计每个窗口下最热门的商品,我们需要再次按窗口进行分组,这里根据ItemViewCount 中的 windowEnd 进行 keyBy()操作。然后使用 ProcessFunction 实现一个自定义的 TopN 函数 TopNHotItems 来计算点击量排名前 3 名的商品,并将排名结果格式化成字符串,便于后续输出。
.keyBy("windowEnd")
.process(new TopNHotItems(3)); // 求点击量前 3 名的商品
ProcessFunction 是 Flink 提供的一个 low-level API,用于实现更高级的功能。它主要提供了定时器 timer 的功能(支持 EventTime 或 ProcessingTime)。本案例中我们 将利 用 timer 来 判断 何 时 收齐 了 某 个 window 下 所有 商 品 的点 击 量 数据 。 由于Watermark 的 进 度 是 全 局 的 , 在 processElement 方 法 中 , 每 当 收 到 一 条 数 据ItemViewCount,我们就注册一个 windowEnd+1 的定时器(Flink 框架会自动忽略同一时间的重复注册)。windowEnd+1 的定时器被触发时,意味着收到了 windowEnd+1的 Watermark,即收齐了该 windowEnd 下的所有商品窗口统计值。我们在 onTimer()中处理将收集的所有商品及点击量进行排序,选出 TopN,并将排名信息格式化成字符串后进行输出。
这里我们还使用了 ListState来存储收到的每条 ItemViewCount消息,保证在发生故障时,状态数据的不丢失和一致性。ListState 是 Flink 提供的类似 Java List 接 口 的 State API , 它 集 成 了 框 架 的 checkpoint 机 制 , 自 动 做 到 了exactly-once 的语义保证。
// 求某个窗口中前 N 名的热门点击商品,key 为窗口时间戳,输出为 TopN 的结果字符串
class TopNHotItems(topSize: Int) extends KeyedProcessFunction[Tuple, ItemViewCount,
String] {
private var itemState : ListState[ItemViewCount] = _
override def open(parameters: Configuration): Unit = {
super.open(parameters)
// 命名状态变量的名字和状态变量的类型
val itemsStateDesc = new ListStateDescriptor[ItemViewCount]("itemState-state",
classOf[ItemViewCount])
// 定义状态变量
itemState = getRuntimeContext.getListState(itemsStateDesc)
}
override def processElement(input: ItemViewCount, context:
KeyedProcessFunction[Tuple, ItemViewCount, String]#Context, collector:
Collector[String]): Unit = {
// 每条数据都保存到状态中
itemState.add(input)
// 注册 windowEnd+1 的 EventTime Timer, 当触发时,说明收齐了属于 windowEnd 窗口的所有商品数据
// 也就是当程序看到 windowend + 1 的水位线 watermark 时,触发 onTimer 回调函数
context.timerService.registerEventTimeTimer(input.windowEnd + 1)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple,
ItemViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
// 获取收到的所有商品点击量
val allItems: ListBuffer[ItemViewCount] = ListBuffer()
import scala.collection.JavaConversions._
for (item <- itemState.get) {
allItems += item
}
// 提前清除状态中的数据,释放空间
itemState.clear()
// 按照点击量从大到小排序
val sortedItems = allItems.sortBy(_.count)(Ordering.Long.reverse).take(topSize)
// 将排名信息格式化成 String, 便于打印
val result: StringBuilder = new StringBuilder
result.append("====================================\n")
result.append("时间: ").append(new Timestamp(timestamp - 1)).append("\n")
for(i <- sortedItems.indices){
val currentItem: ItemViewCount = sortedItems(i)
// e.g. No1: 商品 ID=12224 浏览量=2413
result.append("No").append(i+1).append(":")
.append(" 商品 ID=").append(currentItem.itemId)
.append(" 浏览量=").append(currentItem.count).append("\n")
}
result.append("====================================\n\n")
// 控制输出频率,模拟实时滚动结果
Thread.sleep(1000)
out.collect(result.toString)
}
}
最后我们可以在 main 函数中将结果打印输出到控制台,方便实时观测:.print();
至此整个程序代码全部完成,我们直接运行 main 函数,就可以在控制台看到不断输出的各个时间点统计出的热门商品。
完整代码
最终完整代码如下:
case class UserBehavior(userId: Long, itemId: Long, categoryId: Int, behavior: String,
timestamp: Long)
case class ItemViewCount(itemId: Long, windowEnd: Long, count: Long)
object HotItems {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream = env
.readTextFile("YOUR_PATH\resources\UserBehavior.csv")
.map(line => {
val linearray = line.split(",")
UserBehavior(linearray(0).toLong, linearray(1).toLong, linearray(2).toInt,
linearray(3), linearray(4).toLong)
})
.assignAscendingTimestamps(_.timestamp * 1000)
.filter(_.behavior=="pv")
.keyBy("itemId")
.timeWindow(Time.minutes(60), Time.minutes(5))
.aggregate(new CountAgg(), new WindowResultFunction())
.keyBy(1)
.process(new TopNHotItems(3))
.print()
env.execute("Hot Items Job")
}
// COUNT 统计的聚合函数实现,每出现一条记录加一
class CountAgg extends AggregateFunction[UserBehavior, Long, Long] {
override def createAccumulator(): Long = 0L
override def add(userBehavior: UserBehavior, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc1: Long, acc2: Long): Long = acc1 + acc2
}
// 用于输出窗口的结果
class WindowResultFunction extends WindowFunction[Long, ItemViewCount, Tuple,
TimeWindow] {
override def apply(key: Tuple, window: TimeWindow, aggregateResult: Iterable[Long],
collector: Collector[ItemViewCount]) : Unit = {
val itemId: Long = key.asInstanceOf[Tuple1[Long]].f0
val count = aggregateResult.iterator.next
collector.collect(ItemViewCount(itemId, window.getEnd, count))
}
}
// 求某个窗口中前 N 名的热门点击商品,key 为窗口时间戳,输出为 TopN 的结果字符串
class TopNHotItems(topSize: Int) extends KeyedProcessFunction[Tuple, ItemViewCount,
String] {
private var itemState : ListState[ItemViewCount] = _
override def open(parameters: Configuration): Unit = {
super.open(parameters)
// 命名状态变量的名字和状态变量的类型
val itemsStateDesc = new ListStateDescriptor[ItemViewCount]("itemState-state",
classOf[ItemViewCount])
// 从运行时上下文中获取状态并赋值
itemState = getRuntimeContext.getListState(itemsStateDesc)
}
override def processElement(input: ItemViewCount, context:
KeyedProcessFunction[Tuple, ItemViewCount, String]#Context, collector:
Collector[String]): Unit = {
// 每条数据都保存到状态中
itemState.add(input)
// 注册 windowEnd+1 的 EventTime Timer, 当触发时,说明收齐了属于 windowEnd 窗口的所有商品数据
// 也就是当程序看到 windowend + 1 的水位线 watermark 时,触发 onTimer 回调函数
context.timerService.registerEventTimeTimer(input.windowEnd + 1)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple,
ItemViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
// 获取收到的所有商品点击量
val allItems: ListBuffer[ItemViewCount] = ListBuffer()
import scala.collection.JavaConversions._
for (item <- itemState.get) {
allItems += item
}
// 提前清除状态中的数据,释放空间
itemState.clear()
// 按照点击量从大到小排序
val sortedItems = allItems.sortBy(_.count)(Ordering.Long.reverse).take(topSize)
// 将排名信息格式化成 String, 便于打印
val result: StringBuilder = new StringBuilder
result.append("====================================\n")
result.append("时间: ").append(new Timestamp(timestamp - 1)).append("\n")
for(i <- sortedItems.indices){
val currentItem: ItemViewCount = sortedItems(i)
// e.g. No1: 商品 ID=12224 浏览量=2413
result.append("No").append(i+1).append(":")
.append(" 商品 ID=").append(currentItem.itemId)
.append(" 浏览量=").append(currentItem.count).append("\n")
}
result.append("====================================\n\n")
// 控制输出频率,模拟实时滚动结果
Thread.sleep(1000)
out.collect(result.toString)
}
}
}
更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com
更换 Kafka 作为数据源
实际生产环境中,我们的数据流往往是从 Kafka 获取到的。如果要让代码更贴近生产实际,我们只需将 source 更换为 Kafka 即可:
val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost:9092")
properties.setProperty("group.id", "consumer-group")
properties.setProperty("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
env.setParallelism(1)
val stream = env
.addSource(new FlinkKafkaConsumer[String]("hotitems", new SimpleStringSchema(),
properties))
当然,根据实际的需要,我们还可以将 Sink 指定为 Kafka、ES、Redis 或其它存储,这里就不一一展开实现了。