大数据培训 | 电商用户分析中如何对商品实时统计

本文详细介绍了如何使用Scala和Apache Flink构建一个实时的热门商品统计系统,包括数据准备、事件时间处理、过滤点击行为、滑动窗口聚合和Top N商品排名。通过Kafka作为数据源,实现了每5分钟更新一次的实时统计结果。
摘要由CSDN通过智能技术生成

首先要实现的是实时热门商品统计,我们将会基于 UserBehavior 数据集来进行分析。

项目主体用 Scala 编写,采用 IDEA 作为开发环境进行项目编写,采用 maven作为项目构建和管理工具。首先我们需要搭建项目框架。

创建 Maven 项目

项目框架搭建

打开 IDEA,创建一个 maven 项目,命名为 UserBehaviorAnalysis。由于包含了多 个模 块, 我 们可 以 以 UserBehaviorAnalysis 作 为父 项目 , 并在 其 下建 一个 名 为HotItemsAnalysis 的子项目,用于实时统计热门 top N 商品。

在 UserBehaviorAnalysis 下 新 建 一 个 maven module 作 为 子 项 目 , 命 名 为HotItemsAnalysis。

父项目只是为了规范化项目结构,方便依赖管理,本身是不需要代码实现的,所以 UserBehaviorAnalysis 下的 src 文件夹可以删掉。

声明项目中工具的版本信息

我们整个项目需要的工具的不同版本可能会对程序运行造成影响,所以应该在最外层的 UserBehaviorAnalysis 中声明所有子模块共用的版本信息。

在 pom.xml 中加入以下配置:

UserBehaviorAnalysis/pom.xml

<properties>

<flink.version>1.10.1</flink.version>

<scala.binary.version>2.12</scala.binary.version>

<kafka.version>2.2.0</kafka.version>

</properties>

更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com

 

添加项目依赖

对 于 整 个 项 目 而 言 , 所 有 模 块 都 会 用 到 flink 相 关 的 组 件 , 所 以 我 们 在UserBehaviorAnalysis 中引入公有依赖:

UserBehaviorAnalysis/pom.xml

<dependencies>

<dependency>

<groupId>org.apache.flink</groupId>

<artifactId>flink-scala_${scala.binary.version}</artifactId>

<version>${flink.version}</version>

</dependency>

<dependency>

<groupId>org.apache.flink</groupId>

<artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>

<version>${flink.version}</version>

</dependency>

<dependency>

<groupId>org.apache.kafka</groupId>

<artifactId>kafka_${scala.binary.version}</artifactId>

<version>${kafka.version}</version>

</dependency>

<dependency>

<groupId>org.apache.flink</groupId>

<artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>

<version>${flink.version}</version>

</dependency>

</dependencies>

更多 Java –大数据 –前端 –python 人工智能资料下载,可百度访问:尚硅谷官网www.atguigu.com

同样,对于 maven 项目的构建,可以引入公有的插件:

<build>

<plugins>

<!-- 该插件用于将 Scala 代码编译成 class 文件 -->

<plugin>

<groupId>net.alchim31.maven</groupId>

<artifactId>scala-maven-plugin</artifactId>

<version>4.4.0</version>

<executions>

<execution>

<!-- 声明绑定到 maven 的 compile 阶段 -->

<goals>

<goal>compile</goal>

</goals>

</execution>

</executions>

</plugin>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-assembly-plugin</artifactId>

<version>3.3.0</version>

<configuration>

<descriptorRefs>

<descriptorRef>

jar-with-dependencies

</descriptorRef>

</descriptorRefs>

</configuration>

<executions>

<execution>

<id>make-assembly</id>

<phase>package</phase>

<goals>

<goal>single</goal>

</goals>

</execution>

</executions>

</plugin>

</plugins>

</build>

在 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>来存储收到的每条 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 或其它存储,这里就不一一展开实现了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值