Flink基礎系列31-Table API和Flink SQL之流處理中的特殊概念 一. 流處理中的特殊概念 二.案例 參考:

一. 流處理中的特殊概念

  Table API 和 SQL,本質上還是基於關係型表的操作方式;而關係型表、關係代數,以及 SQL 本身,一般是有界的,更適合批處理的場景。這就導致在進行流處理的過程中,理解會 稍微複雜一些,需要引入一些特殊概念。

1.1 流處理和關係代數(表,及 SQL)的區別

  可以看到,其實關係代數(主要就是指關係型數據庫中的表)和 SQL,主要就是針對批 處理的,這和流處理有天生的隔閡。

1.2 動態表(Dynamic Tables)

  因爲流處理面對的數據,是連續不斷的,這和我們熟悉的關係型數據庫中保存的“表” 完全不同。所以,如果我們把流數據轉換成 Table,然後執行類似於 table 的 select 操作,結 果就不是一成不變的,而是隨着新數據的到來,會不停更新。
我們可以隨着新數據的到來,不停地在之前的基礎上更新結果。這樣得到的表,在 Flink Table API 概念裏,就叫做“動態表”(Dynamic Tables)。

  動態表是 Flink 對流數據的 Table API 和 SQL 支持的核心概念。與表示批處理數據的靜態 表不同,動態表是隨時間變化的。動態表可以像靜態的批處理表一樣進行查詢,查詢一個動 態表會產生持續查詢(Continuous Query)。連續查詢永遠不會終止,並會生成另一個動態表。
查詢(Query)會不斷更新其動態結果表,以反映其動態輸入表上的更改。

1.3 流式持續查詢的過程

  下圖顯示了流、動態表和連續查詢的關係:


流式持續查詢的過程爲:

  1. 流被轉換爲動態表。
  2. 對動態表計算連續查詢,生成新的動態表。
  3. 生成的動態表被轉換回流。

1.3.1 將流轉換成表(Table)

  爲了處理帶有關係查詢的流,必須先將其轉換爲表。 從概念上講,流的每個數據記錄,都被解釋爲對結果表的插入(Insert)修改。因爲流式持續不斷的,而且之前的輸出結果無法改變。本質上,我們其實是從一個、只有插入操作的 changelog(更新日誌)流,來構建一個表。

  爲了更好地說明動態表和持續查詢的概念,我們來舉一個具體的例子。 比如,我們現在的輸入數據,就是用戶在網站上的訪問行爲,數據類型(Schema)如下:

user:   VARCHAR,    // 用戶名
cTime: TIMESTAMP, // 訪問某個 URL 的時間戳
url:    VARCHAR      // 用戶訪問的 URL

  下圖顯示瞭如何將訪問 URL 事件流,或者叫點擊事件流(左側)轉換爲表(右側)。


隨着插入更多的訪問事件流記錄,生成的表將不斷增長。

1.3.2 持續查詢(Continuous Query)

  持續查詢,會在動態表上做計算處理,並作爲結果生成新的動態表。與批處理查詢不同, 連續查詢從不終止,並根據輸入表上的更新更新其結果表。

  在任何時間點,連續查詢的結果在語義上,等同於在輸入表的快照上,以批處理模式執 行的同一查詢的結果。

  在下面的示例中,我們展示了對點擊事件流中的一個持續查詢。

  這個 Query 很簡單,是一個分組聚合做 count 統計的查詢。它將用戶字段上的 clicks 表 分組,並統計訪問的 url 數。圖中顯示了隨着時間的推移,當 clicks 表被其他行更新時如何 計算查詢。


1.3.3 將動態錶轉換成流

  與常規的數據庫表一樣,動態表可以通過插入(Insert)、更新(Update)和刪除(Delete) 更改,進行持續的修改。將動態錶轉換爲流或將其寫入外部系統時,需要對這些更改進行編 碼。Flink 的 Table API 和 SQL 支持三種方式對動態表的更改進行編碼:

  1. 僅追加(Append-only)流
    僅通過插入(Insert)更改,來修改的動態表,可以直接轉換爲“僅追加”流。這個流 中發出的數據,就是動態表中新增的每一行。

  2. 撤回(Retract)流
    Retract 流是包含兩類消息的流,添加(Add)消息和撤回(Retract)消息。 動態表通過將 INSERT 編碼爲 add 消息、DELETE 編碼爲 retract 消息、UPDATE 編碼爲被
    更改行(前一行)的 retract 消息和更新後行(新行)的 add 消息,轉換爲 retract 流。 下圖顯示了將動態錶轉換爲 Retract 流的過程。


  3. Upsert(更新插入)流
    Upsert 流包含兩種類型的消息:Upsert 消息和 delete 消息。轉換爲 upsert 流的動態表, 需要有唯一的鍵(key)。
    通過將 INSERT 和 UPDATE 更改編碼爲 upsert 消息,將 DELETE 更改編碼爲 DELETE 消息, 就可以將具有唯一鍵(Unique Key)的動態錶轉換爲流。

下圖顯示了將動態錶轉換爲 upsert 流的過程。


  這些概念我們之前都已提到過。需要注意的是,在代碼裏將動態錶轉換爲 DataStream 時,僅支持 Append 和 Retract 流。而向外部系統輸出動態表的 TableSink 接口,則可以有不 同的實現,比如之前我們講到的 ES,就可以有 Upsert 模式。

1.4 時間特性

  基於時間的操作(比如 Table API 和 SQL 中窗口操作),需要定義相關的時間語義和時間 數據來源的信息。所以,Table 可以提供一個邏輯上的時間字段,用於在表處理程序中,指示時間和訪問相應的時間戳。

  時間屬性,可以是每個表 schema 的一部分。一旦定義了時間屬性,它就可以作爲一個 字段引用,並且可以在基於時間的操作中使用。

  時間屬性的行爲類似於常規時間戳,可以訪問,並且進行計算。

1.4.1 處理時間(Processing Time)

  處理時間語義下,允許表處理程序根據機器的本地時間生成結果。它是時間的最簡單概 念。它既不需要提取時間戳,也不需要生成 watermark。

  定義處理時間屬性有三種方法:在 DataStream 轉化時直接指定;在定義 Table Schema時指定;在創建表的 DDL 中指定。

1.4.1.1 DataStream 轉化成 Table 時指定

  由 DataStream 轉換成表時,可以在後面指定字段名來定義 Schema。在定義 Schema 期間,可以使用.proctime,定義處理時間字段。

  注意,這個 proctime 屬性只能通過附加邏輯字段,來擴展物理 schema。因此,只能在 schema 定義的末尾定義它。

代碼如下:

// 定義好 DataStream

DataStream<String> inputStream = env.readTextFile("\\sensor.txt") DataStream<SensorReading> dataStream = inputStream
.map( line -> {

String[] fields = line.split(",");

return new SensorReading(fields[0], new Long(fields[1]), new

Double(fields[2]));

} );


// 將 DataStream 轉換爲 Table,並指定時間字段
Table sensorTable = tableEnv.fromDataStream(dataStream, "id, temperature, timestamp, pt.proctime");

1.4.1.2 定義Table Schema時指定

  這種方法其實也很簡單,只要在定義 Schema 的時候,加上一個新的字段,並指定成proctime 就可以了。

代碼如下:

tableEnv.connect(

new FileSystem().path("..\\sensor.txt"))
.withFormat(new Csv())
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.field("temperature", DataTypes.DOUBLE())
.field("pt", DataTypes.TIMESTAMP(3))
.proctime()  // 指定 pt 字段爲處理時間
) // 定義表結構
.createTemporaryTable("inputTable"); // 創建臨時表

1.4.1.3 創建表的 DDL 中指定

在創建表的 DDL 中,增加一個字段並指定成 proctime,也可以指定當前的時間字段。

代碼如下:

String sinkDDL = "create table dataTable (" +
"   id varchar(20) not null, " +
"   ts bigint, " +
"   temperature double, " +
"   pt AS PROCTIME() " +
") with (" +
"   'connector.type' = 'filesystem', " + " 'connector.path' = '/sensor.txt', " + " 'format.type' = 'csv')";


tableEnv.sqlUpdate(sinkDDL);

注意:運行這段 DDL,必須使用 Blink Planner。

1.4.2 事件時間(Event Time)

  事件時間語義,允許表處理程序根據每個記錄中包含的時間生成結果。這樣即使在有亂 序事件或者延遲事件時,也可以獲得正確的結果。

  爲了處理無序事件,並區分流中的準時和遲到事件;Flink 需要從事件數據中,提取時 間戳,並用來推進事件時間的進展(watermark)。

1.4.2.1 DataStream 轉化成 Table 時指定

  在 DataStream 轉換成 Table,schema 的定義期間,使用.rowtime 可以定義事件時間屬性。 注意,必須在轉換的數據流中分配時間戳和 watermark。

  在將數據流轉換爲表時,有兩種定義時間屬性的方法。根據指定的.rowtime 字段名是否 存在於數據流的架構中,timestamp 字段可以:
⚫ 作爲新字段追加到 schema
⚫ 替換現有字段
在這兩種情況下,定義的事件時間戳字段,都將保存 DataStream 中事件時間戳的值。

代碼如下:

DataStream<String> inputStream = env.readTextFile("\\sensor.txt") DataStream<SensorReading> dataStream = inputStream
.map( line -> {

String[] fields = line.split(",");

return new SensorReading(fields[0], new Long(fields[1]), new

Double(fields[2]));

} )

.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(1)) {
@Override

public long extractTimestamp(SensorReading element) {

return element.getTimestamp() * 1000L;

}

});


Table sensorTable = tableEnv.fromDataStream(dataStream, "id, timestamp.rowtime as ts, temperature");

1.4.2.2 定義 Table Schema 時指定

  這種方法只要在定義 Schema 的時候,將事件時間字段,並指定成 rowtime 就可以了。

代碼如下:

tableEnv.connect(

new FileSystem().path("sensor.txt"))
.withFormat(new Csv())
.withSchema(new Schema()
.field("id", DataTypes.STRING())
.field("timestamp", DataTypes.BIGINT())
.rowtime(
new Rowtime()
.timestampsFromField("timestamp")   // 從字段中提取時間戳
.watermarksPeriodicBounded(1000)    // watermark 延遲 1 秒
)
.field("temperature", DataTypes.DOUBLE())
) // 定義表結構
.createTemporaryTable("inputTable"); // 創建臨時表

1.4.2.3 創建表的 DDL 中指定

  事件時間屬性,是使用 CREATE TABLE DDL 中的 WARDMARK 語句定義的。watermark 語 句,定義現有事件時間字段上的 watermark 生成表達式,該表達式將事件時間字段標記爲事 件時間屬性。

代碼如下:

String sinkDDL = "create table dataTable (" +
" id varchar(20) not null, " +
"   ts bigint, " +
"   temperature double, " +
"   rt AS TO_TIMESTAMP( FROM_UNIXTIME(ts) ), " +
"   watermark for rt as rt - interval '1' second" +
") with (" +
"   'connector.type' = 'filesystem', " + " 'connector.path' = '/sensor.txt', " + " 'format.type' = 'csv')";

tableEnv.sqlUpdate(sinkDDL);

  這裏 FROM_UNIXTIME 是系統內置的時間函數,用來將一個整數( 秒數) 轉換成 “YYYY-MM-DD hh:mm:ss”格式(默認,也可以作爲第二個 String 參數傳入)的日期時間 字符串(date time string);然後再用 TO_TIMESTAMP 將其轉換成 Timestamp。

二.案例

代碼:

package org.flink.tableapi;


import org.flink.beans.SensorReading;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.table.api.Over;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.Tumble;
import org.apache.flink.table.api.java.StreamTableEnvironment;
import org.apache.flink.types.Row;

/**
 * @author 只是甲
 * @date   2021-09-30
 */

public class TableTest5_TimeAndWindow {
    public static void main(String[] args) throws Exception {
        // 1. 創建環境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // 2. 讀入文件數據,得到DataStream
        DataStream<String> inputStream = env.readTextFile("C:\\Users\\Administrator\\IdeaProjects\\FlinkStudy\\src\\main\\resources\\sensor.txt");

        // 3. 轉換成POJO
        DataStream<SensorReading> dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
        })
                .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
                    @Override
                    public long extractTimestamp(SensorReading element) {
                        return element.getTimestamp() * 1000L;
                    }
                });

        // 4. 將流轉換成表,定義時間特性
//        Table dataTable = tableEnv.fromDataStream(dataStream, "id, timestamp as ts, temperature as temp, pt.proctime");
        Table dataTable = tableEnv.fromDataStream(dataStream, "id, timestamp as ts, temperature as temp, rt.rowtime");

        tableEnv.registerTable("sensor", dataTable);

        // 5. 窗口操作
        // 5.1 Group Window
        // table API
        Table resultTable = dataTable.window(Tumble.over("10.seconds").on("rt").as("tw"))
                .groupBy("id, tw")
                .select("id, id.count, temp.avg, tw.end");

        // SQL
        Table resultSqlTable = tableEnv.sqlQuery("select id, count(id) as cnt, avg(temp) as avgTemp, tumble_end(rt, interval '10' second) " +
                "from sensor group by id, tumble(rt, interval '10' second)");

        // 5.2 Over Window
        // table API
        Table overResult = dataTable.window(Over.partitionBy("id").orderBy("rt").preceding("2.rows").as("ow"))
                .select("id, rt, id.count over ow, temp.avg over ow");

        // SQL
        Table overSqlResult = tableEnv.sqlQuery("select id, rt, count(id) over ow, avg(temp) over ow " +
                " from sensor " +
                " window ow as (partition by id order by rt rows between 2 preceding and current row)");

//        dataTable.printSchema();
//        tableEnv.toAppendStream(resultTable, Row.class).print("result");
//        tableEnv.toRetractStream(resultSqlTable, Row.class).print("sql");
        tableEnv.toAppendStream(overResult, Row.class).print("result");
        tableEnv.toRetractStream(overSqlResult, Row.class).print("sql");

        env.execute();
    }
}

測試記錄:


參考:

  1. https://www.bilibili.com/video/BV1qy4y1q728
  2. https://ashiamd.github.io/docsify-notes/#/study/BigData/Flink/%E5%B0%9A%E7%A1%85%E8%B0%B7Flink%E5%85%A5%E9%97%A8%E5%88%B0%E5%AE%9E%E6%88%98-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0?id=_11-table-api%e5%92%8cflink-sql
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章