Flink-SQL 开发

背景

Flink SQL 是 Flink 实时计算为简化计算模型,降低用户使用实时计算门槛而设计的 一套符合标准 SQL 语义的开发语言。
自 2015 年开始,阿里巴巴开始调研开源流计算引擎,最终决定基于 Flink 打造新一 代计算引擎,针对 Flink 存在的不足进行优化和改进,并且在 2019 年初将最终代码开源, 也就是我们熟知的 Blink。Blink 在原来的 Flink 基础上最显著的一个贡献就是 Flink SQL 的实现。
Flink SQL 是面向用户的 API 层,在我们传统的流式计算领域,比如 Storm、 SparkStreaming 都会提供一些 Function 或者 Datastream API,用户通过 Java 或 Scala 写业务逻辑,这种方式虽然灵活,但有一些不足,比如具备一定门槛且调优较难,随着版本 的不断更新,API 也出现了很多不兼容的地方。
在这个背景下,毫无疑问,SQL 就成了我们最佳选择,之所以选择将 SQL 作为核心 API, 是因为其具有几个非常重要的特点:
SQL 属于设定式语言,用户只要表达清楚需求即可,不需要了解具体做法;SQL 可优化, 内置多种查询优化器,这些查询优化器可为 SQL 翻译出最优执行计划; SQL 易于理解,不 同行业和领域的人都懂,学习成本较低;SQL 非常稳定,在数据库 30 多年的历史中,SQL 本 身变化较少;流与批的统一,Flink 底层 Runtime 本身就是一个流与批统一的引擎,而 SQL 可以做到 API 层的流与批统一。
Flink 的 SQL 支持,基于实现了 SQL 标准的 Apache Calcite(Apache 开源 SQL 解 析工具)。目前功能尚未完善,处于活跃的开发阶段。

Flink SQL 常用算子

SELECT

SELECT 用于从 DataSet/DataStream 中选择数据,用于筛选出某些列。

WHERE

WHERE 用于从数据集/流中过滤数据,与 SELECT 一起使用,用于根据某些条件对关系做水 平分割,即选择符合条件的记录。

DISTINCT

DISTINCT 用于从数据集/流中去重根据 SELECT 的结果进行去重。
对于流式查询,计算查询结果所需的 State 可能会无限增长,用户需要自己控制查询的状 态范围,以防止状态过大。

GROUP BY

GROUP BY 是对数据进行分组操作。

UNION 和 UNION ALL

UNION 用于将两个结果集合并起来,要求两个结果集字段完全一致,包括字段类型、字段顺 序。
不同于 UNION ALL 的是,UNION 会对结果数据去重。

JOIN

JOIN 用于把来自两个表的数据联合起来形成结果表,
Flink 支持的 JOIN 类型包括:
JOIN - INNER JOIN
LEFT JOIN - LEFT OUTER JOINRIGHT JOIN - RIGHT OUTER JOIN FULL JOIN - FULL OUTER JOIN
这里的 JOIN 的语义和我们在关系型数据库中使用的 JOIN 语义一致。

Group Window

根据窗口数据划分的不同,目前 Apache Flink 有如下 3 种 Bounded Window:

Tumble,滚动窗口,窗口数据有固定的大小,窗口数据无叠加;

Hop,滑动窗口,窗口数据有固定大小,并且有固定的窗口重建频率,窗口数据有叠加;

Session,会话窗口,窗口数据没有固定的大小,根据窗口数据活跃程度划分窗口,窗口数 据无叠加。

Tumble Window

Tumble 滚动窗口有固定大小,窗口数据不重叠,具体语义如下:

flink java 监控 数据库事务 flink sql平台_apache


Hop Window

Hop 滑动窗口和滚动窗口类似,窗口有固定的 size,与滚动窗口不同的是滑动窗口可以通 过 slide 参数控制滑动窗口的新建频率。因此当 slide 值小于窗口 size 的值的时候多个 滑动窗口会重叠,具体语义如下:

flink java 监控 数据库事务 flink sql平台_SQL_02


Session Window

会话时间窗口没有固定的持续时间,但它们的界限由 interval 不活动时间定义,即如果在 定义的间隙期间没有出现事件,则会话窗口关闭。

flink java 监控 数据库事务 flink sql平台_flink_03

Flink SQL 应用

批数据 SQL

用法

  1. 构建 Table 运行环境
  2. 将 DataSet 注册为一张表
  3. 使用 Table 运行环境的 sqlQuery 方法来执行 SQL 语句
    步骤
  4. 获取一个批处理运行环境
  5. 获取一个 Table 运行环境
  6. 创建一个样例类 Order 用来映射数据(订单名、用户名、订单日期、订单金额)
  7. 基于本地 Order 集合创建一个 DataSet source
  8. 使用 Table 运行环境将 DataSet 注册为一张表
  9. 使用 SQL 语句来操作数据(统计用户消费订单的总金额、最大金额、最小金额、订单总 数)
  10. 使用 TableEnv.toDataSet 将 Table 转换为 DataSet
  11. 打印测试
package com.czxy.sql
import org.apache.flink.api.scala.ExecutionEnvironment 
import org.apache.flink.table.api.{Table, TableEnvironment} 
import org.apache.flink.api.scala._ 
import org.apache.flink.types.Row

object BatchFlinkSqlDemo { 
// 3) 创建一个样例类 Order 用来映射数据(订单名、用户名、订单日期、订单金额) 
case class Order(id: Int, userName: String, createTime: String, money: Double) 
def main(args: Array[String]): Unit = { 
	//1) 获取一个批处理运行环境 
	val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment 
	// 2) 获取一个 Table 运行环境 
	val TableEnv = TableEnvironment.getTableEnvironment(env) 
	// 4) 基于本地 Order 集合创建一个 
	DataSet source val orderDataSet: DataSet[Order] = env.fromElements( Order(1, "zhangsan", "2018-10-20 15:30", 358.5), Order(2, "zhangsan", "2018-10-20 16:30", 131.5), Order(3, "lisi", "2018-10-20 16:30", 127.5), Order(4, "lisi", "2018-10-20 16:30", 328.5), Order(5, "lisi", "2018-10-20 16:30", 432.5), Order(6, "zhaoliu", "2018-10-20 22:30", 451.0), Order(7, "zhaoliu", "2018-10-20 22:30", 362.0), Order(8, "zhaoliu", "2018-10-20 22:30", 364.0), Order(9, "zhaoliu", "2018-10-20 22:30", 341.0) )
	// 5) 使用 Table 运行环境将 DataSet 注册为一张表 
	TableEnv.registerDataSet("t_order", orderDataSet) 
	// 6) 使用 SQL 语句来操作数据(统计用户消费订单的总金额、最大金额、最小金额、 订单总数)
 	//订单名、用户名、订单日期、订单金额 
 	val sql = """|select | userName, | sum(money) as totalMoney, | max(money) as maxMoney, | min(money) as minMoney, | count(1) as totalCount |from t_order |group by userName |""".stripMargin 
 	// 7) 使用 TableEnv.toDataSet 将 Table 转换为 
 	DataSet val table: Table = TableEnv.sqlQuery(sql) val result: DataSet[Row] = 			TableEnv.toDataSet[Row](table)
 	// 8) 打印测试 result.print() 
 	}
 }

流数据 SQL

流处理中也可以支持 SQL。
但是需要注意以下几点:

  1. 要使用流处理的 SQL,必须要添加水印时间
  2. 使用 registerDataStream 注册表的时候,使用 ’ 来指定字段
  3. 注册表的时候,必须要指定一个 rowtime,否则无法在 SQL 中使用窗口
  4. 必须要导入 import org.apache.flink.table.api.scala._ 隐式参数
  5. SQL 中使用 trumble(时间列名, interval ‘时间’ sencond) 来进行定义窗口
    步骤
  6. 获取流处理运行环境
  7. 获取 Table 运行环境
  8. 设置处理时间为 EventTime
  9. 创建一个订单样例类 Order ,包含四个字段(订单 ID、用户 ID、订单金额、时间戳)
  10. 创建一个自定义数据源
    a. 使用 for 循环生成 1000 个订单
    b. 随机生成订单 ID(UUID)
    c. 随机生成用户 ID(0-2)
    d. 随机生成订单金额(0-100)
    e. 时间戳为当前系统时间 f. 每隔 1 秒生成一个订单 6) 添加水印,允许延迟 2 秒
  11. 导入 import org.apache.flink.table.api.scala._ 隐式参数
  12. 使用 registerDataStream 注册表,并分别指定字段,还要指定 rowtime 字段
  13. 编写 SQL 语句统计用户订单总数、最大金额、最小金额分组时要使用 tumble(时间列, interval ‘窗口时间’ second) 来创建窗口
  14. 使用 tableEnv.sqlQuery 执行 sql 语句
  15. 将 SQL 的执行结果转换成 DataStream 再打印出来
  16. 启动流处理程序
package com.czxy.sql

import java.util.UUID 
import java.util.concurrent.TimeUnit 
import org.apache.flink.api.scala._ 
import org.apache.flink.streaming.api.TimeCharacteristic 
import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction} 
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrder nessTimestampExtractor 
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment} 
import org.apache.flink.streaming.api.windowing.time.Time 
import org.apache.flink.table.api.{Table, TableEnvironment} 
import org.apache.flink.types.Row import scala.util.Random

/**
 * 使用 Flink SQL 来统计 5 秒内 用户的 订单总数、订单的最大金额、订单的最小金额。
 */
object StreamFlinkSqlDemo {

  //    4) 创建一个订单样例类 Order ,包含四个字段(订单 ID、用户 ID、订单金额、时间戳)
  case class Order(orderId: String, userId: Int, money: Long, createTime: Long)

  def main(args: Array[String]): Unit = {
    //    1) 获取流处理运行环境
    val envs: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //    2) 获取 Table 运行环境
    val tale = TableEnvironment.getTableEnvironment(envs)
    //    3) 设置处理时间为 EventTime
    envs.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //    4) 创建一个订单样例类 Order ,包含四个字段(订单 ID、用户 ID、订单金额、时间戳)
    //    5) 创建一个自定义数据源
    import org.apache.flink.api.scala._
    val orderV = envs.addSource(new RichSourceFunction[Order] {
      var isrun: Boolean = true

      override def run(sourceContext: SourceFunction.SourceContext[Order]): Unit = {
        //    a. 使用 for 循环生成 1000 个订单
        for (i <- 0 until 1000 if isrun) {
          //      b. 随机生成订单 ID(UUID)
          //      c. 随机生成用户 ID(0-2)
          //      d. 随机生成订单金额(0-100)
          //      e. 时间戳为当前系统时间
          val order: Order = Order(UUID.randomUUID().toString, Random.nextInt(3), Random.nextInt(101), System.currentTimeMillis())
          //      f. 每隔 1 秒生成一个订单
          TimeUnit.SECONDS.sleep(1)
          sourceContext.collect(order)
        }
      }

      override def cancel(): Unit = {
        isrun = false
      }
    })
    //    6) 添加水印,允许延迟 2 秒
    val time = orderV.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[Order](Time.seconds(2)) {
      override def extractTimestamp(t: Order): Long = {
        val time = t.createTime
        time
      }
    })
    //    7) 导入 import org.apache.flink.table.api.scala._ 隐式参数
    import org.apache.flink.table.api.scala._
    //    8) 使用 registerDataStream 注册表,并分别指定字段,还要指定 rowtime 字段
    tale.registerDataStream("t_order", time, 'orderId, 'userId, 'money, 'createTime.rowtime)
    //    9) 编写 SQL 语句统计用户订单总数、最大金额、最小金额
    //    分组时要使用 tumble(时间列, interval '窗口时间' second) 来创建窗口
    var sql =
    """
      |select
      | userId,
      | sum(money) as totla,
      | max(money) as maxMoney,
      | min(money) as minMoney,
      | count(1) as countId
      |from t_order
      |group by userId ,tumble(createTime,interval '5' second)
      |""".stripMargin
    //    10) 使用 tableEnv.sqlQuery 执行 sql 语句
    val value = tale.sqlQuery(sql)
    //    11) 将 SQL 的执行结果转换成 DataStream 再打印出来
    val result = tale.toAppendStream[Row](value)
    result.print()
    //    12) 启动流处理程序
    envs.execute(this.getClass.getSimpleName)
  }
}