骑绿牛采青 發表於 2023-6-15 00:00:00

Flink SQL 知其所以然:Table 与 DataStream 的转转转

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>
        1.序篇</li><li>
        2.背景及应用场景介绍</li><li>
        3.Table 与 DataStream API 的转换具体实现<ul class="second_class_ul"><li>
        3.1.先看一个官网的简单案例</li><li>
        3.2.实现第 2 节中的逻辑</li><li>
        3.3.Table 和 DataStream 转换注意事项</li></ul></li><li>
        4.总结与展望<ul class="second_class_ul"></ul></li></ul></div><p>
        <img title="Flink SQL 知其所以然:Table 与 DataStream 的转转转" alt="Flink SQL 知其所以然:Table 与 DataStream 的转转转" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/fb46a3b886879641f185f5b2dd6ebbd1.jpg" width="auto"></p>

<p class="maodian"></p><h2>
        1.序篇</h2>
<p>
        废话不多说,咱们先直接上本文的目录和结论,小伙伴可以先看结论快速了解博主期望本文能给小伙伴们带来什么帮助:</p>
<ol>
<li>
                背景及应用场景介绍:博主期望你能了解到,Flink 支持了 SQL 和 Table API 中的 Table 与 DataStream 互转的接口。通过这种互转的方式,我们就可以将一些自定义的数据源(DataStream)创建为 SQL 表,也可以将 SQL 执行结果转换为 DataStream 然后后续去完成一些在 SQL 中实现不了的复杂操作。肥肠的方便。</li>
        <li>
                目前只有流任务支持互转,批任务不支持:在 1.13 版本中,由于流和批的 env 接口不一样,流任务为 StreamTableEnvironment,批任务为 TableEnvironment,目前只有 StreamTableEnvironment 支持了互转的接口,TableEnvironment 没有这样的接口,因此目前流任务支持互转,批任务不支持。但是 1.14 版本中流批任务的 env 都统一到了 StreamTableEnvironment 中,流批任务中就都可以进行互转了。</li>
        <li>
                Retract 语义 SQL 转 DataStream 需要重点注意:Append 语义的 SQL 转为 DataStream 使用的 API 为 StreamTableEnvironment::toDataStream,Retract 语义的 SQL 转为 DataStream 使用的 API 为 StreamTableEnvironment::toRetractStream,两个接口不一样,小伙伴萌一定要特别注意。</li>
</ol>
<p class="maodian"></p><h2>
        2.背景及应用场景介绍</h2>
<p>
        相信大家看到本文的标题时,会比较好奇,要写 SQL 就纯 SQL 呗,要写 DataStream 就纯 DataStream 呗,为啥还要把这两个接口做集成呢?</p>
<p>
        博主举一个案例:在拼多多发优惠券的场景下,为了控制成本,希望能在每日优惠券发放金额加和超过 1w 时,及时报警出来,控制预算。</p>
<p>
        优惠券表的发放数据:</p>
<p>
        <img title="Flink SQL 知其所以然:Table 与 DataStream 的转转转" alt="Flink SQL 知其所以然:Table 与 DataStream 的转转转" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/4f3a2ee483e285f868f10b3d04128eec.jpg" width="auto"></p>
<p>
        最终期望的结果是:每天的 money 之和超过 1w 的时候,报警报警报警!!!</p>
<p>
        那么针对上述场景,有两种对应的解决方案:</p>
<ol>
<li>
                方案 1:可想而知,DataStream 是必然能够解决我们的问题的。</li>
        <li>
                方案 2:DataStream 开发效率不高,可以使用 SQL 计算优惠券发放的结果,但是 SQL 无法做到报警。所以可以将 SQL 的查询的结果(即 Table)转为 DataStream,然后在 DataStream 后自定义报警逻辑的算子,超过阈值进行报警。</li>
</ol>
<p>
        本节就介绍方案 2 的实现思路。</p>
<p>
        注意:</p>
<p>
        当然还有一些其他的比如模式识别监控异常然后报警的场景使用 DataStream 去实现就更加复杂了,所以我们也可以使用类似的思路,先 SQL 实现业务逻辑,然后接一个 DataStream 算子实现报警逻辑。</p>

<p class="maodian"></p><h2>
        3.Table 与 DataStream API 的转换具体实现</h2>
<p class="maodian"></p><h3>
        3.1.先看一个官网的简单案例</h3>
<p>
        官网的案例主要是让大家看看要做到 Table 与 DataStream API 的转换会涉及到使用哪些接口。</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>import org.apache.flink.streaming.api.datastream.DataStream; </span></span>
</li>
        <li>
                <span>import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; </span>
</li>
        <li class="alt">
                <span>import org.apache.flink.<span class="keyword">table</span><span>.api.</span><span class="keyword">Table</span><span>; </span></span>
</li>
        <li>
                <span>import org.apache.flink.<span class="keyword">table</span><span>.api.bridge.java.StreamTableEnvironment; </span></span>
</li>
        <li class="alt">
                <span>import org.apache.flink.types.Row; </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); </span>
</li>
        <li>
                <span>StreamTableEnvironment tableEnv = StreamTableEnvironment.<span class="keyword">create</span><span>(env); </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>DataStream&lt;String&gt; dataStream = env.fromElements(<span class="string">"Alice"</span><span>, </span><span class="string">"Bob"</span><span>, </span><span class="string">"John"</span><span>); </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>// 1. 使用 StreamTableEnvironment::fromDataStream API 将 DataStream 转为 <span class="keyword">Table</span><span> </span></span>
</li>
        <li class="alt">
                <span><span class="keyword">Table</span><span> inputTable = tableEnv.fromDataStream(dataStream); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>// 将 <span class="keyword">Table</span><span> 注册为一个临时表 </span></span>
</li>
        <li>
                <span>tableEnv.createTemporaryView(<span class="string">"InputTable"</span><span>, inputTable); </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>// 然后就可以在这个临时表上做一些自定义的查询了 </span>
</li>
        <li class="alt">
                <span><span class="keyword">Table</span><span> resultTable = tableEnv.sqlQuery(</span><span class="string">"SELECT UPPER(f0) FROM InputTable"</span><span>); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>// 2. 也可以使用 StreamTableEnvironment::toDataStream 将 <span class="keyword">Table</span><span> 转为 DataStream </span></span>
</li>
        <li>
                <span>// 注意:这里只能转为 DataStream&lt;Row&gt;,其中的数据类型只能为 Row </span>
</li>
        <li class="alt">
                <span>DataStream&lt;Row&gt; resultStream = tableEnv.toDataStream(resultTable); </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>// 将 DataStream 结果打印到控制台 </span>
</li>
        <li>
                <span>resultStream.print(); </span>
</li>
        <li class="alt">
                <span>env.<span class="keyword">execute</span><span>(); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>// prints: </span>
</li>
        <li>
                <span>// +I </span>
</li>
        <li class="alt">
                <span>// +I </span>
</li>
        <li>
                <span>// +I </span>
</li>
</ol>
<p>
        可以看到重点的接口就是:</p>
<ol>
<li>
                StreamTableEnvironment::toDataStream:将 Table 转为 DataStream</li>
        <li>
                StreamTableEnvironment::fromDataStream:将 DataStream 转为 Table</li>
</ol>
<p class="maodian"></p><h3>
        3.2.实现第 2 节中的逻辑</h3>
<p>
        我们使用上面介绍的两个接口对优惠券发放金额预警的案例做一个实现。</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>@Slf4j </span></span>
</li>
        <li>
                <span><span class="keyword">public</span><span> class AlertExample { </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>    <span class="keyword">public</span><span> </span><span class="keyword">static</span><span> void main(String[] args) throws Exception { </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        FlinkEnv flinkEnv = FlinkEnvUtils.getStreamTableEnv(args); </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        String createTableSql = <span class="string">"CREATE TABLE source_table (\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"    id BIGINT,\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"    money BIGINT,\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"    row_time AS cast(CURRENT_TIMESTAMP as timestamp_LTZ(3)),\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"    WATERMARK FOR row_time AS row_time - INTERVAL '5' SECOND\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">") WITH (\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'connector' = 'datagen',\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'rows-per-second' = '1',\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'fields.id.min' = '1',\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'fields.id.max' = '100000',\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'fields.money.min' = '1',\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'fields.money.max' = '100000'\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">")\n"</span><span>; </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        String querySql = <span class="string">"SELECT UNIX_TIMESTAMP(CAST(window_end AS STRING)) * 1000 as window_end, \n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"      window_start, \n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"      sum(money) as sum_money,\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"      count(distinct id) as count_distinct_id\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"FROM TABLE(CUMULATE(\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"         TABLE source_table\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"         , DESCRIPTOR(row_time)\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"         , INTERVAL '5' SECOND\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"         , INTERVAL '1' DAY))\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"GROUP BY window_start, \n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"        window_end"</span><span>; </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        // 1. 创建数据源表,即优惠券发放明细数据 </span>
</li>
        <li class="alt">
                <span>        flinkEnv.streamTEnv().executeSql(createTableSql); </span>
</li>
        <li>
                <span>        // 2. 执行 query 查询,计算每日发放金额 </span>
</li>
        <li class="alt">
                <span>        <span class="keyword">Table</span><span> resultTable = flinkEnv.streamTEnv().sqlQuery(querySql); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>        // 3. 报警逻辑(toDataStream 返回 Row 类型),如果 sum_money 超过 1w,报警 </span>
</li>
        <li>
                <span>        flinkEnv.streamTEnv() </span>
</li>
        <li class="alt">
                <span>                .toDataStream(resultTable, Row.class) </span>
</li>
        <li>
                <span>                .flatMap(new FlatMapFunction&lt;Row, Object&gt;() { </span>
</li>
        <li class="alt">
                <span>                    @Override </span>
</li>
        <li>
                <span>                    <span class="keyword">public</span><span> void flatMap(Row value, Collector&lt;Object&gt; </span><span class="keyword">out</span><span>) throws Exception { </span></span>
</li>
        <li class="alt">
                <span>                        long l = Long.parseLong(String.valueOf(value.getField(<span class="string">"sum_money"</span><span>))); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>                        if (l &gt; 10000L) { </span>
</li>
        <li>
                <span>                            log.info(<span class="string">"报警,超过 1w"</span><span>); </span></span>
</li>
        <li class="alt">
                <span>                        } </span>
</li>
        <li>
                <span>                    } </span>
</li>
        <li class="alt">
                <span>                }); </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>        flinkEnv.env().<span class="keyword">execute</span><span>(); </span></span>
</li>
        <li>
                <span>    } </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>} </span>
</li>
</ol>
<p>
        执行效果如下:</p>
<p>
        <img title="Flink SQL 知其所以然:Table 与 DataStream 的转转转" alt="Flink SQL 知其所以然:Table 与 DataStream 的转转转" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/0cf9288de4b95f4fdfb4d7efd37bb5b9.jpg" width="auto"></p>
<p class="maodian"></p><h3>
        3.3.Table 和 DataStream 转换注意事项</h3>
<p>
        3.3.1.目前只支持流任务互转(1.13)</p>
<p>
        目前在 1.13 版本中,Flink 对于 Table 和 DataStream 的转化是有一些限制的:</p>
<p>
        目前流任务使用的 env 为 StreamTableEnvironment,批任务为 TableEnvironment,而 Table 和 DataStream 之间的转换目前只有 StreamTableEnvironment 的接口支持。</p>
<p>
        所以其实小伙伴萌可以理解为只有流任务才支持 Table 和 DataStream 之间的转换,批任务是不支持的(虽然可以使用流模式处理有界流(批数据),但效率较低,这种骚操作不建议大家搞)。</p>
<p>
        那什么时候才能支持批任务的 Table 和 DataStream 之间的转换呢?</p>
<p>
        1.14 版本支持。1.14 版本中,流和批的都统一到了 StreamTableEnvironment 中,因此就可以做 Table 和 DataStream 的互相转换了。</p>
<p>
        3.3.2.Retract 语义 SQL 转 DataStream 注意事项</p>
<p>
        Retract 语义的 SQL 使用 toDataStream 转换会报错不支持。具体报错截图如下。意思是不支持 update 类型的结果数据。</p>
<p>
        <img title="Flink SQL 知其所以然:Table 与 DataStream 的转转转" alt="Flink SQL 知其所以然:Table 与 DataStream 的转转转" border="0" height="auto" src="https://zhuji.jb51.net/uploads/img/202305/275c49f6e118876654d2e08b92534288.jpg" width="auto"></p>
<p>
        如果要把 Retract 语义的 SQL 转为 DataStream,我们需要使用 toRetractStream。如下案例:</p>
<ol class="dp-sql">
<li class="alt">
                <span><span>@Slf4j </span></span>
</li>
        <li>
                <span><span class="keyword">public</span><span> class AlertExampleRetract { </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>    <span class="keyword">public</span><span> </span><span class="keyword">static</span><span> void main(String[] args) throws Exception { </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        FlinkEnv flinkEnv = FlinkEnvUtils.getStreamTableEnv(args); </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        String createTableSql = <span class="string">"CREATE TABLE source_table (\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"    id BIGINT,\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"    money BIGINT,\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"    `time` as cast(CURRENT_TIMESTAMP as bigint) * 1000\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">") WITH (\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'connector' = 'datagen',\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'rows-per-second' = '1',\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'fields.id.min' = '1',\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'fields.id.max' = '100000',\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"  'fields.money.min' = '1',\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"  'fields.money.max' = '100000'\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">")\n"</span><span>; </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>        String querySql = <span class="string">"SELECT max(`time`), \n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"      sum(money) as sum_money\n"</span><span> </span></span>
</li>
        <li class="alt">
                <span>                + <span class="string">"FROM source_table\n"</span><span> </span></span>
</li>
        <li>
                <span>                + <span class="string">"GROUP BY (`time` + 8 * 3600 * 1000) / (24 * 3600 * 1000)"</span><span>; </span></span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>        // 1. 创建数据源表,即优惠券发放明细数据 </span>
</li>
        <li class="alt">
                <span>        flinkEnv.streamTEnv().executeSql(createTableSql); </span>
</li>
        <li>
                <span>        // 2. 执行 query 查询,计算每日发放金额 </span>
</li>
        <li class="alt">
                <span>        <span class="keyword">Table</span><span> resultTable = flinkEnv.streamTEnv().sqlQuery(querySql); </span></span>
</li>
        <li>
                <span>        // 3. 报警逻辑(toRetractStream 返回 Tuple2&lt;Boolean, Row&gt; 类型),如果 sum_money 超过 1w,报警 </span>
</li>
        <li class="alt">
                <span>        // Tuple2&lt;Boolean, Row&gt; f0 的 Boolean 标识是否是回撤消息 </span>
</li>
        <li>
                <span>        flinkEnv.streamTEnv() </span>
</li>
        <li class="alt">
                <span>                .toRetractStream(resultTable, Row.class) </span>
</li>
        <li>
                <span>                .flatMap(new FlatMapFunction&lt;Tuple2&lt;Boolean, Row&gt;, Object&gt;() { </span>
</li>
        <li class="alt">
                <span>                    @Override </span>
</li>
        <li>
                <span>                    <span class="keyword">public</span><span> void flatMap(Tuple2&lt;Boolean, Row&gt; value, Collector&lt;Object&gt; </span><span class="keyword">out</span><span>) throws Exception { </span></span>
</li>
        <li class="alt">
                <span>                        long l = Long.parseLong(String.valueOf(value.f1.getField(<span class="string">"sum_money"</span><span>))); </span></span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>                        if (l &gt; 10000L) { </span>
</li>
        <li>
                <span>                            log.info(<span class="string">"报警,超过 1w"</span><span>); </span></span>
</li>
        <li class="alt">
                <span>                        } </span>
</li>
        <li>
                <span>                    } </span>
</li>
        <li class="alt">
                <span>                }); </span>
</li>
        <li>
                <span> </span>
</li>
        <li class="alt">
                <span>        flinkEnv.env().<span class="keyword">execute</span><span>(); </span></span>
</li>
        <li>
                <span>    } </span>
</li>
        <li class="alt">
                <span> </span>
</li>
        <li>
                <span>} </span>
</li>
</ol>
<p class="maodian"></p><h2>
        4.总结与展望</h2>
<p>
        本文主要介绍了 flink 中 Table 和 DataStream 互转使用方式,并介绍了一些使用注意事项,总结如下:</p>
<ol>
<li>
                背景及应用场景介绍:博主期望你能了解到,Flink 支持了 SQL 和 Table API 中的 Table 与 DataStream 互转的接口。通过这种互转的方式,我们就可以将一些自定义的数据源(DataStream)创建为 SQL 表,也可以将 SQL 执行结果转换为 DataStream 然后后续去完成一些在 SQL 中实现不了的复杂操作。肥肠的方便。</li>
        <li>
                目前只有流任务支持互转,批任务不支持:在 1.13 版本中,由于流和批的 env 接口不一样,流任务为 StreamTableEnvironment,批任务为 TableEnvironment,目前只有 StreamTableEnvironment 支持了互转的接口,TableEnvironment 没有这样的接口,因此目前流任务支持互转,批任务不支持。但是 1.14 版本中流批任务的 env 都统一到了 StreamTableEnvironment 中,流批任务中就都可以进行互转了。</li>
        <li>
                Retract 语义 SQL 转 DataStream 需要重点注意:Append 语义的 SQL 转为 DataStream 使用的 API 为 StreamTableEnvironment::toDataStream,Retract 语义的 SQL 转为 DataStream 使用的 API 为 StreamTableEnvironment::toRetractStream,两个接口不一样,小伙伴萌一定要特别注意。</li>
</ol>
<p>
        原文链接:https://mp.weixin.qq.com/s/b6mT6zX1mOYvGgaLukt87g</p>
頁: [1]
查看完整版本: Flink SQL 知其所以然:Table 与 DataStream 的转转转