1.物化视图

传统的数据库SQL和实时SQL处理的差别还是很大的,这里简单列出一些区别:

flink run 动态参数 flink 动态表_flink run 动态参数

尽管存在这些差异,但使用关系查询和SQL处理流并非不可能。高级关系数据库系统提供称为物化视图的功能。物化视图定义为SQL查询,就像常规虚拟视图一样。与虚拟视图相比,物化视图缓存查询的结果,使得在访问视图时不需要执行查询。缓存的一个常见挑战是避免缓存提供过时的结果。物化视图在修改其定义查询的基表时会过时。Eager View Maintenance是一种在更新基表后立即更新实例化视图的技术。
如果我们考虑以下内容,Eager View Maintenance和流上的SQL查询之间的联系就变得很明显:

  • 数据库表是INSERT,UPDATE和DELETEDML语句流的结果,通常被称为更新日志流。
  • 物化视图定义为SQL查询。为了更新视图,查询需要持续处理视图源表的更改日志流。
  • 物化视图是流式SQL查询的结果。
    有了上面的基础,下面可以介绍一下动态表的概念了。

2.动态表的概念

动态表flink table api和SQL处理流数据的核心概念。与静态表相比,动态表随时间而变化,但可以像静态表一样查询动态表,只不过查询动态表需要产生连续查询。连续查询永远不会终止,会生成动态表作为结果表。查询不断更新其(动态)结果表以反映其(动态)输入表的更改。最终,动态表上的连续查询与定义物化视图的查询非常相似。

值得注意的是,连续查询的结果始终在语义上等同于在输入表的快照上执行批处理的到的相同查询结果。

下图显示了流,动态表和连续查询的关系:

flink run 动态参数 flink 动态表_数据库_02

  • A stream is converted into a dynamic table.
    Stream 被转化为 动态表 dynamic table
  • A continuous query is evaluated on the dynamic table yielding a new dynamic table.
    在产生的动态表上执行连续不断的查询,产生一个动态结果表。(每来一条新的数据都会触发这个sql query)
  • The resulting dynamic table is converted back into a stream
    结果动态表再次被转化为数据流。

3.stream转化成表

当然,想要用经典的sql去分析流数据,肯定要先将其转化为表。从概念上讲,流的每个新增记录都被解释为对结果表的Insert操作。最终,可以理解为是在从一个INSERT-only changelog流上构建一个表。

下图显示了click事件流(左侧)如何转换为表(右侧)。随着更多点击流记录的插入,生成的表不断增长。

flink run 动态参数 flink 动态表_flink run 动态参数_03

注意:stream转化的表内部并没有被物化。

3.连续查询

在动态表上执行连续查询(每当新纪录到来时,或者窗口触发时),并生成新的动态表作为结果表。与批处理查询不同,连续查询绝不会终止,而且会根据输入表的更新来更新它的结果表。在任何时间点,连续查询的结果在语义上等同于在输入表的快照上以批处理模式得到的查询的结果。

在下文中,我们将在用点击事件流定义的clicks表上展示两个示例查询。

第一个查询是一个简单的GROUP-BY COUNT聚合查询。主要是对clicks表按照user分组,然后统计url得到访问次数。下图展示了clicks表在数据增加期间查询是如何执行的。

flink run 动态参数 flink 动态表_flink_04

假设当查询启动的事以后,clicks表为空。当第一行数据插入clicks表的时候,查询开始计算产生结果表。当[Mary, ./home]插入的时候,查询会在结果表上产生一行[Mary, 1]。当[Bob, ./cart]插入clicks表之后,查询会再次更新结果表,增加一行[Bob, 1]。当第三行,[Mary, ./prod?id=1]插入clicks表后,查询会更新结果表的[Mary, 1]为[Mary, 2]。最后,第四行数据插入clicks后,查询会给结果表增加一行[Liz, 1].

第二个查询仅仅是在上个查询的基础上增加了一个1小时的滚动窗口。下图展示了整个流水过程。

flink run 动态参数 flink 动态表_sql_05

这个就类似批处理了,每个小时产生一次计算结果然后更新结果表。cTime的时间范围在12:00:00 ~12:59:59的时候总共有四行数据,查询计算出了两行结果,并将其追加到结果表。Ctime窗口在13:00:00 and 13:59:59的时候,总共有三行数据,查询再次产生两行结果追加到结果表。随着时间的推移,click数据会被追加到clicks表,结果表也会不断有新的结果产生。
Update 和 append 查询
尽管两个示例查询看起来非常相似(都计算了分组计数聚合),但是内部逻辑还是区别较大:

  • 第一个查询更新以前发出的结果,即结果表的更改日志流包含INSERT和UPDATE更改。
  • 第二个查询仅append到结果表,即结果表的更改日志流仅包含INSERT更改。
    查询是生成仅append表还是update表有一些区别:
  • 产生update变化的查询通常必须维护更多状态。
  • 将仅append(即只有insert操作)表转换为流与将update表(即既有插入也有更新)转换为流的方式是不同。

4.查询限制

并不是所有的查询都能以流查询的格式执行的。因为有些查询计算起来成本比较高,要么就是要维护的状态比较大,要么就是计算更新成本高。
状态大小:连续查询在无界流上执行,通常应该运行数周或数月,甚至7*24小时。因此,连续查询处理的数据总量可能非常大。为了更新先前生成的结果,可能需要维护所有输出的行。例如,第一个示例查询需要存储每个用户的URL计数,以便能够增加计数,并在输入表收到新行时发出新结果。如果仅统计注册用户,则要维护的计数可能不会太高。但是,如果未注册的用户分配了唯一的用户名,则要维护的计数数将随着时间的推移而增长,最终可能在某天查询失败。意思是随着用户的增加,需要为每个用户都维护一个计数状态,则状态也会逐渐增加,总有一天会超过内存限制导致查询失败。 那么怎么解决呢? 一般我们会加上窗口来计算,比如一天,这样的话对每个用户会维护一天的状态,窗口触发计算的时候会清除状态,下次该用户再来的时候会形成新的一天的窗口。。。。依次逐步计算。 不过计算的最终结果是以天为维度的, 所以如果需要统计所有的url计数则需要用户额外写sql.
这也是可以理解的, 毕竟flink本身只适合实时处理实时展示某段时间的结果,而不是缓存所有的历史状态,这不是flink设计的初衷。 所以请谨慎思考你的sql是否会随着时间推移产生巨大的状态,如果是则说明此需求不适合用flink处理。 或者咨询口径提供者,是否可以接受用窗口逻辑展示某段时间的状态。

SELECT user, COUNT(url)
FROM clicks
GROUP BY user;

5.计算更新

计算更新:有时即使只添加或更新了单个输入记录,某些查询也需要重新计算和更新大部分发出的结果行。显然,这样的查询不适合作为连续查询执行。下面sql是一个示例查询,该查询基于最后一次点击的时间为每个用户计算RANK 。一旦clicks表接收到新增行,用户的lastAction就会更新,并且必须计算新的排名。但是,由于两行不能具有相同的排名,因此所有排名较低的行也需要更新。因此分析下来下面的查询也不适合flink去做, 维护无限增长的状态本身是一个风险行为,虽然短期可能不会有什么影响,但是长时间下去必将失败。

SELECT user, RANK() OVER (ORDER BY lastLogin)
FROM (
 SELECT user, MAX(cTime) AS lastAction FROM clicks GROUP BY user
);

7.表转化为流

在实际开发中:DataStream 和Table Api/Sql联合使用屡见不鲜,Table 和 DataStream 的互相转换很常用。 下面是所有的flink支持的table 转化为DataStream 所有方法。

flink run 动态参数 flink 动态表_flink_06

table 的sql行为决定了其转化为DataStream 流的时候应该用:上面的哪些方法。

7.1sql行为大致分为两种

  1. 仅仅产生insert 流
  2. 既有insert 也有update 也有delete
    可以像传统数据库表一样使用INSERT, UPDATE, 和DELETE修改动态表。当将动态表转化为stream或者写入外部系统的时候,需要对修改进行编码。Flink的Table API和SQL支持三种方式来编码动态表的变化。
  • Append-only stream:假如动态表的更改操作仅仅产生insert 流 ,那么变为stream就仅仅需要将插入的行发送出去即可。此时可以用:toAppendStream(…),或者toDataStream(…)
  • Retract stream: retract(回撤)流是包含两种类型的消息的流,增加消息和回撤消息。通过将INSERT编码为增加消息,DELETE编码为回撤消息,将UPDATE编码为对先前行的回撤消息和对新增行的增加消息,来完成将动态表转换为收回流。下图显示了动态表到回收流的转换。
    此时可以用:toRetractStream(…)

Upsert流: upsert流是一种包含两种消息,upsert消息和删除消息的流。转换为upsert流的动态表需要唯一键。具有唯一键的动态表通过将INSERT和UPDATE编码为upsert消息,DELETE编码为删除消息来完成动态表转化为流。流算符需要知道唯一键属性才能正确处理消息。与回撤流的主要区别在于,UPDATE使用单个消息对update进行编码,因此更有效。下图显示了动态表到upsert流的转换。

此时可以用toChangelogStream(…)

flink run 动态参数 flink 动态表_flink run 动态参数_07

7.2.总结

  1. toAppendStream(…)和toDataStream(…): 产生仅insert 流,这意味着table 对应的sql只是select 过滤操作,并无任何的聚合计算。即:If the Table is also modified by update or delete changes, the conversion will fail. 这两个方法没什么大的区别,感兴趣可以去看下源码。
  2. toRetractStream(…):将table sql行为解释为撤回和插入两种动作:The message will be encoded as Tuple2. The first field is a Boolean flag, the second field holds the record。 第一个字段是true 或者是false, true表示此条数据是插入操作,false表示此数据是撤回操作。 其中:sql delete 被解释为撤回, sql update 被解释为撤回和插入。sql insert被解释为插入操作。
  3. toChangelogStream(…): this method produces instances of Row and sets the RowKind flag that is contained in every record during runtime. 意思是此方法将每行数据转化为Row()对象,且Row对象有个属性为RowKind, RowKind记录了行的行为。 我们来看下RowKind都有哪些值:
public enum RowKind {

    // Note: Enums have no stable hash code across different JVMs, use toByteValue() for
    // this purpose.

    /** Insertion operation. */
    INSERT("+I", (byte) 0),

    /**
     * Update operation with the previous content of the updated row.
     *
     * <p>This kind SHOULD occur together with {@link #UPDATE_AFTER} for modelling an update that
     * needs to retract the previous row first. It is useful in cases of a non-idempotent update,
     * i.e., an update of a row that is not uniquely identifiable by a key.
     */
    UPDATE_BEFORE("-U", (byte) 1),

    /**
     * Update operation with new content of the updated row.
     *
     * <p>This kind CAN occur together with {@link #UPDATE_BEFORE} for modelling an update that
     * needs to retract the previous row first. OR it describes an idempotent update, i.e., an
     * update of a row that is uniquely identifiable by a key.
     */
    UPDATE_AFTER("+U", (byte) 2),

    /** Deletion operation. */
    DELETE("-D", (byte) 3);
    ...
    ...
    
    }

可以看到RowKind有四个行为,INSERT,UPDATE_BEFORE,UPDATE_AFTER,DELETE
也就是说toChangelogStream(…)对sql的解释如下:sql delete 解释为DELETE,sql update 解释为:UPDATE_BEFORE, UPDATE_AFTER, sql插入解释为:INSERT

7.2补充

  • RowKind.UPDATE_BEFORE和RowKind.UPDATE_AFTER, 则为retract形式的changelog stream
  • 有RowKind.UPDATE_AFTER, 则为upsert形式的changelog stream

关于changelog流,DataStream 和Table Api都有很好的支持
1DataStream Api: 可以直接通过changelog流转化为table,这意味着flink天然的支持cdc, 只需要从kafka中读取到数据库的操作日志,然后构造Row{RowKind,value}对象,接着转化为changelong流:tablEnv.fromChangelog[Row…]),此时此刻动态表就相当于数据库中表的再现, 就可以直接用sql查询了。
纯Table Api: 可以用canal format 器直接解释为动态表,这种更加方便,推荐canal format.
下面是一个DataStream 的demo:

package com.test.demo.streamAndTable;

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import org.apache.flink.types.RowKind;

public class ChangeLog {
    public static void main(String[] args) {
        // create environments of both APIs
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
    //通过row对象构造changelog流数据
        DataStream<Row> dataStream = env.fromElements(
                Row.ofKind(RowKind.INSERT, "Alice", 12),
                Row.ofKind(RowKind.INSERT, "Bob", 5),
                Row.ofKind(RowKind.UPDATE_BEFORE, "Alice", 12),
                Row.ofKind(RowKind.UPDATE_AFTER, "Alice", 14),
                Row.ofKind(RowKind.UPDATE_AFTER, "Bob", 100),
                Row.ofKind(RowKind.INSERT,"Mary",20),
                Row.ofKind(RowKind.DELETE,"Mary",20)

        );

        tableEnv.executeSql("CREATE TABLE ChangeLog (" +
                "name string ," +
                "age INT," +
                "PRIMARY KEY (name) NOT ENFORCED" +
                ") WITH (" +
                "'connector' = 'jdbc'," +
                "'url' = 'jdbc:mysql://ip:3306/TestDB'" +
                ",'table-name' = 'change_log'," +
                "'username'='root'," +
                "'password'='123456'," +
                "'driver'='com.mysql.cj.jdbc.Driver')");
//        changelog流转化为动态表
        Table table = tableEnv.fromChangelogStream(dataStream);

//         创建视图
        tableEnv.createTemporaryView("InputTable", table);
        //将查询结果插入数据库
        tableEnv.executeSql("insert into ChangeLog SELECT f0 AS name, f1 as age FROM InputTable");

    }



}

结果为:
Alice 14
Bob 100