【Flink】Flink实时计算最热门Top N商品(Scala)

前进的路很艰难,但只要坚持下去,终会收获丰硕的果实!

一.项目需求

每隔5分钟输出最近一小时内点击量最多的前N个商品
具体步骤
• 抽取出业务时间戳,告诉Flink框架基于事件时间(Event Time)做窗口
• 过滤出点击行为数据
• 按一小时的窗口大小,每5分钟统计一次,做滑动窗口聚合(Sliding Window)
• 按每个窗口聚合,输出每个窗口中点击量前N名的商品

二.数据准备

在这里插入图片描述

字段名说明(从上往下与上述对应)
userIdLong 加密后的用户ID
itemIdLong 加密后的商品ID
categoryIdInt 加密后的商品所属类别ID
behaviorString 用户行为类型,包括(pv:点击, buy:购买 ,cart:加入购物车, fav:喜爱)
timestampLong 行为发生的时间戳,单位秒

数据下载连接
链接: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.熟练的使用窗口函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值