Flink-基于Table设置事件属性

基于时间的操作(比如时间窗口),需要定义相关的时间语义和时间数据来源的信息。在Table API 和 SQL 中,会给表单独提供一个逻辑上的时间字段,专门用来在表处理程序中指示时间。
所以所谓的时间属性(time attributes),其实就是每个表模式结构(schema)的一部分。它可以在创建表的 DDL 里直接定义为一个字段,也可以在流转换成表时定义。一旦定义了时间属性,它就可以作为一个普通字段引用,并且可以在基于时间的操作中使用。时间属性的数据类型为 TIMESTAMP,它的行为类似于常规时间戳,可以直接访问并且进行计算。
按照时间语义的不同,我们可以把时间属性的定义分成事件时间(event time)和处理时间(processing time)两种情况。
 

1.事件时间

我们在实际应用中,最常用的就是事件时间。在事件时间语义下,允许表处理程序根据每个数据中包含的时间戳(也就是事件发生的时间)来生成结果。
事件时间语义最大的用途就是处理乱序事件或者延迟事件的场景。我们通过设置水位线(watermark)来表示事件时间的进展,而水位线可以根据数据的最大时间戳设置一个延迟时间。这样即使在出现乱序的情况下,对数据的处理也可以获得正确的结果。
为了处理无序事件,并区分流中的迟到事件。Flink 需要从事件数据中提取时间戳,并生成水位线,用来推进事件时间的进展。
事件时间属性可以在创建表 DDL 中定义,也可以在数据流和表的转换中定义。

1.1在创建表的DDL中定义

在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个字段,通过 WATERMARK语句来定义事件时间属性。WATERMARK 语句主要用来定义水位线(watermark)的生成表达式,这个表达式会将带有事件时间戳的字段标记为事件时间属性,并在它基础上给出水位线的延迟时间。具体定义方式如下:
CREATE TABLE EventTable(
user STRING,
url STRING,
ts TIMESTAMP(3),
WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
) WITH (
...
);
这里我们把 ts 字段定义为事件时间属性,而且基于 ts 设置了 5 秒的水位线延迟。这里的“5 秒”是以“时间间隔”的形式定义的,格式是 INTERVAL <数值> <时间单位>:INTERVAL '5' SECOND
这里的数值必须用单引号引起来,而单位用 SECOND 和 SECONDS 是等效的。
Flink 中支持的事件时间属性数据类型必须为 TIMESTAMP 或者 TIMESTAMP_LTZ。这里TIMESTAMP_LTZ 是指带有本地时区信息的时间戳(TIMESTAMP WITH LOCAL TIMEZONE);一般情况下如果数据中的时间戳是“年-月-日-时-分-秒”的形式,那就是不带时区信息的,可以将事件时间属性定义为 TIMESTAMP 类型。
而如果原始的时间戳就是一个长整型的毫秒数,这时就需要另外定义一个字段来表示事件时间属性,类型定义为 TIMESTAMP_LTZ 会更方便:
CREATE TABLE events (
user STRING,
url STRING,
ts BIGINT,
ts_ltz AS TO_TIMESTAMP_LTZ(ts, 3),
WATERMARK FOR ts_ltz AS time_ltz - INTERVAL '5' SECOND
) WITH (
...
);
这里我们另外定义了一个字段 ts_ltz,是把长整型的 ts 转换为 TIMESTAMP_LTZ 得到的;进而使用 WATERMARK 语句将它设为事件时间属性,并设置 5 秒的水位线延迟。
 

1.2在数据流转换为表时定义

事件时间属性也可以在将 DataStream 转换为表的时候来定义。我们调用 fromDataStream()方法创建表时,可以追加参数来定义表中的字段结构;这时可以给某个字段加上 rowtime() 后缀,就表示将当前字段指定为事件时间属性。这个字段可以是数据中本不存在、额外追加上去的“逻辑字段”,就像之前 DDL 中定义的第二种情况;也可以是本身固有的字段,那么这个字段就会被事件时间属性所覆盖,类型也会被转换为 TIMESTAMP。不论那种方式,时间属性字段中保存的都是事件的时间戳(TIMESTAMP 类型)。
需要注意的是,这种方式只负责指定时间属性,而时间戳的提取和水位线的生成应该之前就在 DataStream 上定义好了。由于 DataStream 中没有时区概念,因此 Flink 会将事件时间属性解析成不带时区的 TIMESTAMP 类型,所有的时间值都被当作 UTC 标准时间。
在代码中的定义方式如下:// 方法一:
// 流中数据类型为二元组 Tuple2,包含两个字段;需要自定义提取时间戳并生成水位线
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 声明一个额外的逻辑字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())
// 方法二:
// 流中数据类型为三元组 Tuple3,最后一个字段就是事件时间戳
val stream = inputStream.assignTimestampsAndWatermarks(...)
// 不再声明额外字段,直接用最后一个字段作为事件时间属性
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").rowtime())
 
 

2.处理时间

相比之下处理时间就比较简单了,它就是我们的系统时间,使用时不需要提取时间戳(timestamp)和生成水位线(watermark)。因此在定义处理时间属性时,必须要额外声明一个字段,专门用来保存当前的处理时间。
类似地,处理时间属性的定义也有两种方式:创建表 DDL 中定义,或者在数据流转换成表时定义。
 

2.1在创建表的 DDL 中定义

在创建表的 DDL(CREATE TABLE 语句)中,可以增加一个额外的字段,通过调用系统内置的 PROCTIME()函数来指定当前的处理时间属性,返回的类型是 TIMESTAMP_LTZ。
CREATE TABLE EventTable(
user STRING,
url STRING,
ts AS PROCTIME()
) WITH (
...
);
这里的时间属性,其实是以“计算列”(computed column)的形式定义出来的。所谓的计算列是 Flink SQL 中引入的特殊概念,可以用一个 AS 语句来在表中产生数据中不存在的列,并且可以利用原有的列、各种运算符及内置函数。在前面事件时间属性的定义中,将 ts 字段转换成 TIMESTAMP_LTZ 类型的 ts_ltz,也是计算列的定义方式。
 

2.2 在数据流转换为表时定义

处 理 时 间 属 性 同 样 可 以 在 将 DataStream 转 换 为 表 的 时 候 来 定 义 。 我 们 调 用fromDataStream()方法创建表时,可以用 proctime()后缀来指定处理时间属性字段。由于处理时间是系统时间,原始数据中并没有这个字段,所以处理时间属性一定不能定义在一个已有字段上,只能定义在表结构所有字段的最后,作为额外的逻辑字段出现。
代码中定义处理时间属性的方法如下:
val stream = ...
// 声明一个额外的字段作为处理时间属性字段
val table = tEnv.fromDataStream(stream, $("user"), $("url"), $("ts").proctime())
 
相关代码:
package com.zhen.flink.table

import java.time.Duration

import org.apache.flink.api.common.eventtime.{SerializableTimestampAssigner, WatermarkStrategy}
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.Expressions.$
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

/**
  * @Author FengZhen
  * @Date 10/11/22 1:57 PM
  * @Description TODO
  */
object TimeAndWindowTest {


  def main(args: Array[String]): Unit = {

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    // 创建表环境
    val tableEnv = StreamTableEnvironment.create(env)

    // 1.在创建表的DDL中指定时间属性字段
    tableEnv.executeSql("CREATE TABLE eventTable (" +
      " uid STRING," +
      " url STRING," +
      " ts BIGINT," +
      " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts/1000))," +
      " proc_time AS PROCTIME(), " + //处理时间
      " WATERMARK FOR et AS et - INTERVAL '3' SECOND " +
      ") WITH (" +
      " 'connector' = 'filesystem'," +
      " 'path' = '/Users/FengZhen/Desktop/accumulate/0_project/flink_learn/src/main/resources/data/input/clicks.txt', " +
      " 'format' = 'csv' " +
      ")")

    // 2.在将流转换成表的时候指定时间属性字段
    // 读取数据源,并分配时间戳、生成水位线
    val eventStream = env
      .fromElements(
        Event("Alice", "./home", 1000L),
        Event("Bob", "./cart", 1000L),
        Event("Alice", "./prod?id=1", 25 * 60 * 1000L),
        Event("Alice", "./prod?id=4", 55 * 60 * 1000L),
        Event("Bob", "./prod?id=5", 3600 * 1000L + 60 * 1000L),
        Event("Cary", "./home", 3600 * 1000L + 30 * 60 * 1000L),
        Event("Cary", "./prod?id=7", 3600 * 1000L + 59 * 60 * 1000L)
      )
      //如果数据为标准的升序数据,直接assignAscendingTimestamps
      //.assignAscendingTimestamps(_.timestamp)
      //如果数据为乱序数据,则assignTimestampsAndWatermarks
      .assignTimestampsAndWatermarks(
        WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(2))
        .withTimestampAssigner(new SerializableTimestampAssigner[Event] {
          override def extractTimestamp(element: Event, recordTimestamp: Long): Long = element.timestamp
        })
      )

    // 将数据流转换成表,并指定时间属性
//    val eventTable = tableEnv.fromDataStream(
//      eventStream,
//      $("url"),
//      $("user").as("uid"),
//      $("timestamp").as("ts"),
//      $("et").rowtime() //新增一个字段,表示为当前的时间属性字段
//    )

    //可以直接将原有字段指定位rowtime
    val eventTable = tableEnv.fromDataStream(
      eventStream,
      $("url"),
      $("user").as("uid"),
      $("timestamp").rowtime().as("ts"),
      $("proc_time").proctime() // 处理时间
    )

    /**
      * 1.
      * (
      * `uid` STRING,
      * `url` STRING,
      * `ts` BIGINT,
      * `et` TIMESTAMP(3) *ROWTIME* AS TO_TIMESTAMP(FROM_UNIXTIME(`ts` / 1000)),
      * `proc_time` TIMESTAMP_LTZ(3) NOT NULL *PROCTIME* AS PROCTIME(),
      * WATERMARK FOR `et`: TIMESTAMP(3) AS `et` - INTERVAL '3' SECOND
      * )
      */
    tableEnv.from("eventTable").printSchema()

    /**
      * 2.
      * (
      * `url` STRING,
      * `uid` STRING,
      * `ts` TIMESTAMP(3) *ROWTIME*,
      * `proc_time` TIMESTAMP_LTZ(3) *PROCTIME*
      * )
      */
    eventTable.printSchema()

  }

}

 

posted on 2022-10-11 14:30  嘣嘣嚓  阅读(306)  评论(0编辑  收藏  举报

导航