Flink实战:用户行为分析之热门商品TopN统计

环境

ubuntu14、flink1.7.2、scala2.11、kafka2.3.0、jdk1.8、idea2019

步骤

  • 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口
  • 过滤出点击行为(pv)数目
  • 按一小时的窗口大小,每 5 分钟统计一次,做滑动窗口聚合(Sliding Window)
  • 按每个窗口聚合,输出每个窗口中点击量前 N 名的商品

实现

创建maven项目,命名UserBehaviorAnalysis,其pom内容如下:



    4.0.0

    com.ustc
    UserBehaviorAnalysis
    pom
    1.0-SNAPSHOT

    
    
        1.7.2
        2.11
        2.3.0
    

    
    
        HotItemsAnalysis
    

    
        
            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}
        
    

    
    
        
            
            
                net.alchim31.maven
                scala-maven-plugin
                3.4.6
                
                    
                        
                            testCompile
                        
                    
                
            

            
                org.apache.maven.plugins
                maven-assembly-plugin
                3.0.0
                
                    
                        jar-with-dependencies
                    
                
                
                    
                        make-assembly
                        package
                        
                            single
                        
                    
                
            
        
    


在该项目中创建子模块(右击->module),取名 HotItemsAnalysis,其pom内容(保持默认内容即可):



    
        UserBehaviorAnalysis
        com.ustc
        1.0-SNAPSHOT
    
    4.0.0

    HotItemsAnalysis


在 HotItemsAnalysis子模块中,将Java包名改为scala,并添加Object命名HotItems(右击scala文件夹->new->Object),HotItems.scala内容如下:

import java.sql.Timestamp
import java.util.Properties

import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
import org.apache.flink.api.java.tuple.{Tuple, Tuple1}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.util.Collector

import scala.collection.mutable.ListBuffer



//输入样例类
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 = {
    //1. 创建执行环境
    val env=StreamExecutionEnvironment.getExecutionEnvironment
    //指定Time类型为EventTime
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //并发度(slot)
    env.setParallelism(1)

    //2. 读取数据
      //2.1 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 dataStream=env.addSource(new FlinkKafkaConsumer[String]("hotitems",new SimpleStringSchema(),properties))
      //2.2 文件数据源
    //val dataStream=env.readTextFile("")
      .map(line=>{
        val linearray=line.split(",")
        UserBehavior(linearray(0).trim.toLong,linearray(1).trim.toLong,linearray(2).trim.toInt,linearray(3).trim,linearray(4).trim.toLong)
      })
      //抽取时间戳和生成watermark(由于数据源的每条数据的时间戳是单调递增的,所有将每条数据的业务时间当做watermark,若是乱序数据,需要使用BoundedOutOfOrdernessTimestampExtractor)
      .assignAscendingTimestamps(_.timestamp*1000L) //时间戳转为ms单位

    // 3. 转换处理数据
    val processedStream=dataStream
      .filter(_.behavior=="pv") //过滤点击事件
      .keyBy(_.itemId) //对商品分组,分成多个并行流
      .timeWindow( Time.minutes(60),Time.minutes(5) )   //5分钟一个窗口(若后面没有延迟,则会5分钟输出一个窗口)
      .aggregate(new CountAgg(),new WindowResult())
        .keyBy(_.windowEnd) //按窗口结束时间分组
        .process(new TopNHotItems(3))

    //4. sink,控制台输出
    processedStream.print()

    env.execute("Hot Items Job")
  }

}

//自定义预聚合函数(累加器),适合简单聚合
class CountAgg() extends AggregateFunction[UserBehavior,Long,Long]{
  override def add(in: UserBehavior, acc: Long): Long = acc+1

  //累加器初始值为0
  override def createAccumulator(): Long = 0L

  override def getResult(acc: Long): Long = acc

  override def merge(acc: Long, acc1: Long): Long = acc+acc1
}



//自定义预聚合函数计算平均数
/**
 * 输入类型:UserBehavior
 * 中间聚合状态类型:(Long,Int) ->Long:时间戳和,Int:个数
 * 输出类型:Double
 */
class AverageAgg() extends AggregateFunction[UserBehavior,(Long,Int),Double]{
  //当前输入值:in  ,中间状态:acc
  override def add(in: UserBehavior, acc: (Long, Int)): (Long, Int) = (acc._1+in.timestamp,acc._2+1)

  override def createAccumulator(): (Long, Int) = (0L,0)

  override def getResult(acc: (Long, Int)): Double = acc._1/acc._2
  //合并两个累加器,仍是中间状态的改变
  override def merge(acc: (Long, Int), acc1: (Long, Int)): (Long, Int) = (acc._1+acc1._1,acc._2+acc1._2)
}



//自定义窗口函数,输出ItemViewCount(商品ID、)
/**
 * 输入类型:预聚合函数的输出 Long
 * 输出类型: ItemViewCount
 * key类型: Tuple类型
 * Window类型:TimeWindow
 */
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()))
  }
}



//自定义的处理函数,
/**
 * key类型:Long
 * 输入类型:ItemViewCount
 * 输出类型:String
 * @param topSize
 */
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]("item-state",classOf[ItemViewCount]))
  }

  override def processElement(value: ItemViewCount, context: KeyedProcessFunction[Long, ItemViewCount, String]#Context, collector: Collector[String]): Unit = {
    //将每条到来的数据存入状态列表
    itemState.add(value)
    //注册一个定时器
    context.timerService().registerEventTimeTimer(value.windowEnd+1)  //延迟1s触发
  }

  //定时器触发时,对所有数据排序,并输出结果
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, ItemViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
    //将所有state中数据取出放入List Buffer中
    val allItems:ListBuffer[ItemViewCount]=new ListBuffer()

    //遍历引入转换包
    import scala.collection.JavaConversions._
    for(item<-itemState.get()){
      allItems+=item
    }

    //按照点击量(count)降序,并取前N
    val sortedItems=allItems.sortBy(_.count)(Ordering.Long.reverse).take(topSize)

    //清空状态
    itemState.clear()

    //将排名记过格式化输出
    val result:StringBuilder=new StringBuilder()
    result.append("时间: ").append(new Timestamp(timestamp-1)) //窗口关闭时间timestamp为定时器触发时间
      .append("\n")

    //输出每个商品信息
    for (i<-0 to sortedItems.length-1){
      val currentItem=sortedItems(i)
      result.append("No").append(i+1).append(":")
        .append(" 商品ID=").append(currentItem.itemId)
        .append( "浏览量=").append(currentItem.count)
        .append("\n")
    }
    result.append("=============================")
    //控制输出频率
    Thread.sleep(1000)
    out.collect(result.toString())


  }
}

测试

kafka数据源:滑动窗口->5分钟一个窗口结果输出(若后面没有延迟,则会每5分钟输出一次往前一个小时内的统计结果)

  • 启动kafka
cd /usr/local/kafka/  
./bin/zookeeper-server-start.sh config/zookeeper.properties
./bin/kafka-server-start.sh config/server.properties
  • 启动kafka生产者终端
./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic hotitems
  • 右击启动HotItems.scala程序 

测试数据:

543462,1715,1464116,pv,1511658000          ->2017/11/26 9:00:00
662867,2244074,1575622,pv,1511658060       ->2017/11/26 9:01:00
561558,3611281,965809,pv,1511658120        ->2017/11/26 9:02:00
625915,3611281,570735,pv,1511658300        ->2017/11/26 9:05:00
578814,1715,982926,pv,1511658301           ->2017/11/26 9:05:01
578814,1715,982926,pv,1511658330           ->2017/11/26 9:05:30
87335,1256540,1451783,pv,1511658540        ->2017/11/26 9:09:00
429984,2244074,2355072,pv,1511658600       ->2017/11/26 9:10:00
937166,1715,2355072,pv,1511661600          ->2017/11/26 10:00:00
937170,1715,2355072,pv,1511665200  	      ->2017/11/26 11:00:00
  • 控制台输出

其中时间为时间窗口结束时间,由于时间窗口长度=60mins,滑动距离=5mins,而数据的EventTime作为Watermark,会每隔5mins统计一次,统计事件由到来的数据的EventTime值进行触发。
第一条数据EventTime的时间戳 1511658000 ->2017/11/26 9:00:00 ,若第一条数据的EventTime的时间戳  1511658060 ->2017/11/26 9:01:00,第一个窗口的结束时间仍然为 9:05:00(不包括该时刻的数据),当时间戳为9:05:00的数据到来时,引发窗口[8:05:00,9:05:00)关闭,并对EventTime在该时间段窗口内的数据进行计数统计。


时间: 2017-11-26 09:05:00.0      [08:05:00.0,09:05:00.0)  #时间戳为2017/11/26 9:05:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:05:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=1       
No2: 商品ID=3611281浏览量=1
No3: 商品ID=2244074浏览量=1
=============================
时间: 2017-11-26 09:10:00.0      [08:10:00.0,09:10:00.0)  #时间戳为2017/11/26 9:10:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:10:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=1
=============================
时间: 2017-11-26 09:15:00.0      [08:15:00.0,09:15:00.0)  #时间戳为2017/11/26 9:15:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:15:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 09:20:00.0      [08:20:00.0,09:20:00.0)  #时间戳为2017/11/26 9:20:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:20:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 09:25:00.0      [08:25:00.0,09:25:00.0)  #时间戳为2017/11/26 9:25:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:25:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 09:30:00.0      [08:30:00.0,09:30:00.0)  #时间戳为2017/11/26 9:30:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:30:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=2244074浏览量=2
No3: 商品ID=3611281浏览量=2
=============================
时间: 2017-11-26 09:35:00.0      [08:35:00.0,09:35:00.0)  #时间戳为2017/11/26 9:35:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:35:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=2244074浏览量=2
No3: 商品ID=3611281浏览量=2
=============================
时间: 2017-11-26 09:40:00.0      [08:40:00.0,09:40:00.0)  #时间戳为2017/11/26 9:40:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:40:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=2244074浏览量=2
No3: 商品ID=3611281浏览量=2
=============================
时间: 2017-11-26 09:45:00.0      [08:45:00.0,09:45:00.0)  #时间戳为2017/11/26 9:45:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:45:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=2244074浏览量=2
No3: 商品ID=3611281浏览量=2
=============================
时间: 2017-11-26 09:50:00.0      [08:50:00.0,09:50:00.0)  #时间戳为2017/11/26 9:50:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:50:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 09:55:00.0      [08:55:00.0,09:55:00.0)  #时间戳为2017/11/26 9:55:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 9:55:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 10:00:00.0      [09:00:00.0,10:00:00.0)  #时间戳为2017/11/26 10:00:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 10:00:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=3611281浏览量=2
No3: 商品ID=2244074浏览量=2
=============================
时间: 2017-11-26 10:05:00.0      [09:05:00.0,10:05:00.0)  #时间戳为2017/11/26 10:05:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 10:05:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=3
No2: 商品ID=2244074浏览量=1
No3: 商品ID=1256540浏览量=1
=============================
时间: 2017-11-26 10:10:00.0      [09:10:00.0,10:10:00.0)  #时间戳为2017/11/26 10:00:00或更晚的数据到来引发该窗口关闭并统计,但由于代码中定时器延迟1s触发,当时间戳为2017/11/26 10:00:01的数据到来时才会在控制台输出统计结果
No1: 商品ID=1715浏览量=1
No2: 商品ID=2244074浏览量=1
=============================
时间: 2017-11-26 10:15:00.0      [09:15:00.0,10:15:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:20:00.0      [09:20:00.0,10:20:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:25:00.0      [09:25:00.0,10:25:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:30:00.0      [09:30:00.0,10:30:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:35:00.0      [09:35:00.0,10:35:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:40:00.0      [09:40:00.0,10:40:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:45:00.0      [09:45:00.0,10:45:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:50:00.0      [09:50:00.0,10:50:00.0)
No1: 商品ID=1715浏览量=1
=============================
时间: 2017-11-26 10:55:00.0      [09:55:00.0,10:55:00.0)
No1: 商品ID=1715浏览量=1
=============================

截图部分 

Flink实战:用户行为分析之热门商品TopN统计_第1张图片


 

 

 BUG1:

Caused by: org.apache.kafka.common.errors.InvalidTopicException: Topic 'hotitems' is invalid 


 解决:

将kafka、flink集群、应用应用程序重新启动,但不知道到原因。

 

你可能感兴趣的:(Flink)