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() } }