FlinkTable文档与个人使用实例

本篇笔记通过翻译flink官方文档的table api文档,进行学习并且给出了相关代码示例。flink版本为1.10.0

1 概论

Apache Flink提供了两个关系API—表API和SQL—用于统一流和批处理。表API是一种用于Scala和Java的语言集成查询API,它允许以非常直观的方式组合来自关系操作符(如选择、筛选和连接)的查询。Flink的SQL支持是基于Apache Calcite实现的SQL标准。无论输入是批处理输入(数据集)还是流输入(DataStream),在这两个接口中指定的查询具有相同的语义并指定相同的结果。

表API和SQL接口彼此紧密集成,以及Flink的DataStream和DataSet API。您可以轻松地在所有api和构建在这些api上的库之间切换。例如,可以使用CEP库从DataStream提取模式,然后使用表API分析模式,或者在对预处理数据运行Gelly图形算法之前,可以使用SQL查询扫描、过滤和聚合批处理表。

请注意,表API和SQL的功能还没有完成,正在积极开发中。并不是所有的操作都被[Table API, SQL]和[stream, batch]输入的每种组合所支持。

使用Table Api需要引入的依赖,针对java语言

<!--引入blink版本的planner-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

想要运行我下面的例子,你需要引入以下pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.cxc</groupId>
    <artifactId>flink-study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <description>flink测试</description>

    <properties>
        <slf4j.version>1.7.6</slf4j.version>
        <log4j.version>1.2.12</log4j.version>
        <flink.version>1.10.0</flink.version>
        <scala.binary.version>2.12</scala.binary.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-core</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-elasticsearch6_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
        </dependency>

        <!--引入table api-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <!--引入blink版本的planner-->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <!--引入flink json 用于定义formatter-->
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-json -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-json</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>

</project>

2 概念和Common API

表API和SQL集成在一个联合API中。这个API的中心概念是作为查询输入和输出的表。本文档展示了使用表API和SQL查询的程序的常见结构,如何注册表、如何查询表以及如何发出表。

2.1 两个计划器之间的区别

  • Blink将批处理作业视为流的一种特殊情况。因此,也不支持表和数据集之间的转换,批处理作业将不会转换为DateSet程序,而是转换为DataStream程序,与流作业相同。

  • Blink计划器不支持批处理表源,使用有界流表源代替它。

  • Blink计划器只支持全新的目录,不支持已被弃用的ExternalCatalog。

  • 旧规划器和Blink规划器的FilterableTableSource实现是不兼容的。旧的规划器将把PlannerExpressions下推到FilterableTableSource,而Blink规划器将下推表达式。

  • 基于字符串的键-值配置选项(有关详细信息,请参阅配置文档)仅用于Blink计划器。计划配置在两个计划器中的实现(CalciteConfig)是不同的。Blink计划器将多个接收器优化为一个DAG(只支持TableEnvironment,不支持StreamTableEnvironment)。旧的计划总是优化每个下沉到一个新的DAG,所有DAG是相互独立的。旧的计划器现在不支持目录统计信息,而Blink计划器支持。

2.2 Table API和SQL程序的结构

// create a TableEnvironment for specific planner batch or streaming
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// create a Table
tableEnv.connect(...).createTemporaryTable("table1");
// register an output Table
tableEnv.connect(...).createTemporaryTable("outputTable");

// create a Table object from a Table API query
Table tapiResult = tableEnv.from("table1").select(...);
// create a Table object from a SQL query
Table sqlResult  = tableEnv.sqlQuery("SELECT ... FROM table1 ... ");

// emit a Table API result Table to a TableSink, same for SQL result
tapiResult.insertInto("outputTable");

// execute
tableEnv.execute("java_job");

2.3 创建一个TableEnvironment

TableEnvironment是Table API和SQL集成的中心概念。它负责:

  • Table在内部目录中注册
  • 注册目录
  • 加载可插拔模块
  • 执行SQL查询
  • 注册用户定义的(标量,表或聚合)函数
  • DataStreamDataSet转换为Table
  • 持有对ExecutionEnvironment或的引用StreamExecutionEnvironment

下面代码展示了使用不同的计划器的TableEnvironment

// **********************
// FLINK STREAMING QUERY
// **********************
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.java.StreamTableEnvironment;

EnvironmentSettings fsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build();
StreamExecutionEnvironment fsEnv = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment fsTableEnv = StreamTableEnvironment.create(fsEnv, fsSettings);
// or TableEnvironment fsTableEnv = TableEnvironment.create(fsSettings);

// ******************
// FLINK BATCH QUERY
// ******************
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.table.api.java.BatchTableEnvironment;

ExecutionEnvironment fbEnv = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment fbTableEnv = BatchTableEnvironment.create(fbEnv);

// **********************
// BLINK STREAMING QUERY
// **********************
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.java.StreamTableEnvironment;

StreamExecutionEnvironment bsEnv = StreamExecutionEnvironment.getExecutionEnvironment();
EnvironmentSettings bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
StreamTableEnvironment bsTableEnv = StreamTableEnvironment.create(bsEnv, bsSettings);
// or TableEnvironment bsTableEnv = TableEnvironment.create(bsSettings);

// ******************
// BLINK BATCH QUERY
// ******************
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;

EnvironmentSettings bbSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inBatchMode().build();
TableEnvironment bbTableEnv = TableEnvironment.create(bbSettings);

2.4 创建表

A TableEnvironment维护使用标识符创建的表目录的映射。每个标识符由3部分组成:目录名称(Catalog),数据库名称(Database)和对象名称(Table)。如果未指定目录或数据库,则将使用当前默认值(请参阅表标识符扩展部分中的示例)。

表可以是虚拟(VIEWS)或常规(TABLES)。VIEWS可以从现有Table对象创建,通常是Table API或SQL查询的结果。TABLES描述外部数据,例如文件,数据库表或消息队列。

2.4.1 临时表和永久表

表可以是临时的,并与单个Flink会话的生命周期相关,也可以是永久的,并且在多个Flink会话和群集中可见。

永久表需要一个目录(例如Hive Metastore)来维护有关表的元数据。创建永久表后,连接到目录的任何Flink会话都可以看到该表,并且该表将一直存在,直到明确删除该表为止。

另一方面,临时表始终存储在内存中,并且仅在它们在其中创建的Flink会话期间存在。这些表对其他会话不可见。它们未绑定到任何目录或数据库,但可以在一个目录或数据库的名称空间中创建。如果删除了临时表的相应数据库,则不会删除这些临时表。

表始终使用由目录,数据库和表名组成的三部分标识符进行注册

2.4.2 虚拟表创建

// get a TableEnvironment
TableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// table is the result of a simple projection query 
Table projTable = tableEnv.from("X").select(...);

// register the Table projTable as table "projectedTable"
tableEnv.createTemporaryView("projectedTable", projTable);

2.4.3 连接器表

表也可以通过使用连接器来进行创建,可以使用kafka,elasticsearch等连接器进行创建。主要有以下几个步骤:连接连接器–>定义格式化–>定义scheme–>创建view

tableEnvironment
  .connect(...)
  .withFormat(...)
  .withSchema(...)
  .inAppendMode()
  .createTemporaryTable("MyTable")

2.4.4 扩展表标识符,利用反引号转义

表始终使用由目录,数据库和表名组成的三部分标识符进行注册。

用户可以将一个目录和其中的一个数据库设置为“当前目录”和“当前数据库”。使用它们,上述三部分标识符中的前两个部分可以是可选的-如果未提供它们,则将引用当前目录和当前数据库。用户可以通过表API或SQL切换当前目录和当前数据库。

标识符遵循SQL要求,这意味着可以使用反引号(```)对其进行转义。此外,所有SQL保留关键字都必须转义。

TableEnvironment tEnv = ...;
tEnv.useCatalog("custom_catalog");
tEnv.useDatabase("custom_database");

Table table = ...;

// 注册名为example的视图,使用全局默认的catalog和database
tableEnv.createTemporaryView("exampleView", table);

// 注册名为example的视图,使用全局默认的catalog,使用名为other_database的database
tableEnv.createTemporaryView("other_database.exampleView", table);

// 注册名为view的视图,使用全局默认的catalog和database,因为view是关键字,所以需要使用反引号转义
tableEnv.createTemporaryView("`View`", table);

// 注册名为example.View的视图,使用全局默认的catalog和database
tableEnv.createTemporaryView("`example.View`", table);

//注册时指定catalog和database
tableEnv.createTemporaryView("other_catalog.other_database.exampleView", table);

2.5 查询表

查询表的方式有两种,一种是利用table api,一种是利用sql查询

2.5.1 Table API

TableAPI的操作就是filter,group by,select等api操作

Table orders = tableEnv.from("Orders");   //Orders是一个DataStream

Table revenue = orders
  .filter("cCountry === 'FRANCE'")
  .groupBy("cID, cName")
  .select("cID, cName, revenue.sum AS revSum");

2.5.2 Sql查询

sql查询直接通过写sql去进行查询,注意在使用这张Orders表之前记得去注册这张表

Table revenue = tableEnv.sqlQuery(
    "SELECT cID, cName, SUM(revenue) AS revSum " +
    "FROM Orders " +
    "WHERE cCountry = 'FRANCE' " +
    "GROUP BY cID, cName"
  );

2.6 Table Sink

我们可以利用table sink将数据发送到想要的地方,例如一个文件、kafka、es、mysql等等

使用table sink的方式有两种:第一种是将table转换为DataStream,然后利用addSInk的方式发出;第二种是定义输出表,然后将结果insert到输出表中

TableEnvironment tableEnv = ...; 

// 创建一个输出表的schema
final Schema schema = new Schema()
    .field("a", DataTypes.INT())
    .field("b", DataTypes.STRING())
    .field("c", DataTypes.LONG());

//创建一个输出表,将结果输出到csv文件中去
tableEnv.connect(new FileSystem("/path/to/file"))
    .withFormat(new Csv().fieldDelimiter('|').deriveSchema())
    .withSchema(schema)
    .createTemporaryTable("CsvSinkTable");

// 利用查询语句或者table api得到一个计算结果table
Table result = ...
// 将结果table插入到输出表中去
result.insertInto("CsvSinkTable");

2.7 解释并执行查询

对于flink old planner和blink planner来说,翻译执行查询的行为是不同的。

2.7.1 Old Planner

oldPlanner根据输入流的不同,将他们转换为DataStream和DataSet

根据表API和SQL查询的输入是流输入还是批处理输入,它们将转换为DataStreamDataSet程序。查询在内部表示为逻辑查询计划,并分为两个阶段:

  1. 优化逻辑计划
  2. 转换为DataStream或DataSet程序

在以下情况下,将转换Table API或SQL查询:

  • 将a Table发射到a TableSink,即何时Table.insertInto()被调用。
  • 指定SQL更新查询,即何时TableEnvironment.sqlUpdate()调用。
  • 将a Table转换为DataStreamor DataSet

转换后,将像常规DataStream或DataSet程序一样处理Table API或SQL查询,并在调用StreamExecutionEnvironment.execute()或时执行ExecutionEnvironment.execute()

2.7.2 Blink Planner

无论表API和SQL查询的输入是流传输还是批处理,都将转换为DataStream程序。查询在内部表示为逻辑查询计划,并分为两个阶段:

  1. 优化逻辑计划,
  2. 转换为DataStream程序。

翻译查询的行为是不同的TableEnvironmentStreamTableEnvironment

对于TableEnvironment,表API或SQL查询在TableEnvironment.execute()调用时会被翻译,因为TableEnvironment它将优化多个接收器为一个DAG。

当使用时StreamTableEnvironment,在以下情况下转换表API或SQL查询:

  • 将a Table发射到a TableSink,即何时Table.insertInto()被调用。
  • 指定SQL更新查询,即何时TableEnvironment.sqlUpdate()调用。
  • 将a Table转换为DataStream

转换后,将像常规的DataStream程序一样处理Table API或SQL查询,并在调用TableEnvironment.execute()或时执行StreamExecutionEnvironment.execute()

2.8 与DataStream和DataSet API集成

两个Planner都可以与DataStream API集成。只有旧的planner可以与DataSet API集成,批量上的Blink planner不能与两者结合。注意:下面讨论的数据集API只与批处理上的旧计划器相关。

Table API和SQL查询可以很容易地集成到DataStream和DataSet程序中。例如,可以查询外部表(例如从RDBMS),做一些预处理,如过滤、投射、聚合,或加入元数据,然后进一步处理数据DataStream数据或DataSet的API(和任何库建在这些API,如CEP或葛里炸药)。相反,表API或SQL查询也可以应用于DataStream或DataSet程序的结果。

这种交互可以通过将数据令或数据集转换为表来实现,反之亦然。在本节中,我们将描述这些转换是如何完成的。

2.8.1 从DataStream或者DataSet创建一个view

如果我们想使用sql查询,那么就必须要创建视图

// 获取表执行环境
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

DataStream<Tuple2<Long, String>> stream = ...

// 注册名为myTable的视图,字段默认为f0,f1
tableEnv.createTemporaryView("myTable", stream);

//注册名为myTable2的视图,字段指定为myLong和myString
tableEnv.createTemporaryView("myTable2", stream, "myLong, myString");

2.8.2 将DataStream或DataSet转换为表

如果想使用table api进行查询,那么就不需要创建视图,只需要创建表即可。

//获取表执行环境
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

//获取一个数据流,类型为二元组
DataStream<Tuple2<Long, String>> stream = ...

//利用fromDataStream方法将DataStream转换为table,默认字段为f0和f1
Table table1 = tableEnv.fromDataStream(stream);

//创建一个字段为myLong和myString的表
Table table2 = tableEnv.fromDataStream(stream, "myLong, myString");

//接下来就可以使用table api进行相关操作,例如select、filter等等

2.8.3 将表转换为DataStream或DataSet

表可以转换为DataStream和DataTable

在将表转换为数据令或数据集时,需要指定结果数据令或数据集的数据类型,即。表中的行要转换为的数据类型。通常最方便的转换类型是Row。以下列表概述了不同选项的特点:

  • : 字段按位置映射,任意数量的字段,支持空值,没有类型安全的访问。
  • POJO:字段按名称映射(POJO字段必须命名为Table字段),任意数量的字段,支持null值,类型安全访问。
  • 案例类:字段按位置映射,不支持null值,类型安全访问。
  • 元组:按位置映射字段,限制为22(Scala)或25(Java)字段,不支持null值,类型安全访问。
  • 原子类型Table必须具有单个字段,不支持null值,类型安全访问。

2.8.4 将表转换为DataStream实例

流查询的结果表将被动态更新。,当新记录到达查询的输入流时,它会发生变化。因此,将这种动态查询转换到的DataStream需要对表的更新进行编码。

将表转换为DataStream的方式有两种:

  1. 追加模式(Append Mode): 只有当动态表仅被INSERT修改时,才能使用此模式。它只用于追加,以前发出的结果永远不会更新。
  2. 撤回模式(Retract Mode): 这个模式可以一直使用。它使用布尔标志对插入和删除更改进行编码。
// 获取流式表环境
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// 获取一个表,拥有两个字段 (String name, Integer age)
Table table = ...

// 将表转换为appendStream,并且指定类型为Row
DataStream<Row> dsRow = tableEnv.toAppendStream(table, Row.class);

// 将表转换为一个二元组,类型为String和Integer
// 利用TupleTypeInfo进行转换
TupleTypeInfo<Tuple2<String, Integer>> tupleType = new TupleTypeInfo<>(
  Types.STRING(),
  Types.INT());
DataStream<Tuple2<String, Integer>> dsTuple = 
  tableEnv.toAppendStream(table, tupleType);

// 将table转换为RetractStream,指定类型为Row
//   布尔类型的值表明修改的类型,true是插入,delete是删除
DataStream<Tuple2<Boolean, Row>> retractStream = 
  tableEnv.toRetractStream(table, Row.class);

2.9 数据类型到Table Schema的映射

Flink的DataStream和DataSet api支持非常多样化的类型。复合类型,如元组(内置Scala和Flink Java元组)、pojo、Scala case类和Flink的Row类型,允许使用包含多个字段的嵌套数据结构,这些字段可以在表表达式中访问。其他类型被视为原子类型。下面,我们将描述表API如何将这些类型转换为内部行表示,并展示将DataStream转换为表的示例。

数据类型到表模式的映射有两种方式:基于字段位置或基于字段名称。

2.9.1 基于位置的映射

基于位置的映射可用于为字段提供更有意义的名称,同时保持字段的顺序。该映射可用于定义字段顺序的复合数据类型以及原子类型。复合数据类型(如元组、行和case类)具有这样的字段顺序。但是,POJO的字段必须基于字段名进行映射(请参阅下一节)。字段可以被投影出来,但不能使用别名重命名。

在定义基于位置的映射时,指定的名称必须不存在于输入数据类型中,否则API将假定映射应该基于字段名称发生。如果没有指定字段名,则使用复合类型的默认字段名和字段顺序,对于原子类型则使用f0。

// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section;

DataStream<Tuple2<Long, Integer>> stream = ...

// convert DataStream into Table with default field names "f0" and "f1"
Table table = tableEnv.fromDataStream(stream);

// convert DataStream into Table with field "myLong" only
Table table = tableEnv.fromDataStream(stream, "myLong");

// convert DataStream into Table with field names "myLong" and "myInt"
Table table = tableEnv.fromDataStream(stream, "myLong, myInt");

2.9.2 基于名字的映射

基于名称的映射可以用于任何数据类型,包括pojo。它是定义表模式映射的最灵活的方法。映射中的所有字段都按名称引用,并且可以使用别名重命名为。字段可以被重新排序并投射出去。

如果没有指定字段名,则使用复合类型的默认字段名和字段顺序,对于原子类型则使用f0。

//获取流式表环境
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

DataStream<Tuple2<Long, Integer>> stream = ...

//将DataStream转换为Table,默认字段为f0和f1
Table table = tableEnv.fromDataStream(stream);

//将DataStream转换为Table,且只有一个字段名为f1
Table table = tableEnv.fromDataStream(stream, "f1");

//将DataStream转换为Table,并且将f1和f0交换顺序
Table table = tableEnv.fromDataStream(stream, "f1, f0");

//将DataStream转换为Table,交换f0和f1的顺序并将他们起一个别名
Table table = tableEnv.fromDataStream(stream, "f1 as myInt, f0 as myLong");

2.9.3 原子类型(Atomic Type)

Flink将原语(整数、双精度、字符串)或泛型类型(不能被分析和分解的类型)视为原子类型。原子类型的DataStream或DataSet被转换为具有单个属性的表。属性的类型是从原子类型推断出来的,可以指定属性的名称。

// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

DataStream<Long> stream = ...

// convert DataStream into Table with default field name "f0"
Table table = tableEnv.fromDataStream(stream);

// convert DataStream into Table with field name "myLong"
Table table = tableEnv.fromDataStream(stream, "myLong");

2.9.4 Tuple

Flink支持Scala的内置元组,并为Java提供了自己的元组类。这两种元组的数据列和数据集都可以转换为表。可以通过为所有字段提供名称来重命名字段(基于位置的映射)。如果没有指定字段名称,则使用默认字段名称。如果原始字段名(f0, f1,…for Flink元组和_1,_2,…for Scala元组)被引用,API假设映射是基于名称的,而不是基于位置的。基于名称的映射允许使用别名(as)重新排序字段和投影。

// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

DataStream<Tuple2<Long, String>> stream = ...

// convert DataStream into Table with default field names "f0", "f1"
Table table = tableEnv.fromDataStream(stream);

// convert DataStream into Table with renamed field names "myLong", "myString" (position-based)
Table table = tableEnv.fromDataStream(stream, "myLong, myString");

// convert DataStream into Table with reordered fields "f1", "f0" (name-based)
Table table = tableEnv.fromDataStream(stream, "f1, f0");

// convert DataStream into Table with projected field "f1" (name-based)
Table table = tableEnv.fromDataStream(stream, "f1");

// convert DataStream into Table with reordered and aliased fields "myString", "myLong" (name-based)
Table table = tableEnv.fromDataStream(stream, "f1 as 'myString', f0 as 'myLong'");

2.9.5 POJO

Flink支持作为复合类型的pojo。这里记录了决定POJO的规则。

在将POJO DataStream或数据集转换为表而不指定字段名时,将使用原始POJO字段的名称。名称映射需要原始名称,不能按位置进行。字段可以使用别名(使用as关键字)重命名、重新排序和投影。

//获取表环境
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

// Person有两个属性,name和age
DataStream<Person> stream = ...

// 将DataStream转换为默认字段名“age”,“name”的表(字段按名称排序!)
Table table = tableEnv.fromDataStream(stream);

//将DataStream转换为具有重命名字段“myAge”、“myName”(基于名称)的表
Table table = tableEnv.fromDataStream(stream, "age as myAge, name as myName");

// 将DataStream转换为table,只要name字段
Table table = tableEnv.fromDataStream(stream, "name");

//将DataStream转换为table,
Table table = tableEnv.fromDataStream(stream, "name as myName");

2.9.6 Row

Row数据类型支持任意数量的字段和具有空值的字段。字段名可以通过RowTypeInfo指定,也可以在将Row DataStrean或DataSet转换为表时指定。row类型支持按位置和名称映射字段。字段可以通过为所有字段提供名称来重命名(基于位置的映射),或者单独选择用于投影/排序/重命名(基于名称的映射)。

// get a StreamTableEnvironment, works for BatchTableEnvironment equivalently
StreamTableEnvironment tableEnv = ...; // see "Create a TableEnvironment" section

//在' RowTypeInfo '中指定了两个字段"name"和"age"的行DataStream
DataStream<Row> stream = ...

//将DataStream转换为表的默认字段名“name”,“age”
Table table = tableEnv.fromDataStream(stream);

//将DataStream转换为具有重命名字段名“myName”、“myAge”(基于位置)的表
Table table = tableEnv.fromDataStream(stream, "myName, myAge");

//将DataStream转换为具有重命名字段“myName”、“myAge”(基于名称)的表
Table table = tableEnv.fromDataStream(stream, "name as myName, age as myAge");

//将DataStream转换为带有投影字段“name”的表(基于名称)
Table table = tableEnv.fromDataStream(stream, "name");

//将DataStream转换为带有投影和重命名字段“myName”的表(基于名称)
Table table = tableEnv.fromDataStream(stream, "name as myName");

2.10 查询优化

暂时搁置:给出官网的文档地址:查询优化官方文档

3 时间语义

flink在流传输中支持不同的时间语义:

  1. 处理时间(Processing time)

  2. 事件时间(Event time)

  3. 吸收时间(Ingestion time)

本节根据官网资料介绍一下这三个时间语义,然后说明在table api中的应用

在这里插入图片描述

3.1 处理时间

处理时间是指机器执行相应操作的系统时间。

当流程序在处理时间上运行时,所有基于时间的操作(如时间窗口)将使用运行各自操作符的机器的系统时钟。每小时的处理时间窗口将包括系统时钟指示整个小时的时间之间到达特定操作员的所有记录。例如,如果应用程序在9:15am开始运行,第一个每小时的处理时间窗口将包括在9:15am到10:00am之间处理的事件,下一个窗口将包括在10:00am到11:00am之间处理的事件,以此类推。

处理时间是时间的最简单概念,不需要在流和机器之间进行协调。它提供了最好的性能和最低的延迟。然而,在分布式和异步环境处理时间不提供决定论,因为它是容易的速度记录到系统中(例如从消息队列),运营商之间流动的速度记录在系统内部,并中断(预定,或以其他方式)。

3.2 事件时间

事件时间是每个单独事件在其产生设备上发生的时间。这个时间通常在记录输入Flink之前嵌入到记录中,并且事件时间戳可以从每个记录中提取出来。在事件时间,时间的进展取决于数据,而不是墙上的时钟。事件时间程序必须指定如何生成事件时间水印,这是一种信号事件时间进展的机制。这种水印机制将在下面的一节中进行描述。

在完美的世界中,事件时间处理将产生完全一致和确定性的结果,无论事件何时到达或它们的顺序如何。但是,除非已知事件按顺序(按时间戳)到达,否则在等待无序事件时,事件时间处理会导致一些延迟。由于只能等待有限的时间,这就限制了事件时间应用程序的确定性。

假设所有数据都已到达,事件时间操作将按预期运行,即使在处理无序或延迟的事件,或者在重新处理历史数据时,也将产生正确和一致的结果。例如,每小时的事件时间窗口将包含包含该小时内的事件时间戳的所有记录,而不考虑它们到达的顺序或处理它们的时间。(更多信息请参见事件后期部分。)

请注意,有时当事件时间程序实时处理实时数据时,它们将使用一些处理时间操作,以确保它们以及时的方式进行。

3.3 吸收时间

吸收时间是事件进入Flink的时间。在source操作符中,每条记录获取源的当前时间作为时间戳,基于时间的操作(如时间窗口)引用该时间戳。

吸收时间在概念上位于事件时间和处理时间之间。与处理时间相比,它的开销稍微大一些,但是提供了更可预测的结果。由于吸收时间使用稳定的时间戳(在源处分配一次),因此对记录的不同窗口操作将引用相同的时间戳,而在处理时间中,每个窗口操作符可能将记录分配到不同的窗口(基于本地系统时钟和任何传输延迟)。

与事件时间相比,吸收时间程序不能处理任何无序的事件或延迟的数据,但程序不必指定如何生成水印。

在内部,吸收时间的处理方式与事件时间非常相似,但是具有自动时间戳分配和自动水印生成功能。

4.代码实例

首先我们创建一个KafkaUserBean,用于表示从kafka接受到的数据

注意:pojo的创建一定要有一个空构造函数,否则将无法转换为想要的table

/**
 * create by chenxichao
 */
public class KafkaUserBean {
    private Integer userid;
    private String username;
    private String password;

    public KafkaUserBean() {
    }

    public KafkaUserBean(Integer userid, String username, String password) {
        this.userid = userid;
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "KafkaUserBean{" +
                "userid=" + userid +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    public Integer getUserid() {
        return userid;
    }

    public void setUserid(Integer userid) {
        this.userid = userid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

KAFKA中发送JSON数据样例

{"userid":"1","username":"chenxichao","password":"123456"}
{"userid":"2","username":"mafeifei","password":"123456"}
{"userid":"3","username":"lubenwei","password":"123456"}
{"userid":"4","username":"mamaha","password":"123456"}
{"userid":"5","username":"nakelulu","password":"123456"}
{"userid":"6","username":"gongben","password":"123456"}
{"userid":"7","username":"lvbu","password":"123456"}
{"userid":"8","username":"chenze","password":"123456"}

4.1 将DataStream转换为table

public class TableApiDemo {

    public static void main(String[] args) throws Exception{

        //默认使用老版本的planner
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .useOldPlanner()
                .inStreamingMode()
                .build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env,settings);

        //从kafka中读取数据
        String topic = "firsttopic";
        Properties prop = new Properties();
        //kafka服务器地址
        prop.setProperty("bootstrap.servers", "192.168.245.11:9092");
        prop.setProperty("group.id", "flink_consumer_test01");
        prop.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("auto.offset.reset", "latest");

        DataStreamSource<String> kafkaSourceStream = env.addSource(new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), prop));

        //数据流的转换
        DataStream<KafkaUserBean> beanSource = kafkaSourceStream.map(value -> {
            JSONObject json = JSONObject.parseObject(value);
            return new KafkaUserBean(json.getInteger("userid"), json.getString("username"), json.getString("password"));
        });

        //将数据流转换为三元组
//        DataStream<Tuple3<Integer, String, String>> tuple3Source = kafkaSourceStream.map(value -> {
//            JSONObject json = JSONObject.parseObject(value);
//            return new Tuple3<>(json.getInteger("userid"), json.getString("username"), json.getString("password"));
//        });

//        beanSource.print();

        //将beanSource转换为table
        Table beanTable = tableEnv.fromDataStream(beanSource);
        tableEnv.createTemporaryView("kafkaBeanTable",beanSource);
        beanTable.printSchema();

        //调用table api,得到转换结果
//        Table aggResultSqlTable = tableEnv.sqlQuery("select userid,count(userid) as cnt from kafkaBeanTable group by userid");
//        tableEnv.toRetractStream(aggResultSqlTable, TypeInformation.of(new TypeHint<Tuple2<Integer, Long>>() {})).print("agg");
        Table aggResultSqlTable = tableEnv.sqlQuery("select password,userid,username from kafkaBeanTable");
        DataStream<Tuple2<Boolean, KafkaUserBean>> resultStream = tableEnv.toRetractStream(aggResultSqlTable, KafkaUserBean.class);
        resultStream.print();


        env.execute("flinktablejob");
    }
}

4.2 从kafka中读取数据然后输出到Elasticseach

/**
 * create by chenxichao
 */
public class KafkaSource {

    private static final String topic = "firsttopic";

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        //从kafka中读取数据
        Properties prop = new Properties();
        //kafka服务器地址
        prop.setProperty("bootstrap.servers", "192.168.245.11:9092");
        prop.setProperty("group.id", "flink_consumer_test01");
        prop.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("auto.offset.reset", "latest");

        DataStreamSource<String> kafkaSourceStream = env.addSource(new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), prop));


//        //自定义source
//        DataStreamSource<String> stream = env.addSource(new CustomSource());
//        stream.print();


                //数据流的转换
//        DataStream<KafkaUserBean> beanSource = stream.map(value -> {
//            JSONObject json = JSONObject.parseObject(value);
//            return new KafkaUserBean(json.getInteger("userid"), json.getString("username"), json.getString("password"));
//        });
//        beanSource.print();


        //存放进入es
        List<HttpHost> httpHosts = Collections.singletonList(new HttpHost("192.168.245.138", 9200));
        ElasticsearchSinkFunction<String> esSinkFunction = new ElasticsearchSinkFunction<String>() {
            @Override
            public void process(String string, RuntimeContext runtimeContext, RequestIndexer requestIndexer) {
                IndexRequest indexRequest = new IndexRequest("testflink", "kafkauser").source(string, XContentType.JSON);
                requestIndexer.add(indexRequest);
                System.out.println("save:" + string);
            }
        };

        kafkaSourceStream.addSink(new ElasticsearchSink.Builder<>(httpHosts,esSinkFunction).build());


        //存入kafka中
        //kafkaSourceStream.addSink(new FlinkKafkaProducer<String>("192.168.245.11:9092","sinktest",new SimpleStringSchema()));

        env.execute("test-flink-kafkaSourceStream");
    }
}

4.3 利用connect创建table

/**
 * 利用schema定义输入表和输出表
 * 从kafka读取数据,利用sql进行处理后,再输出到kafka中去
 * create by chenxichao
 */
public class TableExampleWithSchema {
    public static void main(String[] args) throws Exception{
        //不指定输入与输出
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        //通过schema定义输入表kafkaInputTable和输出表kafkaOutputTable
        tableEnv.connect(new Kafka().version("universal").topic("firsttopic")
                .property("bootstrap.servers", "192.168.245.11:9092").property("zookeeper.connect","192.168.245.11:2181"))
                .withFormat(new Json())   //定义读取数据之后的格式化方法
                .withSchema(new Schema().field("userid", DataTypes.INT())
                        .field("username",DataTypes.STRING()).field("password",DataTypes.STRING()))
                .createTemporaryTable("kafkaInputTable");

        //定义输出表
        tableEnv.connect(new Kafka().version("universal").topic("secondtopic")
                .property("bootstrap.servers", "192.168.245.11:9092").property("zookeeper.connect","192.168.245.11:2181"))
                .withFormat(new Json())   //定义读取数据之后的格式化方法
                .withSchema(new Schema().field("userid", DataTypes.INT())
                        .field("username",DataTypes.STRING()).field("password",DataTypes.STRING()))
                .createTemporaryTable("kafkaOutputTable");


        Table resultTable = tableEnv.sqlQuery("SELECT userid,username,password FROM kafkaInputTable");
        DataStream<Tuple2<Boolean, KafkaUserBean>> resultStream = tableEnv.toRetractStream(resultTable, KafkaUserBean.class);

        resultStream.print();

        env.execute("test table schema job");

    }
}

4.4 窗口的使用

这里的时间语义使用处理时间,功能为:从kafka中读取一次,每10s计算一次count,然后输出

/**
 * table中使用窗口进行计算
 * 窗口设置在流转换为table时进行
 * create by chenxichao
 */
public class TableWindowExample {
    public static void main(String[] args) throws Exception{
        //默认使用老版本的planner
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        EnvironmentSettings settings = EnvironmentSettings.newInstance()
                .useOldPlanner()
                .inStreamingMode()
                .build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env,settings);
        //指定事件时间语义,需要在流的读取时指定watermark
//        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);


        //从kafka中读取数据
        String topic = "firsttopic";
        Properties prop = new Properties();
        //kafka服务器地址
        prop.setProperty("bootstrap.servers", "192.168.245.11:9092");
        prop.setProperty("group.id", "flink_consumer_test01");
        prop.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        prop.setProperty("auto.offset.reset", "latest");

        DataStreamSource<String> kafkaSourceStream = env.addSource(new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), prop));

        //数据流的转换
        DataStream<KafkaUserBean> beanSource = kafkaSourceStream.map(value -> {
            JSONObject json = JSONObject.parseObject(value);
            return new KafkaUserBean(json.getInteger("userid"), json.getString("username"), json.getString("password"));
        });

        //使用处理时间作为时间窗口,处理时间即为当前机器时间
        Table table = tableEnv.fromDataStream(beanSource, "userid,username,password,user_action_time.proctime");
        table.printSchema();

        //开一个10s的滚动窗口,统计一共发送了多少个数,这里使用table api实现
        Table resultTable = table.window(Tumble.over("10.seconds").on("user_action_time").as("userActionWindow"))
                .groupBy("userid,userActionWindow").select("count(userid)");


        //利用sql实现
        tableEnv.createTemporaryView("sqlTable",table);

        Table groupResultSqlTable = tableEnv.sqlQuery
                ("SELECT count(userid) FROM sqlTable GROUP BY userid,tumble(user_action_time,interval '10' second)");
        tableEnv.toRetractStream(groupResultSqlTable,Row.class).print();

        env.execute("window table job");
    }
}

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值