Flink系列Table API和SQL之:时间属性
一、时间属性和窗口
- 基于时间的操作(比如时间窗口),需要定义相关的时间语义和时间数据来源的信息。在Table API和SQL中,会给表单独提供一个逻辑上的时间字段,专门用来在表处理程序中指示时间。
- 所谓的时间属性(time attributes),就是每个表模式结构(schema)的一部分。可以在创建表的DDL里直接定义一个字段,也可以在DataStream转换成表时定义。一旦定义了时间属性,它就可以作为一个普通字段引用,并且可以在基于时间的操作中使用。
- 时间属性的数据类型为TIMESTAMP,它的行为类似于常规时间戳,可以直接访问并且进行计算。
- 按照时间语义的不同,可以把时间属性的定义分成事件时间(event time)和处理时间(processing time)两种情况。
二、事件时间
- 在实际应用中,最常用的就是事件时间。在事件时间语义下,允许表处理程序根据每个数据中包含的时间戳(也就是事件发生的时间)来生成结果。
- 事件时间语义最大的用途就是处理乱序事件或者延迟事件的场景。通过设置水位线(watermark)来表示事件时间的进展,而水位线可以根据数据的最大事件戳设置一个延迟时间。这样即使在出现乱序的情况下,对数据的处理也可以获得正确的结果。
- 为了处理无序时间,并区分流中的迟到事件。Flink需要从事件数据中提取时间戳,并生成水位线,用来推进事件时间的进展。
- 事件时间属性可以在创建表DDL中定义,也可以在数据流和表的转换中定义。
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秒的水位线延迟
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 1. 在创建表的DDL中直接定义时间属性
String createDDL = "CREATE TABLE clickTable (" +
" user_name STRING, " +
" url STRING, " +
" ts BIGINT, " +
" et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " +
" WATERMARK FOR et AS et - INTERVAL '1' SECOND " +
") WITH (" +
" 'connector' = 'filesystem', " +
" 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " +
" 'format' = 'csv' " +
")";
tableEnv.executeSql(createDDL);
2.在数据流转换为表时定义
- 事件时间属性也可以在将DataStream转换为表的时候来定义,调用fromDataStream()方法创建表时,可以追加参数来定义表中的字段结构。这时可以给某个字段加上.rowtime()后缀,就表示将当前字段指定为事件时间属性。这个字段可以是数据中本不存在、额外追加上去的逻辑字段,也可以是本身固有的字段,那么这个字段就会被事件时间属性所覆盖,类型也会被转换为TIMESTAMP。不论哪种方式,时间属性字段中保存的都是事件的时间戳(TIMESTAMP类型)。
- 需要注意的是,这种方式只负责指定时间属性,而时间戳的提取和水位线的生成应该之前就在DataStream上定义好了,由于DataStream中没有时区概念,因此Flink会将事件时间属性解析成不带时区的TIMESTAMP类型,所有的时间值都被当作UTC标准时间。
在代码中定义方式如下:
方法一:
流中数据类型为二元组Tuple2,包含两个字断,需要自定义提取时间戳并生成水位线
DataStream<Tuple2<String,String>> stream = inputStream.assignTimestampsAndWatermarks(...);
声明一个额外的逻辑字段作为事件时间属性
Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").rowtime());
方法二:
流中数据类型为三元组Tuple3,最后一个字段就是事件时间戳
DataStream<Tuple3<String,String,Long>> stream = inputStream.assignTimestampsAndWatermarks(...);
不再声明额外字段,直接用最后一个字段作为事件时间属性
Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").rowtime());
// 2. 在流转换成Table时定义时间属性
SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long l) {
return event.timestamp;
}
}));
Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"),
$("et").rowtime());
clickTable.printSchema();
(
`user` STRING,
`url` STRING,
`ts` BIGINT,
`et` TIMESTAMP(3) *ROWTIME*
)
三、处理时间
相比之下,处理时间就比较简单了,就是我们的系统时间,使用时不需要提取时间戳(timestamp)和生成水位线(watermark)。因此在定义处理时间属性时,必须要额外声明一个字段,专门用来保存当前的处理时间。
类似地,处理时间属性的定义也有两种方式:创建表DDL中定义,或者在数据流转换成表时定义。
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.在数据流转换为表时定义
处理时间属性同样可以在将DataStream转换为表的时候来定义。调用fromDataStream()方法创建表时,可以用.proctime()后缀来指定处理时间属性字段。由于处理时间是系统时间,原始数据中并没有这个字段,所以处理时间属性一定不能定义在一个已有字段上,只能定义在表结构所有字段的最后,作为额外的逻辑字段出现。
代码中定义处理时间属性的方法如下:
DataStream<Tuple2<String,String>> stream = ...;
声明一个额外的字段作为处理时间属性字段
Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").proctime());