Flink学习(十六) ProcessFunctionAPI(底层API)

我们之前学习的转换算子是无法访问时间的时间戳信息和水位线信息的。而这些在一些应用场景下,极为重要,例如MapFunction这样的map转换算子就无法访问时间戳或者当前事件的事件时间。

基于此,DataStreamAPI提供了一系列的Low-Level的转换算子,可以访问时间戳、watermark以及注册定时事件,还可以输出特定的一些事件,例如超时事件等等。ProcessFuntion用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子是无法实现的),例如,Flink SQL就是使用ProcessFunction实现的。

Flink提供了8个Process Function:

1、ProcessFunction

2、KeyedProcessFuntion

3、CoProcessFunction

4、ProcessJoinFunction

5、BrodcastProcessFunction

6、KeyedBroadcastProcessFuntion

7、ProcessWindowFunction

8、ProcessAllWindowFunction

 

KeyedProcessFuntion

重点介绍并使用KeyedProcessFuntion

KeyedProcessFuntion用来操作KeyedStream、KeyedProessFunction会处理流的每一个元素,输出为0个,1个或者多个元素,所有的ProcessFunction都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法,而KyedProcessFunction[KEY,IN,OUT]还额外提供了两个方法:

processElement(v:IN,ctx:Context,out:Collector[OUT]),流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出,Context可以方法元素的时间戳,元素的key,以及TimeServer时间事务,Context还可以将结果输出到别的流(side outpurts)。

onTimer(timestamp:Long,ctx:OnTimerContext,out:collector[OUT]) 是一个回调函数,当之前注册的定时器触发时调用。参数timestamp为定时器设定的触发的时间戳。Collector为输出结果的集合。OnTimerContext和processElement的Conetxt参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)

 

TimeServer和定时器(Timers)

Context和OnTImerContext所特有的TimeServer对象拥有以下方法:

 

1、返回当前处理事件

2、返回当前watermark的时间戳

3、删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行

4、删除之前注册处理时间定时器,如果没有这个时间戳的定时器,则不执行

5、会注册当前key的enven time定时器,当水位线大于等于定时器注册的时间时,触发定时器执行回调函数

6、会注册当前key的peocessing timer定时器,当processingtime到达定时时间时,触发timer,也就是onTimer方法

当定时器timer触发时,会执行回调函数onTimer() ,注意定时器timer只能在KyedStream上使用

案例测试:

package com.wyh.processFunctionApi

import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.util.Collector


//温度传感器读数样例类
case class SensorReading(id: String, timestamp: Long, temperature: Double)

object ProcessFunctionTest {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    val stream = env.socketTextStream("localhost", 7777)

    //Transform操作
    val dataStream: DataStream[SensorReading] = stream.map(data => {
      val dataArray = data.split(",")
      SensorReading(dataArray(0).trim, dataArray(1).trim.toLong, dataArray(2).trim.toDouble)
    })
      //===到来的数据是升序的,准时发车,用assignAscendingTimestamps
      //指定哪个字段是时间戳 需要的是毫秒 * 1000
      //      .assignAscendingTimestamps(_.timestamp * 1000)
      //===处理乱序数据
      //      .assignTimestampsAndWatermarks(new MyAssignerPeriodic())
      //==底层也是周期性生成的一个方法 处理乱序数据 延迟1秒种生成水位 同时分配水位和时间戳 括号里传的是等待延迟的时间
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(1)) {
      override def extractTimestamp(t: SensorReading): Long = {
        t.timestamp * 1000
      }
    })

    val processedStream = dataStream.keyBy(_.id)
      .process(new TempIncreAlert())

    processedStream.print("process data")


    dataStream.print("input data")


    env.execute("window Test")


  }

}

class TempIncreAlert() extends KeyedProcessFunction[String, SensorReading, String] {
  //定义一个状态,用来保存上一个数据的温度值
  //用懒加载的方式,一开始定义的时候我们还不执行,等到调用的时候去执行
  //所有的状态都这么定义 当成一个变量直接用
  lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState(new ValueStateDescriptor[Double]("lastTemp", classOf[Double]))
  //定义一个状态用来保存定时器的时间戳
  lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("currentTimer", classOf[Long]))

  //判断温度连续上升
  //跟上一次数据进行比较 如果比较一直大 10秒种内进行报警
  //注册一个定时器 把上一次的数据保存成当前的状态
  override def processElement(value: SensorReading, ctx: KeyedProcessFunction[String, SensorReading, String]#Context, out: Collector[String]): Unit = {
    //每来一条数据的时候,从状态中取出上一次的温度值
    val preTemp = lastTemp.value()
    var curTimerTs = currentTimer.value()

    //更新温度值
    lastTemp.update(value.temperature)

    //加个if判断最开始的温度是否为0来判断是否是第一条数据 温度上升且没有设置过定时器,则注册定时器
    if(preTemp==0.0){
        println("这是第一条数据进来")
    }else if ((value.temperature>preTemp) && (curTimerTs==0L)) {
      
      val timerTs = ctx.timerService().currentProcessingTime() + 10000L

      //传入当前时间加1  是时间戳
      ctx.timerService().registerProcessingTimeTimer(timerTs)
      currentTimer.update(timerTs)
    } else if (value.temperature <= preTemp) {
      //如果温度下降 或者是第一条数据 删除定时器
      ctx.timerService().deleteProcessingTimeTimer(curTimerTs)
      //删除定时器之后将状态清空
      currentTimer.clear()
    }
  }

  //在回调函数中执行定时器到的逻辑
  //当前的时间 ctx上下文 out输出信息
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext, out: Collector[String]): Unit = {
    //直接输出报警信息
    out.collect(ctx.getCurrentKey + "温度连续上升")
    //考虑真实情况,将状态都清空
    currentTimer.clear()
  }
}

在Linux命令行中使用命令 nc -lk 7777开启一个服务

 

 先输入数据:

sensor_1,1547718199,39

等待10秒发现没有反应,继续输入数据,等待10秒发现,报警信息!

posted @ 2020-05-24 20:15  Xiaohu_BigData  阅读(585)  评论(0编辑  收藏  举报