前进的路很艰难,但只要坚持下去,终会收获丰硕的果实!
一.项目需求
每隔5分钟输出最近一小时内点击量最多的前N个商品
具体步骤
• 抽取出业务时间戳,告诉Flink框架基于事件时间(Event Time)做窗口
• 过滤出点击行为数据
• 按一小时的窗口大小,每5分钟统计一次,做滑动窗口聚合(Sliding Window)
• 按每个窗口聚合,输出每个窗口中点击量前N名的商品
二.数据准备
字段名 | 说明(从上往下与上述对应) |
---|---|
userId | Long 加密后的用户ID |
itemId | Long 加密后的商品ID |
categoryId | Int 加密后的商品所属类别ID |
behavior | String 用户行为类型,包括(pv :点击, buy :购买 ,cart :加入购物车, fav :喜爱) |
timestamp | Long 行为发生的时间戳,单位秒 |
数据下载连接
链接:https://pan.baidu.com/s/1neTpDyCpirdcVxZ_5DOIUg
提取码:9e07
三.代码演示
1.定义样例类
数据用样例包装后进行传输
/**
* 定义输入数据的样例类
* @param userid 用户ID
* @param itemId 商品ID
* @param categoryId 商品所属类别
* @param behavior 用户行为类型
* @param timestamp 行为发生的时间戳
*/
case class UserBehavior(userid: Long, itemId: Long, categoryId: Int, behavior: String, timestamp: Long)
/**
* 定义窗口聚合结果样例类
*
* @param itemId 窗口id
* @param windowEnd 窗口结束事件
* @param cout 窗口聚合结果
*/
case class ItemViewCount(itemId:Long,windowEnd:Long,cout:Long)
2.主体代码
//1.创建流式处理环境
val environment = StreamExecutionEnvironment.getExecutionEnvironment
//设置全局并行度
environment.setParallelism( 1 )
//设置时间语义为事件时间
environment.setStreamTimeCharacteristic( TimeCharacteristic.EventTime )
//2.读取数据
val dataStream: DataStream[String] = environment.readTextFile( "C:\\Users\\86188\\Documents\\Tencent " +
"Files\\510580876\\FileRecv\\UserBehaviorAnalysis\\HotItemsAnalysis\\src\\main\\resources\\UserBehavior.csv" )
//将读进来的数据转换样例类格式
val inputStream = dataStream.map( data => {
val field = data.split( "," )
UserBehavior( field( 0 ).trim.toLong, field( 1 ).trim.toLong, field( 2 ).trim.toInt, field( 3 ).trim, field( 4 ).trim.toLong )
} )
//指点字段为事件事件
.assignAscendingTimestamps( _.timestamp * 1000L )
//3.transaction 处理数据
val processDataString = inputStream
.filter( _.behavior == "pv" ) //筛选出用户行为为 pv(浏览)记录
.keyBy( _.itemId ) //根据商品 id 进行分组
.timeWindow( Time.hours( 1 ), Time.minutes( 5 ) ) //定义滑动窗口 大小1小时,滑动时间5分钟
.aggregate( new CoutAgg(), new WindowResult() ) //窗口聚合
.keyBy( _.windowEnd ) //按照窗口分组
.process( new TopNHotItems( 3 ) ) //求出topn前3
//4.sink:控制台输出
processDataString.print()
environment.execute()
3.函数类
CoutAgg类
1.每条记录调用此函数,对每条数据进行累加
2.这个函数继承父类AggregateFunction,该函数的输出为windowFunction的输入
/**
* 泛型具体含义
* UserBehavior 表示输入的数据类型
* Long 中间结果的状态,即累加器的状态
* Long 最终的聚合结果,该结果做为windowFunction的输入传入
*/
class CoutAgg extends AggregateFunction[UserBehavior, Long, Long] {
/**
* 该方法用于初始化累加器
*
* @return 返回累加器的初始值
*/
override def createAccumulator(): Long = 0L
/**
* 每传入一条记录都会调用该方法
*
* @param value 传入的数据
* @param accumulator 累加器
* @return 返回累加器结果
*/
override def add(value: UserBehavior, accumulator: Long): Long = accumulator + 1L
/**
* 输出累加器的状态
*/
override def getResult(accumulator: Long): Long = accumulator
/**
* 累加器和另一个分区的累加器聚合逻辑
*
* @param a 第一个累加器
* @param b 第二个累加器
* @return 返回聚合结果
*/
override def merge(a: Long, b: Long): Long = a + b
}
WindowResult类
每个窗口调用该函数
class WindowResult extends WindowFunction[Long,ItemViewCount,Long,TimeWindow]{
override def apply(key: Long,
window: TimeWindow,
input: Iterable[Long],
out: Collector[ItemViewCount]): Unit = {
out.collect(ItemViewCount(key,window.getEnd,input.iterator.next()))
}
}
class TopNHotItems(topSize: Int) extends KeyedProcessFunction[Long, ItemViewCount, String] {
//定义一个状态列表
private var itemState: ListState[ItemViewCount] = _
override def open(parameters: Configuration): Unit = {
//初始化状态列表
itemState = getRuntimeContext.getListState( new ListStateDescriptor[ItemViewCount]( "itemState", classOf[ItemViewCount] ) )
}
override def processElement(value: ItemViewCount,
ctx: KeyedProcessFunction[Long, ItemViewCount, String]#Context,
ut: Collector[String]): Unit = {
//将每行数据存入状态列表
itemState.add( value )
//注册一个定时器
ctx.timerService().registerEventTimeTimer( value.windowEnd + 1 )
}
//定时器触发时,对所有数据进行排序,并输出结果
override def onTimer(timestamp: Long,
ctx: KeyedProcessFunction[Long, ItemViewCount, String]#OnTimerContext,
out: Collector[String]): Unit = {
//将所有state中的数据取出,放到listbuffer中
val allItems: ListBuffer[ItemViewCount] = new ListBuffer[ItemViewCount]
import scala.collection.JavaConversions._
for (item <- itemState.get()) {
allItems += item
}
//按照count大小进行排序,并取前n个
val sortedItems: ListBuffer[ItemViewCount] = allItems
.sortBy( _.cout )( Ordering.Long.reverse ) //Ordering.Long.reverse表示降序排序
.take( topSize )
//清空状态
itemState.clear()
//将排名结果格式化输出
val result: StringBuilder = new StringBuilder()
result.append( "时间:" ).append(transactionTime(timestamp)).append( "\n" )
//输出每一个商品的信息
for (elem <- sortedItems.indices) {
val currentItem = sortedItems( elem )
result.append( "排名" ).append( elem + 1 ).append( ":" )
.append( " 商品ID=" ).append( currentItem.itemId )
.append( " 浏览量=" ).append( currentItem.cout )
.append( "\n" )
}
result.append( "===============================" )
//控制输出频率
Thread.sleep( 1000 )
out.collect( result.toString() )
}
def transactionTime(ts: Long): String ={
val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val result = format.format(new Date(ts))
result
}
}
四.运行效果
五.项目小结
1.需要熟练掌握flink算子和函数式接口
2.清晰的理解数据的传输过程
3.熟练的使用窗口函数