将一个Table转换成DataStream时,有仅插入流(Insert-Only-Streams)、更新日志流(Changelog Streams)两种方式,具体使用哪种方式取决于表中是否存在更新操作
关系型表/SQL | 流处理 | |
---|---|---|
处理的数据对象 | 字段元组的有界集合 | 字段元组的无限序列 |
查询(Query) | 可以访问到完整的数据输入 | 无法访问到所有数据,必须持续等待流式输入 |
查询终止条件 | 生成固定大小的结果集后终止 | 永不停止,根据持续收到的数据不断更新查询结果 |
当流中有新数据到来,初始的表中会插入一行,基于这个表定义的SQL查询,就应该在之前的基础上更新结果,这样得到的表就会不断地动态变化,因此被称为动态表(Dynamic Tables)。动态表是Flink在Table API和SQL中的核心概念,它为流数据处理提供了表和SQL支持。
动态表的概念,我们在传统的关系型数据库中已经有所接触了,数据库中的表,是一系列INSERT、UPDATE和DELETE语句执行的结果,在关系型数据库中,一般把它称为更新日志流(changelog stream)。当我们保存了表在某一时刻的快照,那么接下来只要读取更新日志流,就可以得到表之后的变化过程和最终结果。在很多关系型数据库(如Oracle、DB2)中都有物化视图(Materialized Views)的概念,可以用来缓存SQL查询的结果,它的更新就是不停地处理更新日志流的过程
数据再不断变化,基于它定义的SQL查询也就永远不会停止,一致随着新数据的到来而继续执行,这样的查询被称作持续查询。
对动态表定义的查询操作,都是持续查询;而持续查询的结果也会是一个动态表
持续查询的步骤:
只要API将流和动态表的转换封装起来,就可以直接在数据流上执行SQL查询,用处理表的方式来做流处理
为了能够使用SQL做流处理,必须先把流转换成动态表。如果把流看作一张表,那么流中每个数据的到来都看作是对表的一次插入操作,在表的末尾添加一行数据。因为流式连续不断的,而且之前的输出结果无法改变、只能在后面追加,所以我们其实是通过一个只有插入操作(insert-only)和更新日志流,来构建一个表
在代码中定义一个SQL查询:
Table urlCountTable = tableEnv.sqlQuery("SELECT user, COUNT(url) as cnt FROM EventTable GROUP BY user");
原始的动态表注册为EventTable,经过查询转换后得到urlCountTable,动态表中有两个字段
[
user: VARCHAR, // 用户名
cnt: BIGINT // 用户访问 url 的次数
]
当原始动态表不停插入新的数据时,查询得到的urlCountTable会持续地进行更改,由于count数量可能会叠加增长,因此这里的更改操作可以是简单的插入,也可以是对之前数据的更新。
上述查询结果表的插入与更新的具体步骤如下:
当我们执行一个简单的条件查询,结果表中就会像原始表EventTable一样,只有插入操作了
Table aliceVisitTable = tableEnv.sqlQuery("SELECT url, user FROM EventTable WHERE user = 'Cary'");
这样的持续查询,就被称为追加查询(Append Query),它定义的结果表的更新日志流只有INSERT操作。追加查询得到的结果表
更新查询的判断标准:结果表中的数据是否会有UPDATE操作
考虑开一个滚动窗口,统计每一小时内所有用户的点击次数,并在结果表中增加一个endT字段,表示当前统计窗口的结束时间
[
user: VARCHAR, // 用户名
endT: TIMESTAMP, // 窗口结束时间
cnt: BIGINT // 用户访问 url 的次数
]
与之前分组聚合一样,当原始动态表不停插入新的数据时,查询得到的结果result会持续地进行更改。时间戳在12:00:00 到 12:59:59 之间的有四条数据,其中 Alice 三次点击、Bob 一次点击;所以当水位线达到 13:00:00 时窗口关闭,输出到结果表中的就是新增两条数据[Alice, 13:00:00, 3]和[Bob, 13:00:00, 1]。同理,当下一小时的窗口关闭时, 也会将统计结果追加到 result 表后面,而不会更新之前的数据。窗口的统计结果时一次性写入结果表,结果表的更新日志流中只包含插入INSERT操作,而没有更新UPDATE操作
在实际应用中,有些持续查询会因为计算代价太高而受到限制。代价太高指的是:需要维护的状态持续增长、更新数据的计算太复杂
动态表可以通过插入、更新、删除操作,进行持续地更改。将动态表转换为流或其将其写入外部系统时,就需要对这些更改操作进行编码,通过发送编码消息的方式告诉外部系统要执行的操作。在Flink中,Table API和SQL支持三种编码方式
仅追加流(Append-only):仅通过插入更改来修改的动态表,可以直接转换为仅追加流。流中发出的数据对应动态表中新增的每一行
撤回流(Retract):包含两类消息的流,添加消息和撤回消息
(具体的编码规则:INSERT插入操作编码为add消息;DELETE删除操作编码为retract消息;而UPDATE更新操作为更改行的retract消息,再add消息)
这里我们用+代表add,-代表retract。
1、当 Alice 的第一个点击事件到来时,结果表新增一条数据[Alice, 1]
2、而当 Alice 的第二个点击事件到来时,结果表会将[Alice, 1]更新为[Alice, 2],对应的编码就是删除[Alice, 1],插入[Alice, 2]
更新插入流(Upsert):包含两类消息,更新插入消息和删除消息
(upsert是updata、insert的合成词,所以对于更新插入流来说,INSERT插入操作和UPDATE更新操作,统一编码为upsert消息;而DELETE删除操作则编码为delete消息)
动态表中必须有唯一的键,通过这个key进行查询,如果存在对应的数据就做更新,如果不存在就直接插入,这是一个动态表可以转换为更新插入流的必要条件。收到这条流数据的外部系统,也需要指定唯一的键,这样才能正确处理消息
更新插入流和撤回流的区别:更新操作由于有key的存在,只需要用单条消息编码就可以,效率更高
注意:在代码里将动态表转换为DataStream时,只支持仅追加和撤回流,调用toChangelogStream()得到的是撤回流