打酱油的后端
IP:上海
0关注数
0粉丝数
0获得的赞
工作年
编辑资料
链接我:

创作·63

全部
问答
动态
项目
学习
专栏
打酱油的后端

Flink系列之:大状态与 Checkpoint 调优

一、概述Flink 应用要想在大规模场景下可靠地运行,必须要满足如下两个条件:应用程序需要能够可靠地创建 checkpoints。在应用故障后,需要有足够的资源追赶数据输入流。第一部分讨论如何大规模获得良好性能的 checkpoints。 后一部分解释了一些关于要规划使用多少资源的最佳实践。二、监控状态和 Checkpoints监控 checkpoint 行为最简单的方法是通过 UI 的 checkpoint 部分。这两个指标(均通过 Task 级别 Checkpointing 指标 展示) 以及在 监控 Checkpoint)中,当看 checkpoint 详细信息时,特别有趣的是:算子收到第一个 checkpoint barrier 的时间。当触发 checkpoint 的耗费时间一直很高时,这意味着 checkpoint barrier 需要很长时间才能从 source 到达 operators。 这通常表明系统处于反压下运行。Alignment Duration,为处理第一个和最后一个 checkpoint barrier 之间的时间。在 unaligned checkpoints 下,exactly-once 和 at-least-once checkpoints 的 subtasks 处理来自上游 subtasks 的所有数据,且没有任何中断。 然而,对于 aligned exactly-once checkpoints,已经收到 checkpoint barrier 的通道被阻止继续发送数据,直到所有剩余的通道都赶上并接收它们的 checkpoint barrier(对齐时间)。理想情况下,这两个值都应该很低 - 较高的数值意味着 由于存在反压(没有足够的资源来处理传入的记录),导致checkpoint barriers 在作业中的移动速度较慢,这也可以通过处理记录的端到端延迟在增加来观察到。 请注意,在出现瞬态反压、数据倾斜或网络问题时,这些数值偶尔会很高。Unaligned checkpoints 可用于加快checkpoint barriers的传播。 但是请注意,这并不能解决导致反压的根本问题(端到端记录延迟仍然很高)。三、Checkpoint 调优应用程序可以配置定期触发 checkpoints。 当 checkpoint 完成时间超过 checkpoint 间隔时,在正在进行的 checkpoint 完成之前,不会触发下一个 checkpoint。默认情况下,一旦正在进行的 checkpoint 完成,将立即触发下一个 checkpoint。当 checkpoints 完成的时间经常超过 checkpoints 基本间隔时(例如,因为状态比计划的更大,或者访问 checkpoints 所在的存储系统暂时变慢), 系统不断地进行 checkpoints(一旦完成,新的 checkpoints 就会立即启动)。这可能意味着过多的资源被不断地束缚在 checkpointing 中,并且 checkpoint 算子进行得缓慢。 此行为对使用 checkpointed 状态的流式应用程序的影响较小,但仍可能对整体应用程序性能产生影响。为了防止这种情况,应用程序可以定义 checkpoints 之间的最小等待时间:StreamExecutionEnvironment.getCheckpointConfig().setMinPauseBetweenCheckpoints(milliseconds)此持续时间是指从最近一个 checkpoint 结束到下一个 checkpoint 开始之间必须经过的最小时间间隔。下图说明了这如何影响 checkpointing。注意: 可以配置应用程序(通过CheckpointConfig)允许同时进行多个 checkpoints。 对于 Flink 中状态较大的应用程序,这通常会使用过多的资源到 checkpointing。 当手动触发 savepoint 时,它可能与正在进行的 checkpoint 同时进行。四、RocksDB 调优许多大型 Flink 流应用程序的状态存储主要是 RocksDB State Backend。 该backend在主内存之上提供了很好的拓展能力,并且可靠地存储了大的 keyed stateRocksDB 的性能可能因配置而异,本节讲述了一些使用 RocksDB State Backend 调优作业的最佳实践。五、增量 Checkpoint在减少 checkpoints 花费的时间方面,开启增量 checkpoints 应该是首要考虑因素。 与完整 checkpoints 相比,增量 checkpoints 可以显着减少 checkpointing 时间,因为增量 checkpoints 仅存储与先前完成的 checkpoint 不同的增量文件,而不是存储全量数据备份。六、RocksDB 或 JVM 堆中的计时器计时器(Timer) 默认存储在 RocksDB 中,这是更健壮和可扩展的选择。当性能调优作业只有少量计时器(没有窗口,且在 ProcessFunction 中不使用计时器)时,将这些计时器放在堆中可以提高性能。 请谨慎使用此功能,因为基于堆的计时器可能会增加 checkpointing 时间,并且自然无法扩展到内存之外。七、RocksDB 内存调优RocksDB State Backend 的性能在很大程度上取决于它可用的内存量。为了提高性能,增加内存会有很大的帮助,或者调整内存的功能。 默认情况下,RocksDB State Backend 将 Flink 的托管内存用于 RocksDB 的缓冲区和缓存(State.Backend.RocksDB.memory.managed:true)尝试提高性能的第一步应该是增加托管内存的大小。这通常会大大改善这种情况,而不是通过调整 RocksDB 底层参数引入复杂性。 尤其是在容器、进程规模较大的情况下,除非应用程序本身逻辑需要大量的 JVM 堆,否则大部分总内存通常都可以用于 RocksDB 。默认的托管内存比例 (0.4) 是保守的,当 TaskManager 进程的内存为很多 GB 时,通常是可以增加该托管内存比例。在 RocksDB 中,写缓冲区的数量取决于应用程序中所拥有的状态数量(数据流中所有算子的状态)。每个状态对应一个列族(ColumnFamily),它需要自己写缓冲区。因此,具有多状态的应用程序通常需要更多的内存才能获得相同的性能。你可以尝试设置 state.backend.rocksdb.memory.managed: false 来使用列族(ColumnFamily)内存的 RocksDB 与使用托管内存的 RocksDB 的性能对比。特别是针对基准测试(假设没有或适当的容器内存限制)或回归测试 Flink 早期版本时,这可能会很有用。 与使用托管内存(固定内存池)相比,不使用托管内存意味着 RocksDB 分配的内存与应用程序中的状态数成比例(内存占用随应用程序的变化而变化)。根据经验,非托管模式(除非使用列族(ColumnFamily)RocksDB)的上限约为 “140MB * 跨所有 tasks 的状态 * slots 个数”。 计时器也算作状态!如果你的应用程序有许多状态,并且你看到频繁的 MemTable 刷新(写端瓶颈),但你不能提供更多的内存,你可以增加写缓冲区的内存比例(state.backend.rocksdb.memory.write-buffer-ratio)。一个高级选项(专家模式)是通过 RocksDBOptionFactory 来调整 RocksDB 的列族(ColumnFamily)选项(块大小、最大后台刷新线程等),以减少具有多种状态的 MemTable 刷新次数:public class MyOptionsFactory implements ConfigurableRocksDBOptionsFactory { @Override public DBOptions createDBOptions(DBOptions currentOptions, Collection<AutoCloseable> handlesToClose) { // increase the max background flush threads when we have many states in one operator, // which means we would have many column families in one DB instance. return currentOptions.setMaxBackgroundFlushes(4); } @Override public ColumnFamilyOptions createColumnOptions( ColumnFamilyOptions currentOptions, Collection<AutoCloseable> handlesToClose) { // decrease the arena block size from default 8MB to 1MB. return currentOptions.setArenaBlockSize(1024 * 1024); } @Override public OptionsFactory configure(ReadableConfig configuration) { return this; } }八、容量规划本节讨论如何确定 Flink 作业应该使用多少资源才能可靠地运行。 容量规划的基本经验法则是:应该有足够的资源保障正常运行时不出现反压 如何检查应用程序是否在反压下运行,详细信息请参阅 反压监控。在无故障时间内无反压运行程序所需的资源之上能够提供一些额外的资源。 需要这些资源来“追赶”在应用程序恢复期间积累的输入数据。 这通常取决于恢复操作需要多长时间(这取决于在故障恢复时需要加载到新 TaskManager 中的状态大小)以及故障恢复的速度。重要提示:基准点应该在开启 checkpointing 来建立,因为 checkpointing 会占用一些资源(例如网络带宽)。临时反压通常是允许的,在负载峰值、追赶阶段或外部系统(sink 到外部系统)出现临时减速时,这是执行流控制的重要部分。在某些操作下(如大窗口)会导致其下游算子的负载激增: 在有窗口的情况下,下游算子可能在构建窗口时几乎无事可做,而在触发窗口时有负载要做。 下游并行度的规划需要考虑窗口的输出量以及处理这种峰值的速度。重要提示:为了方便以后增加资源,请确保将流应用程序的最大并行度设置为一个合理的数字。最大并行度定义了当扩缩容程序时(通过 savepoint )可以设置程序并行度的上限。Flink 的内部以键组(key groups) 的最大并行度为粒度跟踪分布式状态。 Flink 的设计力求使最大并行度的值达到很高的效率,即使执行程序时并行度很低。九、压缩Flink 为所有 checkpoints 和 savepoints 提供可选的压缩(默认:关闭)。 目前,压缩总是使用 snappy 压缩算法(版本 1.1.10.x), 但我们计划在未来支持自定义压缩算法。 压缩作用于 keyed state 下 key-groups 的粒度,即每个 key-groups 可以单独解压缩,这对于重新缩放很重要。可以通过 ExecutionConfig 开启压缩:ExecutionConfig executionConfig = new ExecutionConfig(); executionConfig.setUseSnapshotCompression(true);压缩选项对增量快照没有影响,因为它们使用的是 RocksDB 的内部格式,该格式始终使用开箱即用的 snappy 压缩。十、Task 本地恢复问题引入在 Flink 的 checkpointing 中,每个 task 都会生成其状态快照,然后将其写入分布式存储。 每个 task 通过发送一个描述分布式存储中的位置状态的句柄,向 jobmanager 确认状态的成功写入。 JobManager 反过来收集所有 tasks 的句柄并将它们捆绑到一个 checkpoint 对象中。在恢复的情况下,jobmanager 打开最新的 checkpoint 对象并将句柄发送回相应的 tasks,然后可以从分布式存储中恢复它们的状态。 使用分布式存储来存储状态有两个重要的优势。 首先,存储是容错的,其次,分布式存储中的所有状态都可以被所有节点访问,并且可以很容易地重新分配(例如,用于重新扩缩容)。但是,使用远程分布式存储也有一个很大的缺点:所有 tasks 都必须通过网络从远程位置读取它们的状态。 在许多场景中,恢复可能会将失败的 tasks 重新调度到与前一次运行相同的 taskmanager 中(当然也有像机器故障这样的异常),但我们仍然必须读取远程状态。这可能导致大状态的长时间恢复,即使在一台机器上只有一个小故障。Task 本地状态恢复正是针对这个恢复时间长的问题,其主要思想如下:对于每个 checkpoint ,每个 task 不仅将 task 状态写入分布式存储中, 而且还在 task 本地存储(例如本地磁盘或内存)中保存状态快照的次要副本。请注意,快照的主存储仍然必须是分布式存储,因为本地存储不能确保节点故障下的持久性,也不能为其他节点提供重新分发状态的访问,所以这个功能仍然需要主副本。然而,对于每个 task 可以重新调度到以前的位置进行恢复的 task ,我们可以从次要本地状态副本恢复,并避免远程读取状态的成本。考虑到许多故障不是节点故障,即使节点故障通常一次只影响一个或非常少的节点, 在恢复过程中,大多数 task 很可能会重新部署到它们以前的位置,并发现它们的本地状态完好无损。这就是 task 本地恢复有效地减少恢复时间的原因。请注意,根据所选的 state backend 和 checkpointing 策略,在每个 checkpoint 创建和存储次要本地状态副本时,可能会有一些额外的成本。 例如,在大多数情况下,实现只是简单地将对分布式存储的写操作复制到本地文件。十一、主要(分布式存储)和次要(task 本地)状态快照的关系Task 本地状态始终被视为次要副本,checkpoint 状态始终以分布式存储中的副本为主。 这对 checkpointing 和恢复期间的本地状态问题有影响:对于 checkpointing ,主副本必须成功,并且生成次要本地副本的失败不会使 checkpoint 失败。 如果无法创建主副本,即使已成功创建次要副本,checkpoint 也会失败。只有主副本由 jobmanager 确认和管理,次要副本属于 taskmanager ,并且它们的生命周期可以独立于它们的主副本。 例如,可以保留 3 个最新 checkpoints 的历史记录作为主副本,并且只保留最新 checkpoint 的 task 本地状态。对于恢复,如果匹配的次要副本可用,Flink 将始终首先尝试从 task 本地状态恢复。 如果在次要副本恢复过程中出现任何问题,Flink 将透明地重试从主副本恢复 task。 仅当主副本和(可选)次要副本失败时,恢复才会失败。 在这种情况下,根据配置,Flink 仍可能回退到旧的 checkpoint。Task 本地副本可能仅包含完整 task 状态的一部分(例如,写入一个本地文件时出现异常)。 在这种情况下,Flink 会首先尝试在本地恢复本地部分,非本地状态从主副本恢复。 主状态必须始终是完整的,并且是 task 本地状态的超集。Task 本地状态可以具有与主状态不同的格式,它们不需要相同字节。 例如,task 本地状态甚至可能是在堆对象组成的内存中,而不是存储在任何文件中。如果 taskmanager 丢失,则其所有 task 的本地状态都会丢失。十二、配置 task 本地恢复Task 本地恢复 默认禁用,可以通过 Flink 的 CheckpointingOptions.LOCAL_RECOVERY 配置中指定的键 state.backend.local-recovery 来启用。 此设置的值可以是 true 以启用或 false(默认)以禁用本地恢复。注意,unaligned checkpoints 目前不支持 task 本地恢复。十三、不同 state backends 的 task 本地恢复的详细介绍限制:目前,task 本地恢复仅涵盖 keyed state backends。 Keyed state 通常是该状态的最大部分。 在不久的将来,我们还将支持算子状态和计时器(timers)。以下 state backends 可以支持 task 本地恢复。HashMapStateBackend: keyed state 支持 task 本地恢复。 该实现会将状态复制到本地文件。 这会引入额外的写入成本并占用本地磁盘空间。 将来,我们可能还会提供一种将 task 本地状态保存在内存中的实现。EmbeddedRocksDBStateBackend: 支持 keyed state 的 task 本地恢复。对于全量 checkpoints,状态被复制到本地文件。这会引入额外的写入成本并占用本地磁盘空间。对于增量快照,本地状态基于 RocksDB 的原生 checkpointing 机制。 这种机制也被用作创建主副本的第一步,这意味着在这种情况下,创建次要副本不会引入额外的成本。我们只是保留本地 checkpoint 目录, 而不是在上传到分布式存储后将其删除。这个本地副本可以与 RocksDB 的工作目录共享现有文件(通过硬链接),因此对于现有文件,增量快照的 task 本地恢复也不会消耗额外的磁盘空间。 使用硬链接还意味着 RocksDB 目录必须与所有可用于存储本地状态和本地恢复目录位于同一节点上,否则建立硬链接可能会失败(参见 FLINK-10954)。 目前,当 RocksDB 目录配置在多个物理设备上时,这也会阻止使用本地恢复。十四、Allocation-preserving 调度Task 本地恢复假设在故障下通过 allocation-preserving 调度 task ,其工作原理如下。 每个 task 都会记住其先前的分配,并请求完全相同的 slot 来重新启动恢复。 如果此 slot 不可用,task 将向 resourcemanager 请求一个 新的 slot。 这样,如果 taskmanager 不再可用,则无法返回其先前位置的 task 不会将其他正在恢复的 task 踢出其之前的 slot。 我们的理由是,只有当 taskmanager 不再可用时,前一个 slot 才会消失,在这种情况下,一些 tasks 无论如何都必须请求新的 slot 。 在我们的调度策略中,我们让绝大多数的 tasks 有机会从它们的本地状态中恢复,从而避免了从其他 tasks 处获取它们之前的 slots 的级联效应。
0
0
0
浏览量1795
打酱油的后端

Flink系列之:Savepoints

一、SavepointsSavepoint 是依据 Flink checkpointing 机制所创建的流作业执行状态的一致镜像。 你可以使用 Savepoint 进行 Flink 作业的停止与重启、fork 或者更新。 Savepoint 由两部分组成:稳定存储(列入 HDFS,S3,…) 上包含二进制文件的目录(通常很大),和元数据文件(相对较小)。 稳定存储上的文件表示作业执行状态的数据镜像。 Savepoint 的元数据文件以(相对路径)的形式包含(主要)指向作为 Savepoint 一部分的稳定存储上的所有文件的指针。二、分配算子ID强烈建议你按照本节所述调整你的程序,以便将来能够升级你的程序。主要通过 uid(String) 方法手动指定算子 ID 。这些 ID 将用于恢复每个算子的状态。DataStream<String> stream = env. // Stateful source (e.g. Kafka) with ID .addSource(new StatefulSource()) .uid("source-id") // ID for the source operator .shuffle() // Stateful mapper with ID .map(new StatefulMapper()) .uid("mapper-id") // ID for the mapper // Stateless printing sink .print(); // Auto-generated ID这段代码使用Apache Flink框架创建了一个名为stream的数据流(DataStream类型)。代码中的每一个操作都代表了流处理的一步。首先,代码通过调用addSource方法添加一个名为StatefulSource的有状态源。这个源可以是任何有状态的数据源,比如Kafka。uid(“source-id”)方法用于给源操作符指定一个唯一的ID。接下来,代码调用shuffle方法对流进行一次shuffle操作,该操作可用于重新分区数据。然后,代码调用map方法,使用一个名为StatefulMapper的有状态映射函数对流进行映射操作。同样地,uid(“mapper-id”)方法用于给映射操作符指定一个唯一的ID。最后,代码调用print方法通过无状态的打印sink将流的内容输出到控制台。这个操作会自动生成一个ID。整个代码段构建了一个有状态的数据流处理图,其中包含了源操作符、映射操作符和一个打印sink。该数据流会根据源生成的数据进行一系列的转换和处理,并将结果打印输出。如果不手动指定 ID ,则会自动生成 ID 。只要这些 ID 不变,就可以从 Savepoint 自动恢复。生成的 ID 取决于程序的结构,并且对程序更改很敏感。因此,强烈建议手动分配这些 ID 。三、Savepoint 状态可以将 Savepoint 想象为每个有状态的算子保存一个映射“算子 ID ->状态”:Operator ID | State ------------+------------------------ source-id | State of StatefulSource mapper-id | State of StatefulMapper在上面的示例中,print sink 是无状态的,因此不是 Savepoint 状态的一部分。默认情况下,我们尝试将 Savepoint 的每个条目映射回新程序。四、算子你可以使用命令行客户端来触发 Savepoint,触发 Savepoint 并取消作业,从 Savepoint 恢复,以及删除 Savepoint。从 Flink 1.2.0 开始,还可以使用 webui 从 Savepoint 恢复。五、触发Savepoint当触发 Savepoint 时,将创建一个新的 Savepoint 目录,其中存储数据和元数据。可以通过配置默认目标目录或使用触发器命令指定自定义目标目录(参见:targetDirectory参数来控制该目录的位置。注意: 目标目录必须是 JobManager(s) 和 TaskManager(s) 都可以访问的位置,例如分布式文件系统(或者对象存储系统)上的位置。以 FsStateBackend 或 RocksDBStateBackend 为例:# Savepoint 目标目录 /savepoint/ # Savepoint 目录 /savepoint/savepoint-:shortjobid-:savepointid/ # Savepoint 文件包含 Checkpoint元数据 /savepoint/savepoint-:shortjobid-:savepointid/_metadata # Savepoint 状态 /savepoint/savepoint-:shortjobid-:savepointid/...从 1.11.0 开始,你可以通过移动(拷贝)savepoint 目录到任意地方,然后再进行恢复。在如下两种情况中不支持 savepoint 目录的移动:1)如果启用了 entropy injection :这种情况下,savepoint 目录不包含所有的数据文件,因为注入的路径会分散在各个路径中。 由于缺乏一个共同的根目录,因此 savepoint 将包含绝对路径,从而导致无法支持 savepoint 目录的迁移。2)作业包含了 task-owned state(比如 GenericWriteAhreadLog sink)。和 savepoint 不同,checkpoint 不支持任意移动文件,因为 checkpoint 可能包含一些文件的绝对路径。如果你使用 MemoryStateBackend 的话,metadata 和 savepoint 的数据都会保存在 _metadata 文件中,因此不要因为没看到目录下没有数据文件而困惑。注意: 不建议移动或删除正在运行作业的最后一个 Savepoint ,因为这可能会干扰故障恢复。因此,Savepoint 对精确一次的接收器有副作用,为了确保精确一次的语义,如果在最后一个 Savepoint 之后没有 Checkpoint ,那么将使用 Savepoint 进行恢复。六、Savepoint 格式你可以在 savepoint 的两种二进制格式之间进行选择:标准格式 - 一种在所有 state backends 间统一的格式,允许你使用一种状态后端创建 savepoint 后,使用另一种状态后端恢复这个 savepoint。这是最稳定的格式,旨在与之前的版本、模式、修改等保持最大兼容性。原生格式 - 标准格式的缺点是它的创建和恢复速度通常很慢。原生格式以特定于使用的状态后端的格式创建快照(例如 RocksDB 的 SST 文件)。以原生格式创建 savepoint 的能力在 Flink 1.15 中引入,在那之前 savepoint 都是以标准格式创建的。七、触发 Savepoint$ bin/flink savepoint :jobId [:targetDirectory这将触发 ID 为 :jobId 的作业的 Savepoint,并返回创建的 Savepoint 路径。 你需要此路径来恢复和删除 Savepoint 。你也可以指定创建 Savepoint 的格式。如果没有指定,会采用标准格式创建 Savepoint。$ bin/flink savepoint --type [native/canonical] :jobId [:targetDirectory]八、使用 YARN 触发 Savepoint$ bin/flink savepoint :jobId [:targetDirectory] -yid :yarnAppId这将触发 ID 为 :jobId 和 YARN 应用程序 ID :yarnAppId 的作业的 Savepoint,并返回创建的 Savepoint 的路径。九、使用 Savepoint 停止作业$ bin/flink stop --type [native/canonical] --savepointPath [:targetDirectory] :jobId这将自动触发 ID 为 :jobid 的作业的 Savepoint,并停止该作业。此外,你可以指定一个目标文件系统目录来存储 Savepoint 。该目录需要能被 JobManager(s) 和 TaskManager(s) 访问。你也可以指定创建 Savepoint 的格式。如果没有指定,会采用标准格式创建 Savepoint。十、从 Savepoint 恢复$ bin/flink run -s :savepointPath [:runArgs]这将提交作业并指定要从中恢复的 Savepoint 。 你可以给出 Savepoint 目录或 _metadata 文件的路径。十一、跳过无法映射的状态恢复默认情况下,resume 操作将尝试将 Savepoint 的所有状态映射回你要还原的程序。 如果删除了运算符,则可以通过 --allowNonRestoredState(short:-n)选项跳过无法映射到新程序的状态:十二、Restore 模式Restore 模式 决定了在 restore 之后谁拥有Savepoint 或者 externalized checkpoint的文件的所有权。在这种语境下 Savepoint 和 externalized checkpoint 的行为相似。 这里我们将它们都称为“快照”,除非另有明确说明。如前所述,restore 模式决定了谁来接管我们从中恢复的快照文件的所有权。快照可被用户或者 Flink 自身拥有。如果快照归用户所有,Flink 不会删除其中的文件,而且 Flink 不能依赖该快照中文件的存在,因为它可能在 Flink 的控制之外被删除。每种 restore 模式都有特定的用途。尽管如此,我们仍然认为默认的 NO_CLAIM 模式在大多数情况下是一个很好的折中方案,因为它在提供明确的所有权归属的同时只给恢复后第一个 checkpoint 带来较小的代价。你可以通过如下方式指定 restore 模式:$ bin/flink run -s :savepointPath -restoreMode :mode -n [:runArgs]十三、NO_CLAIM (默认的)在 NO_CLAIM 模式下,Flink 不会接管快照的所有权。它会将快照的文件置于用户的控制之中,并且永远不会删除其中的任何文件。该模式下可以从同一个快照上启动多个作业。为保证 Flink 不会依赖于该快照的任何文件,它会强制第一个(成功的) checkpoint 为全量 checkpoint 而不是增量的。这仅对state.backend: rocksdb 有影响,因为其他 backend 总是创建全量 checkpoint。一旦第一个全量的 checkpoint 完成后,所有后续的 checkpoint 会照常创建。所以,一旦一个 checkpoint 成功制作,就可以删除原快照。在此之前不能删除原快照,因为没有任何完成的 checkpoint,Flink 会在故障时尝试从初始的快照恢复。十四、CLAIM另一个可选的模式是 CLAIM 模式。该模式下 Flink 将声称拥有快照的所有权,并且本质上将其作为 checkpoint 对待:控制其生命周期并且可能会在其永远不会被用于恢复的时候删除它。因此,手动删除快照和从同一个快照上启动两个作业都是不安全的。Flink 会保持配置数量的 checkpoint。注意:Retained checkpoints 被存储在 <checkpoint_dir>/<job_id>/chk- 这样的目录中。Flink 不会接管 <checkpoint_dir>/<job_id> 目录的所有权,而只会接管 chk- 的所有权。Flink 不会删除旧作业的目录。Native 格式支持增量的 RocksDB savepoints。对于这些 savepoints,Flink 将所有 SST 存储在 savepoints 目录中。这意味着这些 savepoints 是自包含和目录可移动的。然而,在 CLAIM 模式下恢复时,后续的 checkpoints 可能会复用一些 SST 文件,这反过来会阻止在 savepoints 被清理时删除 savepoints 目录。 Flink 之后运行期间可能会删除复用的SST 文件,但不会删除 savepoints 目录。因此,如果在 CLAIM 模式下恢复,Flink 可能会留下一个空的 savepoints 目录。十五、LEGACYLegacy 模式是 Flink 在 1.15 之前的工作方式。该模式下 Flink 永远不会删除初始恢复的 checkpoint。同时,用户也不清楚是否可以删除它。导致该的问题原因是, Flink 会在用来恢复的 checkpoint 之上创建增量的 checkpoint,因此后续的 checkpoint 都有可能会依赖于用于恢复的那个 checkpoint。总而言之,恢复的 checkpoint 的所有权没有明确的界定。十六、删除 Savepoint$ bin/flink savepoint -d :savepointPath这将删除存储在 :savepointPath 中的 Savepoint。请注意,还可以通过常规文件系统操作手动删除 Savepoint ,而不会影响其他 Savepoint 或 Checkpoint(请记住,每个 Savepoint 都是自包含的)。 在 Flink 1.2 之前,使用上面的 Savepoint 命令执行是一个更乏味的任务。十七、配置你可以通过 state.savepoints.dir 配置 savepoint 的默认目录。 触发 savepoint 时,将使用此目录来存储 savepoint。 你可以通过使用触发器命令指定自定义目标目录来覆盖缺省值# 默认 Savepoint 目标目录 state.savepoints.dir: hdfs:///flink/savepoints如果既未配置缺省值也未指定自定义目标目录,则触发 Savepoint 将失败。注意: 目标目录必须是 JobManager(s) 和 TaskManager(s) 可访问的位置,例如,分布式文件系统上的位置。
0
0
0
浏览量1807
打酱油的后端

Flink系列Table API和SQL之:开窗Over聚合

一、开窗(Over)聚合与标准SQL中还有另外一类比较特殊的聚合方式,可以针对每一行计算一个聚合值,比如说,可以以每一行数据为基准,计算它之前1小时内所有数据的平均值,也可以计算它之前10个数的平均值。就好像是在每一行上打开了一扇窗户、收集数据进行统计一样,这就是所谓的"开窗函数"。开窗函数的聚合与之前两种聚合有本质的不同:分组聚合、窗口TVF聚合都是多对一的关系,将数据分组之后每组只会得到一个聚合结果。开窗函数是对每行都要做一次开窗聚合,因此聚合之后表中的行数不会有任何减少,是一个多对多的关系。与标准SQL中一致,Flink SQL中的开窗函数也是通过OVER子句来实现的,所以有时开窗聚合也叫做OVER聚合(Over Aggregation)。基本语法如下:SELECT <聚合函数> OVER ( [PARTITION BY <字段1>[,<字段2>,...]] ORDER BY <时间属性字段> <开窗范围>), ... FROM ...这里OVER关键字前面是一个聚合函数,会应用在后面OVER定义的窗口上。在OVER子句中主要有以下几个部分:PARTITION BY(可选):用来指定分区的键key,类似于GROUP BY的分组,这部分是可选的ORDER BY:OVER窗口是基于当前行扩展出的一段数据范围,选择的标准可以基于时间也可以基于数量。不论哪种定义,数据都应该是以某种顺序排列好的,而表中的数据本身是无序的。所以在OVER子句中必须用ORDER BY明确地指出数据基于哪个字段排序。在Flink的流处理中,目前只支持按照时间属性的升序排列,所以这里ORDER BY后面的字段必须是定义好的时间属性。二、开窗范围对于开窗函数而言,还有一个必须要指定的就是开窗的范围,也就是到底要扩展多少行来做聚合。这个范围是由BETWEEN<下界> AND <上界> 来定义的,也就是"从下界到上界"的范围。目前支持的上界只能是CURRENT ROW,也就是定义一个从之前某一行到当前行的范围,所以一般的形式为:BETWEEN ... PRECEDING AND CURRENT ROW前面提到,开窗选择的范围可以基于时间,也可以基于数据的数量。所以开窗范围还应该在两种模式之间作出选择:范围间隔(RANGE intervals)和行间隔(ROW intervals)。范围间隔:范围间隔以RANGE为前缀,就是基于ORDER BY指定的时间字段去选择一个范围,一般就是当前行时间戳之前的一段时间,例如开窗范围选择当前行之前1小时的数据:RANGE BETWEENT INTERVAL '1' HOUR PRECEDING AND CURRENT ROW行间隔:行间隔以ROWS为前缀,就是直接确定要选多少行,由当前行出发向前选取就可以了,例如开窗范围选择当前行之前的5行数据(最终聚合会包括当前行,所以一共6条数据)ROWS BETWEEN 5 PRECEDING AND CURRENT ROW三、开窗(Over)聚合代码示例import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; /** * Copyright (c) 2020-2030 尚硅谷 All Rights Reserved * <p> * Project: FlinkTutorial * <p> * Created by wushengran */ public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 聚合查询转换 // 4. 开窗聚合 Table overWindowResultTable = tableEnv.sqlQuery("SELECT user_name, " + " avg(ts) OVER (" + " PARTITION BY user_name " + " ORDER BY et " + " ROWS BETWEEN 3 PRECEDING AND CURRENT ROW" + ") AS avg_ts " + "FROM clickTable"); // 结果表转换成流打印输出 tableEnv.toDataStream(overWindowResultTable).print("over window: "); env.execute(); } }over window: > +I[Mary, 1000] over window: > +I[Bob, 2000] over window: > +I[Alice, 3000] over window: > +I[Bob, 2500] over window: > +I[Alice, 3500] over window: > +I[Bob, 6000] over window: > +I[Alice, 10333]
0
0
0
浏览量2010
打酱油的后端

Flink系列之:Upsert Kafka SQL 连接器

一、Upsert Kafka SQL 连接器Scan Source: Unbounded 、Sink: Streaming Upsert ModeUpsert Kafka 连接器支持以 upsert 方式从 Kafka topic 中读取数据并将数据写入 Kafka topic。作为 source,upsert-kafka 连接器生产 changelog 流,其中每条数据记录代表一个更新或删除事件。更准确地说,数据记录中的 value 被解释为同一 key 的最后一个 value 的 UPDATE,如果有这个 key(如果不存在相应的 key,则该更新被视为 INSERT)。用表来类比,changelog 流中的数据记录被解释为 UPSERT,也称为 INSERT/UPDATE,因为任何具有相同 key 的现有行都被覆盖。另外,value 为空的消息将会被视作为 DELETE 消息。作为 sink,upsert-kafka 连接器可以消费 changelog 流。它会将 INSERT/UPDATE_AFTER 数据作为正常的 Kafka 消息写入,并将 DELETE 数据以 value 为空的 Kafka 消息写入(表示对应 key 的消息被删除)。Flink 将根据主键列的值对数据进行分区,从而保证主键上的消息有序,因此同一主键上的更新/删除消息将落在同一分区中。二、依赖 <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka</artifactId> <version>3.0.2-1.18</version> </dependency>三、完整示例下面的示例展示了如何创建和使用 Upsert Kafka 表:CREATE TABLE pageviews_per_region ( user_region STRING, pv BIGINT, uv BIGINT, PRIMARY KEY (user_region) NOT ENFORCED ) WITH ( 'connector' = 'upsert-kafka', 'topic' = 'pageviews_per_region', 'properties.bootstrap.servers' = '...', 'key.format' = 'avro', 'value.format' = 'avro' ); CREATE TABLE pageviews ( user_id BIGINT, page_id BIGINT, viewtime TIMESTAMP, user_region STRING, WATERMARK FOR viewtime AS viewtime - INTERVAL '2' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'pageviews', 'properties.bootstrap.servers' = '...', 'format' = 'json' ); -- 计算 pv、uv 并插入到 upsert-kafka sink INSERT INTO pageviews_per_region SELECT user_region, COUNT(*), COUNT(DISTINCT user_id) FROM pageviews GROUP BY user_region;确保在 DDL 中定义主键。这段代码是用来创建两个表,一个是"pageviews_per_region",另一个是"pageviews",并定义了它们的结构和连接器。"pageviews_per_region"表包含了三个字段:user_region(用户所在地区,字符串类型)、pv(页面访问量,长整型)和uv(独立访客量,长整型)。该表的主键为user_region,但不强制执行。"pageviews"表包含了四个字段:user_id(用户ID,长整型)、page_id(页面ID,长整型)、viewtime(访问时间,时间戳类型)和user_region(用户所在地区,字符串类型)。该表还定义了一个称为"viewtime"的水位线(watermark),它指定了在两秒之前的数据不再考虑为计算pv和uv。这两个表都使用了Kafka连接器来读写数据。'connector’属性指定了使用的连接器类型,'topic’属性指定了连接器读写的Kafka主题,'properties.bootstrap.servers’属性指定了Kafka集群的地址。对于"pageviews_per_region"表,'key.format’和’value.format’属性指定了数据的序列化格式为Avro。对于"pageviews"表,'format’属性指定了数据的序列化格式为JSON。最后,使用INSERT INTO语句,在"pageviews_per_region"表中计算出每个地区的pv和uv,并将结果插入到upsert-kafka sink中。总之,这段代码的作用是通过Kafka连接器创建两个表,并将"pageviews"表中的数据计算出每个地区的pv和uv,并插入到"pageviews_per_region"表中。四、可用元数据连接器参数五、键和值格式此连接器需要键和值格式,其中键字段源自 PRIMARY KEY 约束。以下示例显示如何指定和配置键和值格式。格式选项以“键”或“值”加上格式标识符作为前缀。CREATE TABLE KafkaTable ( `ts` TIMESTAMP(3) METADATA FROM 'timestamp', `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING, PRIMARY KEY (`user_id`) NOT ENFORCED ) WITH ( 'connector' = 'upsert-kafka', ... 'key.format' = 'json', 'key.json.ignore-parse-errors' = 'true', 'value.format' = 'json', 'value.json.fail-on-missing-field' = 'false', 'value.fields-include' = 'EXCEPT_KEY' )六、主键约束Upsert Kafka 始终以 upsert 方式工作,并且需要在 DDL 中定义主键。在具有相同主键值的消息按序存储在同一个分区的前提下,在 changelog source 定义主键意味着 在物化后的 changelog 上主键具有唯一性。定义的主键将决定哪些字段出现在 Kafka 消息的 key 中。七、一致性保证默认情况下,如果启用 checkpoint,Upsert Kafka sink 会保证至少一次将数据插入 Kafka topic。这意味着,Flink 可以将具有相同 key 的重复记录写入 Kafka topic。但由于该连接器以 upsert 的模式工作,该连接器作为 source 读入时,可以确保具有相同主键值下仅最后一条消息会生效。因此,upsert-kafka 连接器可以像 HBase sink 一样实现幂等写入。八、为每个分区生成相应的watermarkFlink 支持根据 Upsert Kafka 的 每个分区的数据特性发送相应的 watermark。当使用这个特性的时候,watermark 是在 Kafka consumer 内部生成的。 合并每个分区 生成的 watermark 的方式和 stream shuffle 的方式是一致的。 数据源产生的 watermark 是取决于该 consumer 负责的所有分区中当前最小的 watermark。如果该 consumer 负责的部分分区是 idle 的,那么整体的 watermark 并不会前进。在这种情况下,可以通过设置合适的 table.exec.source.idle-timeout 来缓解这个问题。九、数据类型映射Upsert Kafka 用字节存储消息的 key 和 value,因此没有 schema 或数据类型。消息按格式进行序列化和反序列化,例如:csv、json、avro。因此数据类型映射表由指定的格式确定。
0
0
0
浏览量1036
打酱油的后端

Flink系列之:Table API Connectors之Raw Format

一、Raw FormatRaw format 允许读写原始(基于字节)值作为单个列。注意: 这种格式将 null 值编码成 byte[] 类型的 null。这样在 upsert-kafka 中使用时可能会有限制,因为 upsert-kafka 将 null 值视为 墓碑消息(在键上删除)。因此,如果该字段可能具有 null 值,我们建议避免使用 upsert-kafka 连接器和 raw format 作为 value.format。Raw format 连接器是内置的。二、示例例如,你可能在 Kafka 中具有原始日志数据,并希望使用 Flink SQL 读取和分析此类数据。47.29.201.179 - - [28/Feb/2019:13:17:10 +0000] "GET /?p=1 HTTP/2.0" 200 5316 "https://domain.com/?p=1" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" "2.75"下面的代码创建了一张表,使用 raw format 以 UTF-8 编码的形式从中读取(也可以写入)底层的 Kafka topic 作为匿名字符串值:CREATE TABLE nginx_log ( log STRING ) WITH ( 'connector' = 'kafka', 'topic' = 'nginx_log', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'format' = 'raw' )然后,你可以将原始数据读取为纯字符串,之后使用用户自定义函数将其分为多个字段进行进一步分析。例如 示例中的 my_split。SELECT t.hostname, t.datetime, t.url, t.browser, ... FROM( SELECT my_split(log) as t FROM nginx_log );相对应的,你也可以将一个 STRING 类型的列以 UTF-8 编码的匿名字符串值写入 Kafka topic。三、Format 参数参数 是否必选 默认值类型描述format必选 (none)String指定要使用的格式, 这里应该是 ‘raw’。raw.charset可选 UTF-8String指定字符集来编码文本字符串。raw.endianness可选 big-endianString指定字节序来编码数字值的字节。有效值为’big-endian’和’little-endian’。四、数据类型映射下表详细说明了这种格式支持的 SQL 类型,包括用于编码和解码的序列化类和反序列化类的详细信息。
0
0
0
浏览量1976
打酱油的后端

Flink系列之:Top-N

一、TOP-N适用于流、批Top-N 查询可以根据指定列排序后获得前 N 个最小或最大值。最小值和最大值集都被认为是Top-N查询。在需要从批表或流表中仅显示 N 个底部或 N 个顶部记录时,Top-N 查询是非常有用的。并且该结果集还可用于进一步分析。Flink 使用 OVER 窗口子句和过滤条件的组合来表达一个 Top-N 查询。借助 OVER 窗口的 PARTITION BY 子句能力,Flink 也能支持分组 Top-N。例如:实时显示每个分类下销售额最高的五个产品。对于批处理和流处理模式的SQL,都支持 Top-N 查询。下面展示了 Top-N 的语法:SELECT [column_list] FROM ( SELECT [column_list], ROW_NUMBER() OVER ([PARTITION BY col1[, col2...]] ORDER BY col1 [asc|desc][, col2 [asc|desc]...]) AS rownum FROM table_name) WHERE rownum <= N [AND conditions]参数说明:ROW_NUMBER():根据分区数据的排序,为每一行分配一个唯一且连续的序号,从 1 开始。目前,只支持 ROW_NUMBER 作为 OVER 窗口函数。未来会支持 RANK() 和 DENSE_RANK()。PARTITION BY col1[, col2…]:指定分区字段。每个分区都会有一个 Top-N 的结果。ORDER BY col1 [asc|desc][, col2 [asc|desc]…]: 指定排序列。 每个列的排序类型(ASC/DESC)可以不同。WHERE rownum <= N: Flink 需要 rownum <= N 才能识别此查询是 Top-N 查询。 N 表示将要保留 N 个最大或最小数据。[AND conditions]: 可以在 WHERE 子句中添加其他条件,但是这些其他条件和 rownum <= N 需要使用 AND 结合。Top-N 查询是结果更新的. Flink SQL会根据ORDER BY的字段对输入的数据流进行排序,所以如果前 N 条记录发生了变化,那么变化后的记录将作为回撤/更新记录发送到下游。 建议使用一个支持更新的存储作为 Top-N 查询的结果表。此外,如果 Top-N 条记录需要存储在外部存储中,结果表应该与Top-N查询的唯一键保持一致。Top-N 查询的唯一键是分区字段和 rownum 字段的组合。Top-N 查询也可以获取上游的唯一键。用下面的 job 举例:比如 product_id 是 ShopSales 的唯一键,这时 Top-N 查询的唯一键是[category, rownum] 和 [product_id]。下面的示例展示了在流式表上指定 Top-N SQL 查询。这也是上面提到的 ‘实时显示每个分类下销售额最高的五个产品’ 的示例。CREATE TABLE ShopSales ( product_id STRING, category STRING, product_name STRING, sales BIGINT ) WITH (...); SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS row_num FROM ShopSales) WHERE row_num <= 5二、无排名输出优化如上所述, rownum 将作为唯一键的一个字段写入到结果表,这可能会导致大量数据写入到结果表。例如,排名第九(比如 product-1001)的记录更新为 1,排名 1 到 9 的所有记录都会作为更新信息逐条写入到结果表。如果结果表收到太多的数据,它将会成为这个 SQL 任务的瓶颈。优化的方法是在 Top-N 查询的外层 SELECT 子句中省略 rownum 字段。因为通常 Top-N 的数据量不大,消费端就可以快速地排序。下面的示例中就没有 rownum 字段,只需要发送变更数据(product-1001)到下游,这样可以减少结果表很多 IO。下面的示例展示了用这种方法怎样去优化上面的 Top-N:CREATE TABLE ShopSales ( product_id STRING, category STRING, product_name STRING, sales BIGINT ) WITH (...); -- omit row_num field from the output SELECT product_id, category, product_name, sales FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS row_num FROM ShopSales) WHERE row_num <= 5Attention in Streaming Mode 为了上面的查询输出到外部存储的正确性,外部存储必须和 Top-N 查询拥有相同的唯一键。在上面的示例中,如果 product_id 是查询的唯一键,外部表应该也把 product_id 作为唯一键。
0
0
0
浏览量1227
打酱油的后端

Flink系列之:Print SQL连接器

一、Print SQL连接器Print 连接器允许将每一行写入标准输出流或者标准错误流。设计目的:简单的流作业测试。对生产调试带来极大便利。四种 format 选项:打印内容条件1条件2标识符:任务 ID> 输出数据需要提供前缀打印标识符parallelism > 1标识符> 输出数据需要提供前缀打印标识符parallelism == 1任务 ID> 输出数据不需要提供前缀打印标识符parallelism > 1输出数据不需要提供前缀打印标识符parallelism == 1输出字符串格式为 “$row_kind(f0,f1,f2…)“,row_kind是一个 RowKind 类型的短字符串,例如:”+I(1,1)"。Print 连接器是内置的。注意 在任务运行时使用 Print Sinks 打印记录,你需要注意观察任务日志。二、创建一张基于Print的表CREATE TABLE print_table ( f0 INT, f1 INT, f2 STRING, f3 DOUBLE ) WITH ( 'connector' = 'print' )或者,也可以通过 LIKE子句 基于已有表的结构去创建新表。CREATE TABLE print_table WITH ('connector' = 'print') LIKE source_table (EXCLUDING ALL)三、连接器参数
0
0
0
浏览量1439
打酱油的后端

Flink系列之:基于scala语言实现flink实时消费Kafka Topic中的数据

Flink系列之:基于scala语言实现flink实时消费Kafka Topic中的数据一、引入flink相关依赖二、properties保存连接kafka的配置三、构建flink实时消费环境四、添加Kafka源和处理数据五、完整代码六、执行程序查看消费到的数据一、引入flink相关依赖 <groupId>com.bigdata</groupId> <artifactId>flink</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <flink.version>1.13.1</flink.version> <scala.binary.version>2.12</scala.binary.version> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-scala --> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-scala_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala --> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId> <version>${flink.version}</version> <!-- <scope>provided</scope>--> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients --> <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-connector-kafka_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> </dependencies>二、properties保存连接kafka的配置 //用properties保存kafka连接的相关配置 val properties = new Properties() properties.setProperty("bootstrap.servers","10.129.44.26:9092,10.129.44.32:9092,10.129.44.39:9092") properties.setProperty("sasl.jaas.config","org.apache.kafka.common.security.plain.PlainLoginModule required username=\"debezium\" password=\"swlfalfal\";") properties.setProperty("security.protocol","SASL_PLAINTEXT") properties.setProperty("sasl.mechanism", "PLAIN") properties.setProperty("group.id","flink-test") properties.setProperty("auto.offset.reset","earliest")三、构建flink实时消费环境 val env = StreamExecutionEnvironment.getExecutionEnvironment env.setParallelism(1) env.setRestartStrategy(RestartStrategies.noRestart())四、添加Kafka源和处理数据 val lines: DataStream[String] = env.addSource(new FlinkKafkaConsumer[String] ("debezium-test-optics_uds",new SimpleStringSchema(),properties)) lines.print() //触发执行 env.execute()五、完整代码import org.apache.flink.api.common.restartstrategy.RestartStrategies import org.apache.flink.api.common.serialization.SimpleStringSchema import org.apache.flink.streaming.api.scala._ import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer import java.util.Properties object SourceKafka { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment env.setParallelism(1) env.setRestartStrategy(RestartStrategies.noRestart()) //用properties保存kafka连接的相关配置 val properties = new Properties() properties.setProperty("bootstrap.servers","10.129.44.26:9092,10.129.44.32:9092,10.129.44.39:9092") properties.setProperty("sasl.jaas.config","org.apache.kafka.common.security.plain.PlainLoginModule required username=\"debezium\" password=\"******\";") properties.setProperty("security.protocol","SASL_PLAINTEXT") properties.setProperty("sasl.mechanism", "PLAIN") properties.setProperty("group.id","flink-test") properties.setProperty("auto.offset.reset","earliest") //添加kafka源,并打印数据 val lines: DataStream[String] = env.addSource(new FlinkKafkaConsumer[String] ("debezium-test-optics_uds",new SimpleStringSchema(),properties)) lines.print() //触发执行 env.execute() } }六、执行程序查看消费到的数据{ "schema":{ "type":"struct", "fields":[ { "type":"struct", "fields":[ { "type":"int32", "optional":false, "field":"sid" }, { "type":"string", "optional":false, "field":"sname" }, { "type":"int64", "optional":false, "name":"io.debezium.time.Timestamp", "version":1, "field":"updatetime" }, { "type":"string", "optional":false, "field":"ssex" } ], "optional":true, "name":"debezium_test_optics_uds.Value", "field":"before" }, { "type":"struct", "fields":[ { "type":"int32", "optional":false, "field":"sid" }, { "type":"string", "optional":false, "field":"sname" }, { "type":"int64", "optional":false, "name":"io.debezium.time.Timestamp", "version":1, "field":"updatetime" }, { "type":"string", "optional":false, "field":"ssex" } ], "optional":true, "name":"debezium_test_optics_uds.Value", "field":"after" }, { "type":"struct", "fields":[ { "type":"string", "optional":false, "field":"version" }, { "type":"string", "optional":false, "field":"connector" }, { "type":"string", "optional":false, "field":"name" }, { "type":"int64", "optional":false, "field":"ts_ms" }, { "type":"string", "optional":true, "name":"io.debezium.data.Enum", "version":1, "parameters":{ "allowed":"true,last,false,incremental" }, "default":"false", "field":"snapshot" }, { "type":"string", "optional":false, "field":"db" }, { "type":"string", "optional":true, "field":"sequence" }, { "type":"string", "optional":true, "field":"table" }, { "type":"int64", "optional":false, "field":"server_id" }, { "type":"string", "optional":true, "field":"gtid" }, { "type":"string", "optional":false, "field":"file" }, { "type":"int64", "optional":false, "field":"pos" }, { "type":"int32", "optional":false, "field":"row" }, { "type":"int64", "optional":true, "field":"thread" }, { "type":"string", "optional":true, "field":"query" } ], "optional":false, "name":"io.debezium.connector.mysql.Source", "field":"source" }, { "type":"string", "optional":false, "field":"op" }, { "type":"int64", "optional":true, "field":"ts_ms" }, { "type":"struct", "fields":[ { "type":"string", "optional":false, "field":"id" }, { "type":"int64", "optional":false, "field":"total_order" }, { "type":"int64", "optional":false, "field":"data_collection_order" } ], "optional":true, "field":"transaction" } ], "optional":false, "name":"debezium_test_optics_uds.Envelope" }, "payload":{ "before":null, "after":{ "sid":3600, "sname":"f", "updatetime":1661126400000, "ssex":"a" }, "source":{ "version":"1.9.6.Final", "connector":"mysql", "name":"debezium-uds8-optics8-test_1h", "ts_ms":1665155935000, "snapshot":"false", "db":"dw", "sequence":null, "table":"student", "server_id":223344, "gtid":null, "file":"mysql-bin.000012", "pos":6193972, "row":0, "thread":66072, "query":"/* ApplicationName=DBeaver 21.0.1 - SQLEditor <Script-3.sql> */ insert into dw.student values(3600,'f','20220822','a')" }, "op":"c", "ts_ms":1665155935640, "transaction":null } }
0
0
0
浏览量2017
打酱油的后端

Flink系列之:SQL提示

SQL 提示可以与 SQL 语句一起使用来更改执行计划。本章解释如何使用提示来强制执行各种方法。一般来说,提示可用于:执行计划器:没有完美的计划器,因此实现提示以让用户更好地控制执行是有意义的;附加元数据(或统计信息):一些统计信息,例如“扫描的表索引”和“某些洗牌键的倾斜信息”对于查询来说有些动态,使用提示配置它们会非常方便,因为我们来自规划器的规划元数据很多时候并不那么准确;算子资源限制:在很多情况下,我们会给执行算子一个默认的资源配置,即最小并行度或托管内存(资源消耗UDF)或特殊资源要求(GPU或SSD磁盘)等等,这将非常灵活使用每个查询的提示(而不是作业)来分析资源。一、动态表选项动态表选项允许动态指定或覆盖表选项,与使用 SQL DDL 或连接 API 定义的静态表选项不同,这些选项可以在每个查询的每个表范围内灵活指定。因此它非常适合用于交互式终端中的临时查询,例如,在 SQL-CLI 中,您可以通过添加动态选项 /*+ OPTIONS(’ 来指定忽略 CSV 源的解析错误csv.ignore-parse-errors’=‘true’) */。二、语法为了不破坏SQL兼容性,我们使用Oracle风格的SQL提示语法:table_path /*+ OPTIONS(key=val [, key=val]*) */ key: stringLiteral val: stringLiteral三、例子CREATE TABLE kafka_table1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE kafka_table2 (id BIGINT, name STRING, age INT) WITH (...); -- 覆盖查询源中的表选项 select id, name from kafka_table1 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */; -- 覆盖连接中的表选项 select * from kafka_table1 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */ t1 join kafka_table2 /*+ OPTIONS('scan.startup.mode'='earliest-offset') */ t2 on t1.id = t2.id; -- 覆盖 INSERT 目标表的表选项 insert into kafka_table1 /*+ OPTIONS('sink.partitioner'='round-robin') */ select * from kafka_table2;四、查询提示查询提示可用于建议优化器影响指定查询范围内的查询执行计划。它们的有效范围是指定查询提示的当前查询块(什么是查询块?)。现在,Flink Query Hints 仅支持 Join Hints。五、句法Flink 中的查询提示语法遵循 Apache Calcite 中的查询提示语法:# Query Hints: SELECT /*+ hint [, hint ] */ ... hint: hintName | hintName '(' optionKey '=' optionVal [, optionKey '=' optionVal ]* ')' | hintName '(' hintOption [, hintOption ]* ')' optionKey: simpleIdentifier | stringLiteral optionVal: stringLiteral hintOption: simpleIdentifier | numericLiteral | stringLiteral六、加入提示连接提示允许用户向优化器建议连接策略,以获得更高性能的执行计划。现在 Flink Join Hints 支持 BROADCAST、SHUFFLE_HASH、SHUFFLE_MERGE 和 NEST_LOOP。笔记:连接提示中指定的表必须存在。否则,将抛出表不存在错误。Flink Join Hints 只支持一个查询块中包含一个提示块,如果指定多个提示块,如 /*+ BROADCAST(t1) / /+ SHUFFLE_HASH(t1) */,则解析该查询语句时会抛出异常。在一个提示块中,在单个连接提示中指定多个表,例如 /*+ BROADCAST(t1, t2, …, tn) / 或指定多个连接提示,例如 /+ BROADCAST(t1), BROADCAST(t2), …、BROADCAST(tn) */ 均受支持。对于单个 Join Hints 中的多个表或一个hint 块中的多个 Join Hints,Flink Join Hints 可能会发生冲突。如果发生冲突,Flink 将选择最匹配的表或连接策略。 (参见:连接提示中的冲突案例)七、播送批BROADCAST 建议 Flink 使用 BroadCast join。无论table.optimizer.join.broadcast-threshold如何,带有hint的join端都会被广播,因此当表的hint端数据量很小时,它表现良好。注意:BROADCAST 仅支持等价连接条件的连接,不支持 Full Outer Join。例子:CREATE TABLE t1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t2 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t3 (id BIGINT, name STRING, age INT) WITH (...); -- Flink 将使用广播连接,t1 将是广播表。 SELECT /*+ BROADCAST(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- Flink 将使用广播连接来进行连接,并且 t1、t3 将是广播表。 SELECT /*+ BROADCAST(t1, t3) */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t1.id = t3.id; -- BROADCAST 不支持非等价连接条件。 -- Join Hint 将不起作用,并且只能应用嵌套循环连接。 SELECT /*+ BROADCAST(t1) */ * FROM t1 join t2 ON t1.id > t2.id; -- BROADCAST 不支持完全外连接。 -- 这种情况下Join Hint不起作用,规划器会根据成本选择合适的Join策略。 SELECT /*+ BROADCAST(t1) */ * FROM t1 FULL OUTER JOIN t2 ON t1.id = t2.id;八、随机散列批SHUFFLE_HASH建议Flink使用Shuffle Hash join。带hint的join端将是join构建端,当表的hint端数据量不太大时,它表现良好。注意:SHUFFLE_HASH 仅支持等价连接条件的连接。例子:CREATE TABLE t1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t2 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t3 (id BIGINT, name STRING, age INT) WITH (...); -- Flink 将使用哈希连接,t1 将作为构建端。 SELECT /*+ SHUFFLE_HASH(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- Flink 将使用 hash join 进行连接,t1、t3 将作为连接构建端。 SELECT /*+ SHUFFLE_HASH(t1, t3) */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t1.id = t3.id; -- SHUFFLE_HASH 不支持非等价连接条件。 -- 对于这种情况,Join Hint 将不起作用,只能应用嵌套循环连接。 SELECT /*+ SHUFFLE_HASH(t1) */ * FROM t1 join t2 ON t1.id > t2.id;九、随机合并批SHUFFLE_MERGE 建议 Flink 使用 Sort Merge join。这种类型的Join Hint推荐用于两个大表之间的Join场景或者Join两边的数据都已经有序的场景。注意:SHUFFLE_MERGE 仅支持具有等价连接条件的连接。例子:CREATE TABLE t1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t2 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t3 (id BIGINT, name STRING, age INT) WITH (...); -- 采用排序合并连接策略。 SELECT /*+ SHUFFLE_MERGE(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- 这两种连接均采用排序合并连接策略。 SELECT /*+ SHUFFLE_MERGE(t1, t3) */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t1.id = t3.id; -- SHUFFLE_MERGE don't support non-equivalent join conditions. -- Join Hint will not work, and only nested loop join can be applied. SELECT /*+ SHUFFLE_MERGE(t1) */ * FROM t1 join t2 ON t1.id > t2.id; – SHUFFLE_MERGE 不支持非等价连接条件。– Join Hint 将不起作用,并且只能应用嵌套循环连接。十、嵌套循环批NEST_LOOP 建议 Flink 使用 Nested Loop join。如果没有特殊场景需求,不建议使用此类连接提示。注意:NEST_LOOP 支持等效和非等效连接条件。例子:CREATE TABLE t1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t2 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t3 (id BIGINT, name STRING, age INT) WITH (...); -- Flink 将使用嵌套循环连接,t1 将作为构建端。 SELECT /*+ NEST_LOOP(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- Flink 将使用嵌套循环连接来进行连接,t1、t3 将是连接构建端。 SELECT /*+ NEST_LOOP(t1, t3) */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t1.id = t3.id;十一、LOOKUP流LOOKUP 提示允许用户建议 Flink 优化器:使用同步(sync)或异步(async)查找功能配置异步参数启用查找的延迟重试策略查找提示选项:笔记:'table’选项为必填项,仅支持表名(与FROM子句中的保持一致),注意如果表有别名,则只能使用别名。async选项都是可选的,如果没有配置将使用默认值。重试选项没有默认值,需要启用重试时,所有重试选项都应设置为有效值。1.使用同步和异步查找功能#如果连接器同时具有异步和同步查找功能,则用户可以指定选项值“async”=“false”以建议规划器使用同步查找或“async”=“true”以使用异步查找:        例子:-- suggest the optimizer to use sync lookup LOOKUP('table'='Customers', 'async'='false') -- suggest the optimizer to use async lookup LOOKUP('table'='Customers', 'async'='true')注意:如果未指定“async”选项,优化器更喜欢异步查找,在以下情况下它将始终使用同步查找:连接器仅实现同步查找用户启用“table.optimizer.non-definistic-update.strategy”的“TRY_RESOLVE”模式,并且优化器已检查是否存在由非确定性更新引起的正确性问题。配置异步参数 #用户可以通过异步查找模式下的异步选项配置异步参数。例子:-- configure the async parameters: 'output-mode', 'capacity', 'timeout', can set single one or multi params LOOKUP('table'='Customers', 'async'='true', 'output-mode'='allow_unordered', 'capacity'='100', 'timeout'='180s') 注意:异步选项与作业级别执行选项中的异步选项一致,如果不设置将使用作业级别配置。另一个区别是LOOKUP提示的范围更小,仅限于当前查找操作中设置的提示选项对应的表名(其他查找操作不会受到LOOKUP提示的影响)。例如,如果作业级别配置是:table.exec.async-lookup.output-mode: ORDERED table.exec.async-lookup.buffer-capacity: 100 table.exec.async-lookup.timeout: 180s然后出现以下提示:1. LOOKUP('table'='Customers', 'async'='true', 'output-mode'='allow_unordered') 2. LOOKUP('table'='Customers', 'async'='true', 'timeout'='300s')相当于:3. LOOKUP('table'='Customers', 'async'='true', 'output-mode'='allow_unordered', 'capacity'='100', 'timeout'='180s') 4. LOOKUP('table'='Customers', 'async'='true', 'output-mode'='ordered', 'capacity'='100', 'timeout'='300s')启用查找延迟重试策略 #查找连接的延迟重试旨在解决外部系统延迟更新导致流数据意外丰富的问题​​。提示选项’retry-predicate’='lookup_miss’可以启用同步和异步查找重试,目前仅支持固定延迟重试策略。固定延迟重试策略选项:'retry-strategy'='fixed_delay' -- fixed delay duration 'fixed-delay'='10s' -- max number of retry(counting from the retry operation, if set to '1', then a single lookup process -- for a specific lookup key will actually execute up to 2 lookup requests) 'max-attempts'='3'例子:1.启用异步查找重试LOOKUP('table'='Customers', 'async'='true', 'retry-predicate'='lookup_miss', 'retry-strategy'='fixed_delay', 'fixed-delay'='10s','max-attempts'='3')2.启用同步查找重试LOOKUP('table'='Customers', 'async'='false', 'retry-predicate'='lookup_miss', 'retry-strategy'='fixed_delay', 'fixed-delay'='10s','max-attempts'='3')如果查找源只有一种功能,则可以省略“异步”模式选项:LOOKUP('table'='Customers', 'retry-predicate'='lookup_miss', 'retry-strategy'='fixed_delay', 'fixed-delay'='10s','max-attempts'='3')十二、进一步说明启用缓存对重试的影响FLIP-221 增加了对查找源的缓存支持,有 PARTIAL 和 FULL 缓存模式(NONE     模式表示禁用缓存)。当启用完整缓存时,根本不会重试(因为通过查找源的完整缓存镜像重试查找是没有意义的)。当启用 PARTIAL 缓存时,它将首先从本地缓存查找即将到来的记录,如果缓存未命中,将通过后端连接器进行外部查找(如果缓存命中,则立即返回记录),这将在查找结果时触发重试为空(与禁用缓存相同),重试完成时确定最终查找结果(在PARTIAL缓存模式下,也会更新本地缓存)。关于查找键和‘retry-predicate’=‘lookup_miss’重试条件的注意事项对于不同的连接器,索引查找能力可能会有所不同,例如内置的HBase连接器只能在rowkey上查找(没有二级索引),而内置的JDBC连接器可以提供更强大的任意列的索引查找能力,这是由不同的连接器决定的。物理存储。这里提到的查找键是索引查找的字段或字段组合,以查找连接为例,其中c.id是连接条件“ON o.customer_id = c.id”的查找键:SELECT o.order_id, o.total, c.country, c.zip FROM Orders AS o JOIN Customers FOR SYSTEM_TIME AS OF o.proc_time AS c ON o.customer_id = c.id如果我们将连接条件更改为“ON o.customer_id = c.id and c.country = ‘US’”:SELECT o.order_id, o.total, c.country, c.zip FROM Orders AS o JOIN Customers FOR SYSTEM_TIME AS OF o.proc_time AS c ON o.customer_id = c.id and c.country = 'US'当 Customers 表存储在 MySql 中时,c.id 和 c.country 都将用作查找键:CREATE TEMPORARY TABLE Customers ( id INT, name STRING, country STRING, zip STRING ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://mysqlhost:3306/customerdb', 'table-name' = 'customers' )当Customers表存储在HBase中时,只有c.id可以作为查找键,其余连接条件c.country = 'US’将在查找结果返回后进行评估CREATE TEMPORARY TABLE Customers ( id INT, name STRING, country STRING, zip STRING, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'hbase-2.2', ... )因此,当启用“lookup_miss”重试谓词和固定延迟重试策略时,上述查询将对不同存储产生不同的重试效果。例如,如果 Customers 表中有一行:id=100, country='CN'在处理订单流中id=100的记录时,在jdbc连接器中,对应的查找结果为空(country='CN’不满足条件c.country = ‘US’),因为c.country=‘US’。 id 和 c.country 用作查找键,因此这将触发重试。当在’hbase’连接器中时,仅使用c.id作为查找键,相应的查找结果不会为空(会返回记录id=100,country=‘CN’),因此不会触发重试(对于返回的记录,剩余的连接条件 c.country = ‘US’ 将被评估为不正确)。目前基于SQL语义考虑,只提供了lookup_miss重试谓词,当需要等待维度表的延迟更新时(表中已经存在而不是不存在历史版本记录),用户可以使用可以尝试两种解决方案:使用 DataStream Async I/O 中新的重试支持实现自定义重试谓词(允许对返回的记录进行更复杂的判断)。通过添加另一个连接条件(包括比较由时间戳生成的某种数据版本)来启用延迟重试,对于上面的示例,假设 Customers 表每小时更新一次,我们可以添加一个新的与时间相关的版本字段 update_version,该字段保留为每小时精度,例如,记录的更新时间“2022-08-15 12:01:02”会将 update_version 存储为“2022-08-15 12:00”CREATE TEMPORARY TABLE Customers ( id INT, name STRING, country STRING, zip STRING, -- the newly added time-dependent version field update_version STRING ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://mysqlhost:3306/customerdb', 'table-name' = 'customers' )将 Order 流的时间字段和 Customers.update_version 上的相等条件附加到连接条件:ON o.customer_id = c.id AND DATE_FORMAT(o.order_timestamp, 'yyyy-MM-dd HH:mm') = c.update_version那么当Order的记录无法在Customers表中查找到‘12:00’版本的新记录时,我们可以启用延迟重试。十三、故障排除当开启延迟重试查找时,查找节点更有可能遇到背压问题,这可以通过Web ui“任务管理器”页面上的“线程转储”快速确认。分别从异步和同步查找中,线程睡眠的调用堆栈将出现在:异步查找:RetryableAsyncLookupFunctionDelegator同步查找:RetryableLookupFunctionDelegator笔记:带重试的异步查找无法对所有输入数据进行固定延迟处理(应使用其他较轻的方法来解决,例如挂起的源消耗或使用带重试的同步查找)同步查找中延迟等待重试执行是完全同步的,即,直到当前记录完成后才开始处理下一条记录。在异步查找中,如果“output-mode”为“ORDERED”模式,则延迟重试导致背压的概率可能高于“UNORDERED”模式,在这种情况下,增加异步“容量”可能无法有效减少背压,并且可能有必要考虑缩短延迟时间。十四、连接提示中的冲突案例如果 Join Hints 发生冲突,Flink 会选择最匹配的一个。如果同一个Join Hint策略发生冲突,Flink会选择第一个匹配的表进行Join。当不同的Join Hints策略发生冲突时,Flink会选择第一个匹配的hint进行Join。例子:CREATE TABLE t1 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t2 (id BIGINT, name STRING, age INT) WITH (...); CREATE TABLE t3 (id BIGINT, name STRING, age INT) WITH (...); -- Conflict in One Same Join Hints Strategy Case -- The first hint will be chosen, t2 will be the broadcast table. SELECT /*+ BROADCAST(t2), BROADCAST(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- BROADCAST(t2, t1) will be chosen, and t2 will be the broadcast table. SELECT /*+ BROADCAST(t2, t1), BROADCAST(t1, t2) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- This case equals to BROADCAST(t1, t2) + BROADCAST(t3), -- when join between t1 and t2, t1 will be the broadcast table, -- when join between the result after t1 join t2 and t3, t3 will be the broadcast table. SELECT /*+ BROADCAST(t1, t2, t3) */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t1.id = t3.id; -- Conflict in Different Join Hints Strategies Case -- The first Join Hint (BROADCAST(t1)) will be chosen, and t1 will be the broadcast table. SELECT /*+ BROADCAST(t1) SHUFFLE_HASH(t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -- Although BROADCAST is first one hint, but it doesn't support full outer join, -- so the following SHUFFLE_HASH(t1) will be chosen, and t1 will be the join build side. SELECT /*+ BROADCAST(t1) SHUFFLE_HASH(t1) */ * FROM t1 FULL OUTER JOIN t2 ON t1.id = t2.id; -- Although there are two Join Hints were defined, but all of them are neither support non-equivalent join, -- so only nested loop join can be applied. SELECT /*+ BROADCAST(t1) SHUFFLE_HASH(t1) */ * FROM t1 FULL OUTER JOIN t2 ON t1.id > t2.id;十五、什么是查询块查询块是SQL的基本单元。例如,任何内联视图或 SQL 语句的子查询都被视为与外部查询不同的查询块。例子:一条 SQL 语句可以由多个子查询组成。子查询可以是 SELECT、INSERT 或 DELETE。子查询可以在 FROM 子句、WHERE 子句或 UNION 或 UNION ALL 的子查询中包含其他子查询。对于这些不同的子查询或视图类型,它们可以由多个查询块组成,例如:下面的简单查询只有一个子查询,但它有两个查询块 - 一个用于外部 SELECT,另一个用于子查询 SELECT。下面的查询是一个联合查询,它包含两个查询块 - 一个用于第一个 SELECT,另一个用于第二个 SELECT。下面的查询包含一个视图,它有两个查询块 - 一个用于外部 SELECT,另一个用于视图。
0
0
0
浏览量121
打酱油的后端

Flink系列Table API和SQL之:联结查询

一、常规联结查询常规联结是SQL中原生定义的Join方式,是最通用的一类联结操作。具体语法与标准SQL的联结完全相同,通过关键字JOIN来联结两个表,后面用关键字ON来指明联结条件。在两个动态表的联结中,任何一侧表的插入(INSERT)或更改(UPDATE)操作都会让联结的结果发生改变。例如,如果左侧有新数据到来,会与右侧表中所有之前的数据进行联结合并,右侧表之后到来的新数据也会与这条数据连接合并。所以,常规联结查询一般是更新Update查询。与标准SQL一致,Flink SQL的常规联结也可以分为内联结(INNER JOIN)和外联结(OUTER JOIN),区别在于结果中是否包含不符合联结条件的行。目前仅支持"等值条件"作为联结条件,也就是关键字ON后面必须是判断两表中字段相等的逻辑表达式。1.等值内联结内联结用INNER JOIN来定义,会返回两表中符合联结条件的所有行的组合,也就是所谓的笛卡尔积。目前仅支持等值联结条件。例如之前提到的订单表(定义为Order)和商品表(定义为Product)的联结查询,就可以用以下SQL实现:SELECT * FROM Order INNER JOIN Product ON Order.product_id = Product.id这是一个内联结,联结条件是订单数据的product_id和商品数据的id相等。由于订单表中出现的商品id一定会在商品表中出现,因此这样的到的联结结果表,就包含了订单表Order中所有订单数据对应的详细信息。2.等值外联结外联结返回符合联结条件的所有行的笛卡尔积。还可以将某一侧标中找不到任何匹配的行也单独返回。Flink SQL支持左外(LEFT JOIN)、右外(RIGHT JOIN)和全外(FULL OUTER JOIN),分别表示会将左侧表、右侧表以及双侧表中没有任何匹配的行返回。具体用法如下:SELECT * FROM Order LEFT JOIN Product ON Order.product_id = Product.id二、间隔联结查询DataStream API中的双流Join,包括窗口联结(window join)和间隔联结(interval join)。两条流的Join就对应着SQL中两个表的Join,这是流处理中特有的联结方式。间隔联结返回的,同样是符合约束条件的两条数据的笛卡尔积。只不过这里的约束条件除了常规的联结条件外,还多了一个时间间隔的限制。具体语法有以下要点:两表的联结:间隔联结不需要用JOIN关键字,直接在FROM后将要联结的两表列出来就可以,用逗号分隔。这与标准SQL中的语法一致,表示一个交叉联结,会返回两表中所有行的笛卡尔积。联结条件:联结条件用WHERE子句来定义,用一个等值表达式描述。交叉联结之后再用WHERE进行条件筛选,效果跟内联结INNER JOIN…ON…非常类似。时间间隔限制:可以在WHERE子句中,联结条件后用AND追加一个时间间隔的限制条件:做法是提取左右两侧表中的时间字段,然后用一个表达式来指明两者需要满足的间隔限制。具体定义方式有下面三种,这里分别用ltime和rtime表示左右表中的时间字段: ltime=rtime ltime>=rtime AND ltime<rtime+INTERVAL. ‘10’ MINUTE ltime BETWEEN rtime - INTERVAL ‘10’ SECOND AND rtime + INTERVAL ‘5’ SECOND 判断两者相等,这是最强的时间约束,要求两表中数据的时间必须完全一致才能匹配。一般情况下,还是会放宽一些,给出一个间隔。间隔的定义可以用<,<=,>=,>这一类的关系不等式,也可以用BETWEEN … AND …这样的表达式。例如:现在除了订单表Order外,还有一个发货表Shipment,要求在收到订单后四个小时内发货,就可以用一个间隔联结查询,把所以订单与它对应的发货信息连接合并在一起返回:SELECT * FROM Order o ,Shipment s WHERE o.id = s.order_id AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time在流处理中,间隔联结查询只支持具有时间属性的仅追加表。除了间隔联结之外,Flink SQL还支持时间联结(Temporal Join),这主意是针对版本表而言的。所谓版本表,就是记录了数据随着时间推移版本变化的表,可以理解成一个更新日志,就是具有时间属性、还会进行更新操作的表。当我们联结某个版本表时,并不是把当前的数据连接合并起来就行了,而是希望能够根据数据发生的时间,找到当时的版本。这种根据更新时间提取当时的值进行联结的操作,就叫作"时间联结"。
0
0
0
浏览量1066
打酱油的后端

Flink系列之:ORDER BY语句和LIMIT语句

一、ORDER BY语句适用于流、批一体ORDER BY 子句使结果行根据指定的表达式进行排序。 如果两行根据最左边的表达式相等,则根据下一个表达式进行比较,依此类推。 如果根据所有指定的表达式它们相等,则它们以与实现相关的顺序返回。在流模式下运行时,表的主要排序顺序必须按时间属性升序。 所有后续的 orders 都可以自由选择。 但是批处理模式没有这个限制。SELECT * FROM Orders ORDER BY order_time, order_id二、LIMIT语句适用于批LIMIT 子句限制 SELECT 语句返回的行数。 通常,此子句与 ORDER BY 结合使用,以确保结果是确定性的。以下示例选择 Orders 表中的前 3 行。SELECT * FROM Orders ORDER BY orderTime LIMIT 3 Back to top
0
0
0
浏览量572
打酱油的后端

Flink系列之:分组聚合

像大多数数据系统一样,Apache Flink支持聚合函数;包括内置的和用户定义的。用户自定义函数在使用前必须在目录中注册。聚合函数把多行输入数据计算为一行结果。例如,有一些聚合函数可以计算一组行的 “COUNT”、“SUM”、“AVG”(平均)、“MAX”(最大)和 “MIN”(最小)。SELECT COUNT(*) FROM Orders对于流式查询,重要的是要理解 Flink 运行的是连续查询,永远不会终止。而且它们会根据其输入表的更新来更新其结果表。对于上述查询,每当有新行插入 Orders 表时,Flink 都会实时计算并输出更新后的结果。Apache Flink 支持标准的 GROUP BY 子句来聚合数据。SELECT COUNT(*) FROM Orders GROUP BY order_id对于流式查询,用于计算查询结果的状态可能无限膨胀。状态的大小取决于分组的数量以及聚合函数的数量和类型。例如:MIN/MAX 的状态是重量级的,COUNT 是轻量级的。可以提供一个合适的状态 time-to-live (TTL) 配置来防止状态过大。注意:这可能会影响查询结果的正确性。Flink 对于分组聚合提供了一系列性能优化的方法。一、DISTINCT 聚合DISTINCT 聚合在聚合函数前去掉重复的数据。下面的示例计算 Orders 表中不同 order_ids 的数量,而不是总行数。SELECT COUNT(DISTINCT order_id) FROM Orders对于流式查询,用于计算查询结果的状态可能无限膨胀。状态的大小大多数情况下取决于去重行的数量和分组持续的时间,持续时间较短的 group 窗口不会产生状态过大的问题。可以提供一个合适的状态 time-to-live (TTL) 配置来防止状态过大。注意:这可能会影响查询结果的正确性。二、GROUPING SETSGrouping Sets 可以通过一个标准的 GROUP BY 语句来描述更复杂的分组操作。数据按每个指定的 Grouping Sets 分别分组,并像简单的 group by 子句一样为每个组进行聚合。SELECT supplier_id, rating, COUNT(*) AS total FROM (VALUES ('supplier1', 'product1', 4), ('supplier1', 'product2', 3), ('supplier2', 'product3', 3), ('supplier2', 'product4', 4)) AS Products(supplier_id, product_id, rating) GROUP BY GROUPING SETS ((supplier_id, rating), (supplier_id), ())这个Flink SQL查询的目标是,基于给定的产品评分数据,计算每个供应商的评分总数。首先,我们定义了一个包含供应商ID、产品ID和评分的VALUES子句,表示我们的原始数据。每个元组代表了一个产品的供应商、产品和评分。然后,我们使用AS关键字给VALUES子句指定别名为Products,并指定了三个列名:supplier_id、product_id和rating。接下来,我们使用GROUP BY子句和GROUPING SETS关键字来分组数据。GROUP BY子句定义了我们想要按照哪些列进行分组。在这个查询中,我们定义了三个分组集合:(supplier_id, rating)、(supplier_id)和()。它们分别表示按照supplier_id和rating分组、只按照supplier_id分组以及不进行任何分组。最后,我们使用COUNT(*)函数来计算每个分组的产品评分总数,并将结果作为"total"列返回。这个查询的结果将为每个供应商和评分组合提供评分总数,以及每个供应商的总评分数和所有供应商的总评分结果:+-------------+--------+-------+ | supplier_id | rating | total | +-------------+--------+-------+ | supplier1 | 4 | 1 | | supplier1 | (NULL) | 2 | | (NULL) | (NULL) | 4 | | supplier1 | 3 | 1 | | supplier2 | 3 | 1 | | supplier2 | (NULL) | 2 | | supplier2 | 4 | 1 | +-------------+--------+-------+GROUPING SETS 的每个子列表可以是:空的,多列或表达式,它们的解释方式和直接使用 GROUP BY 子句是一样的。一个空的 Grouping Sets 表示所有行都聚合在一个分组下,即使没有数据,也会输出结果。对于 Grouping Sets 中的空子列表,结果数据中的分组或表达式列会用NULL代替。例如,上例中的 GROUPING SETS ((supplier_id), ()) 里的 () 就是空子列表,与其对应的结果数据中的 supplier_id 列使用 NULL 填充。对于流式查询,用于计算查询结果的状态可能无限膨胀。状态的大小取决于 Grouping Sets 的数量以及聚合函数的类型。可以提供一个合适的状态 time-to-live (TTL)配置来防止状态过大.注意:这可能会影响查询结果的正确性。三、ROLLUPROLLUP 是一种特定通用类型 Grouping Sets 的简写。代表着指定表达式和所有前缀的列表,包括空列表。例如:下面这个查询和上个例子是等效的。SELECT supplier_id, rating, COUNT(*) FROM (VALUES ('supplier1', 'product1', 4), ('supplier1', 'product2', 3), ('supplier2', 'product3', 3), ('supplier2', 'product4', 4)) AS Products(supplier_id, product_id, rating) GROUP BY ROLLUP (supplier_id, rating)这个Flink SQL查询的目标是,基于给定的产品评分数据,计算每个供应商和评分组合的评分总数。首先,我们定义了一个包含供应商ID、产品ID和评分的VALUES子句,表示我们的原始数据。每个元组代表了一个产品的供应商、产品和评分。然后,我们使用AS关键字给VALUES子句指定别名为Products,并指定了三个列名:supplier_id、product_id和rating。接下来,我们使用GROUP BY子句和ROLLUP关键字来进行分组。ROLLUP允许我们构造多个层次的大汇总。在这个查询中,我们使用ROLLUP(supplier_id, rating)来创建了两个层次的分组:一个按供应商ID和评分进行分组的层次,以及一个只按供应商ID进行分组的层次。最后,我们使用COUNT()函数来计算每个分组的产品评分总数,并返回结果中的"supplier_id"、"rating"和"COUNT()"三列。这个查询的结果将为每个供应商和评分组合提供评分总数,以及每个供应商在不同评分水平上的总评分数。同时,结果还包括以评分水平为基础的总评分数和所有供应商的总评分数。四、CUBECUBE 是一种特定通用类型 Grouping Sets 的简写。代表着指定列表以及所有可能的子集和幂集。例如:下面两个查询是等效的。SELECT supplier_id, rating, product_id, COUNT(*) FROM (VALUES ('supplier1', 'product1', 4), ('supplier1', 'product2', 3), ('supplier2', 'product3', 3), ('supplier2', 'product4', 4)) AS Products(supplier_id, product_id, rating) GROUP BY CUBE (supplier_id, rating, product_id) SELECT supplier_id, rating, product_id, COUNT(*) FROM (VALUES ('supplier1', 'product1', 4), ('supplier1', 'product2', 3), ('supplier2', 'product3', 3), ('supplier2', 'product4', 4)) AS Products(supplier_id, product_id, rating) GROUP BY GROUPING SET ( ( supplier_id, product_id, rating ), ( supplier_id, product_id ), ( supplier_id, rating ), ( supplier_id ), ( product_id, rating ), ( product_id ), ( rating ), ( ) )这个Flink SQL查询的目标是,基于给定的产品评分数据,计算每个供应商、评分和产品ID组合的评分总数。首先,我们定义了一个包含供应商ID、产品ID和评分的VALUES子句,表示我们的原始数据。每个元组代表了一个产品的供应商、产品和评分。然后,我们使用AS关键字给VALUES子句指定别名为Products,并指定了三个列名:supplier_id、product_id和rating。接下来,我们使用GROUP BY子句和CUBE关键字来进行分组。CUBE允许我们构造所有可能的组合。在这个查询中,我们使用CUBE(supplier_id, rating, product_id)来创建了所有可能的组合:按供应商ID、评分和产品ID进行分组的组合、只按供应商ID和评分进行分组的组合、只按供应商ID和产品ID进行分组的组合、只按评分和产品ID进行分组的组合,以及只按供应商ID进行分组的组合,只按评分进行分组的组合,只按产品ID进行分组的组合,以及不进行任何分组的组合。最后,我们使用COUNT()函数来计算每个分组的产品评分总数,并返回结果中的"supplier_id"、“rating”、"product_id"和"COUNT()"四列。这个查询的结果将为每个供应商、评分和产品ID组合提供评分总数,以及不同组合下的总评分数。同时,结果还包括每个供应商、每个评分和每个产品ID的总评分数,以及所有供应商、所有评分和所有产品ID的总评分数。五、HAVINGHAVING 会删除 group 后不符合条件的行。 HAVING 和 WHERE 的不同点:WHERE 在 GROUP BY 之前过滤单独的数据行。HAVING 过滤 GROUP BY 生成的数据行。 HAVING 条件中的每一列引用必须是明确的 grouping 列,除非它出现在聚合函数中。SELECT SUM(amount) FROM Orders GROUP BY users HAVING SUM(amount) > 50即使没有 GROUP BY 子句,HAVING 的存在也会使查询变成一个分组查询。这与查询包含聚合函数但没有 GROUP BY 子句时的情况相同。查询认为所有被选中的行形成一个单一的组,并且 SELECT 列表和 HAVING 子句只能从聚合函数中引用列。如果 HAVING 条件为真,这样的查询将发出一条记录,如果不为真,则发出零条记录。
0
0
0
浏览量1400
打酱油的后端

Flink系列之:深入理解ttl和checkpoint,Flink SQL应用ttl案例

一、深入理解Flink TTLFlink TTL(Time To Live)是一种机制,用于设置数据的过期时间,控制数据在内存或状态中的存活时间。通过设置TTL,可以自动删除过期的数据,从而释放资源并提高性能。在Flink中,TTL可以应用于不同的组件和场景,包括窗口、状态和表。窗口:对于窗口操作,可以将TTL应用于窗口中的数据。当窗口中的数据过期时,Flink会自动丢弃这些数据,从而保持窗口中的数据只包含最新的和有效的内容。这样可以减少内存的使用,同时提高窗口操作的计算性能。状态:对于有状态的操作,如键控状态或算子状态,可以为状态设置TTL。当状态中的数据过期时,Flink会自动清理过期的状态,释放资源。这对于长时间运行的应用程序特别有用,可以避免状态无限增长,消耗过多的内存。表:在Flink中,TTL也可以应用于表。可以通过在CREATE TABLE语句的WITH子句中指定TTL的选项来设置表的过期时间。当表中的数据过期时,Flink会自动删除过期的数据行。这对于处理具有实效性(例如日志)的数据特别有用,可以自动清理过期的数据,保持表的内容的新鲜和有效。TTL在实际应用中的作用主要体现在以下几个方面:节省资源:通过设置合适的TTL,可以有效地管理和控制内存和状态的使用。过期的数据会被自动清理,释放资源。这样可以避免无效或过时的数据占用过多的资源,提高应用程序的性能和可扩展性。数据清理:对于具有实效性的数据,如日志数据,可以使用TTL自动清理过期的数据。这可以减少手动管理和维护数据的工作量,保持数据的新鲜和有效。数据一致性:通过设置合适的TTL,可以确保数据在一定时间内保持一致性。过期的数据不再被读取或使用,可以避免数据不一致性的问题。性能优化:TTL可以通过自动清理过期数据来优化查询和计算的性能。只有最新和有效的数据被保留,可以减少数据的处理量,提高计算效率。总而言之,TTL是Flink中一种重要的机制,用于控制数据的过期时间和生命周期。通过适当配置TTL,可以优化资源使用、提高系统性能,并保持数据的一致性和有效性。二、Flink SQL设置TTLFlink SQL中可以使用TTL(Time To Live)来设置数据的过期时间,以控制数据在内存或状态中的存留时间。通过设置TTL,可以自动删除过期的数据,从而节省资源并提高性能。要在Flink SQL中设置TTL,可以使用CREATE TABLE语句的WITH选项来指定TTL的配置。以下是一个示例:CREATE TABLE myTable ( id INT, name STRING, eventTime TIMESTAMP(3), WATERMARK FOR eventTime AS eventTime - INTERVAL '5' MINUTE -- 定义Watermark ) WITH ( 'connector' = 'kafka', 'topic' = 'myTopic', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json', 'json.fail-on-missing-field' = 'false', 'json.ignore-parse-errors' = 'true', 'ttl' = '10m' -- 设置TTL为10分钟 );在上述示例中,通过在CREATE TABLE语句的WITH子句中的’ttl’选项中指定TTL的值(10m),即设置数据在内存中的存活时间为10分钟。过期的数据会自动被删除。需要注意的是,引入TTL机制会增加一定的性能和资源开销。因此,在使用TTL时需要权衡好过期时间和系统的性能需求。三、Flink设置TTL在需要设置TTL的数据源或状态上,使用相应的API(例如DataStream API或KeyedState API)设置TTL值。// DataStream API dataStream.keyBy(<key_selector>).mapStateDescriptor.enableTimeToLive(Duration.ofMillis(<ttl_in_milliseconds>)); // KeyedState API descriptor.enableTimeToLive(Duration.ofMillis(<ttl_in_milliseconds>));在Flink作业中配置TTL检查间隔(默认值为每分钟一次):state.backend.rocksdb.ttl.compaction.interval: <interval_in_milliseconds>四、深入理解checkpointFlink的Checkpoint是一种容错机制,用于在Flink作业执行过程中定期保存数据的一致性检查点。它可以保证作业在发生故障时能够从检查点恢复,并继续进行。下面是一些深入介绍Checkpoint的关键概念和特性:一致性保证:Flink的Checkpoint机制通过保存作业状态的快照来实现一致性保证。在Checkpoint期间,Flink会确保所有的输入数据都已经被处理,并将结果写入状态后再进行检查点的保存。这样可以确保在恢复时,从检查点恢复的作业状态仍然是一致的。保存顺序:Flink的Checkpoint机制保证了保存检查点的顺序。检查点的保存是有序的,即在一个检查点完成之前,不会开始下一个检查点的保存。这种有序的保存方式能够保证在恢复时按照检查点的顺序进行恢复。并行度一致性:Flink的Checkpoint机制能够保证在作业的不同并行任务之间保持一致性。即使在分布式的情况下,Flink也能够确保所有并行任务在某个检查点的位置上都能保持一致。这是通过分布式快照算法和超时机制来实现的。可靠性保证:Flink的Checkpoint机制对于作业的故障恢复非常可靠。当一个任务发生故障时,Flink会自动从最近的检查点进行恢复。如果某个检查点无法满足一致性要求,Flink会自动选择前一个检查点进行恢复,以确保作业能够在一个一致的状态下继续执行。容错机制:Flink的Checkpoint机制提供了容错机制来应对各种故障情况。例如,如果某个任务在保存检查点时失败,Flink会尝试重新保存检查点,直到成功为止。此外,Flink还支持增量检查点,它可以在不保存整个作业状态的情况下只保存修改的部分状态,从而提高了保存检查点的效率。高可用性:Flink的Checkpoint机制还提供了高可用性的选项。可以将检查点数据保存在分布式文件系统中,以防止单点故障。此外,还可以配置备份作业管理器(JobManager)和任务管理器(TaskManager)以确保在某个节点发生故障时能够快速恢复。总结起来,Flink的Checkpoint机制是一种强大且可靠的容错机制,它能够确保作业在发生故障时能够从一致性检查点恢复,并继续进行。通过保存作业状态的快照,Flink能够保证作业的一致性,并提供了高可用性和高效率的保存和恢复机制。Checkpoint是Flink中一种重要的容错机制,用于保证作业在发生故障时能够从上一次检查点恢复,并继续进行处理,从而实现容错性。以下是Checkpoint的主要用途:容错和故障恢复:Checkpoint可以将作业的状态和数据进行持久化,当发生故障时,Flink可以使用最近的检查点来恢复作业的状态和数据,从而避免数据丢失,并继续处理未完成的任务。Exactly-Once语义:通过将检查点和事务(如果应用程序使用Flink的事务支持)结合起来,Flink可以实现Exactly-Once语义,确保结果的一致性和准确性。当作业从检查点恢复时,它将只会处理一次输入数据,并产生一次输出,避免了重复和丢失的数据写入。冷启动和部署:可以使用检查点来实现作业的冷启动,即在作业启动时,从最近的检查点恢复状态和数据,并从上一次检查点的位置继续处理。这对于在作业启动或重新部署时非常有用,可以快速恢复到之前的状态,减少恢复所需的时间。跨版本迁移:当使用不同版本的Flink或更改作业的代码时,可以使用检查点将作业从旧的版本转移到新的版本,从而实现跨版本迁移。总之,Checkpoint是Flink中的关键机制,其用途包括容错和故障恢复、Exactly-Once语义、冷启动和部署以及跨版本迁移。通过使用Checkpoint,可以提高作业的可靠性、一致性和可恢复性。五、Flink设置Checkpoint要设置Flink的Checkpoint和TTL,可以按照以下步骤进行操作:设置Checkpoint:在Flink作业中启用Checkpoint:可以通过在Flink配置文件(flink-conf.yaml)中设置以下属性来开启Checkpoint:execution.checkpointing.enabled: true设置Checkpoint间隔:可以通过以下属性设置Checkpoint的间隔时间(默认值为10秒):execution.checkpointing.interval: <interval_in_milliseconds>设置Checkpoint保存路径:可以通过以下属性设置Checkpoint文件的保存路径(默认为jobmanager根路径):state.checkpoints.dir: <checkpoint_directory_path>六、Flink SQL关联多张表在Flink SQL中,可以通过使用窗口操作来保证在一段时间内多张表的数据总能关联到。窗口操作可以用于基于时间的数据处理,将数据划分为窗口,并在每个窗口上执行关联操作。 下面是一个示例,演示如何在一段时间内关联多张表的数据: ```sql -- 创建两个输入表 CREATE TABLE table1 ( id INT, name STRING, eventTime TIMESTAMP(3), WATERMARK FOR eventTime AS eventTime - INTERVAL '1' SECOND ) WITH ( 'connector.type' = 'kafka', 'connector.version' = 'universal', 'connector.topic' = 'topic1', 'connector.startup-mode' = 'earliest-offset', 'format.type' = 'json' ); CREATE TABLE table2 ( id INT, value STRING, eventTime TIMESTAMP(3), WATERMARK FOR eventTime AS eventTime - INTERVAL '1' SECOND ) WITH ( 'connector.type' = 'kafka', 'connector.version' = 'universal', 'connector.topic' = 'topic2', 'connector.startup-mode' = 'earliest-offset', 'format.type' = 'json' ); -- 执行关联操作 SELECT t1.id, t1.name, t2.value FROM table1 t1 JOIN table2 t2 ON t1.id = t2.id AND t1.eventTime BETWEEN t2.eventTime - INTERVAL '5' MINUTE AND t2.eventTime + INTERVAL '5' MINUTE在上面的例子中,首先创建了两个输入表table1和table2,并分别指定了输入源(此处使用了Kafka作为示例输入源)。然后,在执行关联操作时,使用了通过窗口操作进行时间范围的过滤条件,即"t1.eventTime BETWEEN t2.eventTime - INTERVAL ‘5’ MINUTE AND t2.eventTime + INTERVAL ‘5’ MINUTE",确保了在一段时间内两张表的数据能够关联到。通过使用窗口操作,可以根据具体的时间范围来进行数据关联,从而保证在一段时间内多张表的数据总能关联到。七、Flink SQL使用TTL关联多表Flink还提供了Time-To-Live (TTL)功能,可以用于在表中定义数据的生存时间。当数据的时间戳超过定义的TTL时,Flink会自动将其从表中删除。这在处理实时数据时非常有用,可以自动清理过期的数据。在Flink中使用TTL可以通过创建表时指定TTL属性来实现,如下所示:CREATE TABLE myTable ( id INT, name STRING, event_time TIMESTAMP(3), WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND, PRIMARY KEY (id) NOT ENFORCED, TTL (event_time) AS event_time + INTERVAL '1' HOUR ) WITH ( 'connector.type' = 'kafka', ... )在这个例子中,表myTable定义了一个event_time列,并使用TTL函数指定了数据的生存时间为event_time加上1小时。当数据的event_time超过1小时时,Flink会自动删除这些数据。通过在Flink SQL中同时使用JOIN和TTL,你可以实现多张表的关联,并根据指定的条件删除过期的数据,从而更灵活地处理和管理数据。
0
0
0
浏览量2010
打酱油的后端

Flink系列之:WITH clause

Flink系列之:WITH clause适用流、批提供了一种编写辅助语句以在较大查询中使用的方法。这些语句通常称为公共表表达式 (CTE),可以被视为定义仅针对一个查询而存在的临时视图。WITH 语句的语法为:WITH <with_item_definition> [ , ... ] SELECT ... FROM ...; <with_item_defintion>: with_item_name (column_name[, ...n]) AS ( <select_query> )以下示例定义公共表表达式orders_with_total 并在GROUP BY 查询中使用它。WITH orders_with_total AS ( SELECT order_id, price + tax AS total FROM Orders ) SELECT order_id, SUM(total) FROM orders_with_total GROUP BY order_id;
0
0
0
浏览量334
打酱油的后端

Flink系列Table API和SQL之:普通Top N和窗口Top N

一、普通Top N在Flink SQL中,是通过OVER聚合和一个条件筛选来实现Top N的。具体来说,是通过将一个特殊的聚合函数ROW_NUMBER()应用到OVER窗口上,统计出每一行排序后的行号,作为一个字段提取出来。然后再用WHERE子句筛选行号小于等于N的那些行返回。基本语法如下:SELECT ... FROM ( SELECT ..., ROW_NUMBER() OVER( [PARTITION BY <字段1>[,<字段1>...]] ORDER BY <排序字段1> [asc|desc][,<排序字段2> [asc|desc]...] ) AS row_num FROM ...) WHERE row_num <= N [AND <其他条件>]OVER窗口定义,目的就是利用ROW_NUMBER()函数为每一行数据聚合得到一个排序之后的行号。行号重命名为row_num,并在外层的查询中以row_num<=N作为条件进行筛选,就可以得到根据排序字段统计的Top N结果了。二、普通Top N代码实现import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.table.api.Table; import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; public class TopNExample { public static void main(String[] args) throws Exception{ StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); //在创建表的DDL中直接定义时间属性 String createDDL = "create table clickTable (" + " `user` String, " + " url String, " + " ts bigint, " + " et AS TO_TIMESTAMP( from_unixtime(ts/1000))," + " watermark for et as et-interval '1' second " + ") with ( " + " 'connector' = 'filesystem'," + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv' , " + " 'format' = 'csv'" + ") "; tableEnv.executeSql(createDDL); //普通topN 选取当前所有用户中浏览量最大的两个 Table topNResultTable = tableEnv.sqlQuery("select user,cnt,row_num " + "from (" + " select*,row_number() over(" + " order by cnt desc" + " ) as row_num" + " from ( select user,count(url) as cnt from clickTable group by user)" + " ) where row_num<=2"); tableEnv.toChangelogStream(topNResultTable).print("top 2: "); env.execute(); } }top 2: > +I[Mary, 1, 1] top 2: > +I[Bob, 1, 2] top 2: > -U[Mary, 1, 1] top 2: > +U[Bob, 2, 1] top 2: > -U[Bob, 1, 2] top 2: > +U[Mary, 1, 2] top 2: > -U[Mary, 1, 2] top 2: > +U[Alice, 2, 2] top 2: > -U[Bob, 2, 1] top 2: > +U[Bob, 3, 1] top 2: > -U[Alice, 2, 2] top 2: > +U[Alice, 3, 2]三、窗口Top N除了直接对数据进行Top N的选取,也可以针对窗口来做Top N。具体来说,可以先做一个窗口聚合,将窗口信息window_start、window_end连同每个商品的点击量一并返回,这样就得到了聚合的结果表,包含了窗口信息、商品和统计的点击量。接下来就可以像一般的Top N那样定义OVER窗口了,按窗口分组,按点击量排序,用ROW_NUMBER()统计行号并筛选前N行就可以得到结果。所以窗口Top N的实现就是窗口聚合与OVER聚合的结合使用。四、窗口Top N代码实现import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.table.api.Table; import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; public class TopNExample { public static void main(String[] args) throws Exception{ StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); //在创建表的DDL中直接定义时间属性 String createDDL = "create table clickTable (" + " `user` String, " + " url String, " + " ts bigint, " + " et AS TO_TIMESTAMP( from_unixtime(ts/1000))," + " watermark for et as et-interval '1' second " + ") with ( " + " 'connector' = 'filesystem'," + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv' , " + " 'format' = 'csv'" + ") "; tableEnv.executeSql(createDDL); //窗口topN 一段时间内活跃用户统计 String subQuery = " select user, count(url) as cnt, window_start, window_end " + " from table ( " + " tumble( table clickTable, descriptor(et), interval '10' second )" + ") " + " group by user, window_start, window_end "; Table windowTopNResultTable = tableEnv.sqlQuery("select user, cnt, row_num, window_end " + "from (" + " select * , row_number() over(" + " partition by window_start, window_end " + // 固定写法 " order by cnt desc" + " ) as row_num" + " from ( " + subQuery + " ) " + ") where row_num <= 2"); tableEnv.toDataStream(windowTopNResultTable).print("window Top N :"); env.execute(); } }window Top N :> +I[Bob, 2, 1, 1970-01-01T08:00:10] window Top N :> +I[Alice, 2, 2, 1970-01-01T08:00:10] window Top N :> +I[Bob, 1, 1, 1970-01-01T08:00:20] window Top N :> +I[Alice, 1, 1, 1970-01-01T08:00:30]
0
0
0
浏览量1862
打酱油的后端

Flink系列之:Checkpoints

一、概述Checkpoint 使 Flink 的状态具有良好的容错性,通过 checkpoint 机制,Flink 可以对作业的状态和计算位置进行恢复。二、保留CheckpointCheckpoint 在默认的情况下仅用于恢复失败的作业,并不保留,当程序取消时 checkpoint 就会被删除。当然,你可以通过配置来保留 checkpoint,这些被保留的 checkpoint 在作业失败或取消时不会被清除。这样,你就可以使用该 checkpoint 来恢复失败的作业。CheckpointConfig config = env.getCheckpointConfig(); config.setExternalizedCheckpointCleanup(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);ExternalizedCheckpointCleanup 配置项定义了当作业取消时,对作业 checkpoint 的操作:ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION:当作业取消时,保留作业的 checkpoint。注意,这种情况下,需要手动清除该作业保留的 checkpoint。ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION:当作业取消时,删除作业的 checkpoint。仅当作业失败时,作业的 checkpoint 才会被保留。三、目录结构与 savepoints 相似,checkpoint 由元数据文件、数据文件(与 state backend 相关)组成。可通过配置文件中 “state.checkpoints.dir” 配置项来指定元数据文件和数据文件的存储路径,另外也可以在代码中针对单个作业特别指定该配置项。当前的 checkpoint 目录结构如下所示:/user-defined-checkpoint-dir /{job-id} | + --shared/ + --taskowned/ + --chk-1/ + --chk-2/ + --chk-3/ ... 其中 SHARED 目录保存了可能被多个 checkpoint 引用的文件,TASKOWNED 保存了不会被 JobManager 删除的文件,EXCLUSIVE 则保存那些仅被单个 checkpoint 引用的文件。注意: Checkpoint 目录不是公共 API 的一部分,因此可能在未来的 Release 中进行改变。四、通过配置文件全局配置state.checkpoints.dir: hdfs:///checkpoints/五、创建 state backend 对单个作业进行配置env.setStateBackend(new RocksDBStateBackend("hdfs:///checkpoints-data/"));六、从保留的checkpoint 中恢复状态与 savepoint 一样,作业可以从 checkpoint 的元数据文件恢复运行(savepoint恢复指南)。注意,如果元数据文件中信息不充分,那么 jobmanager 就需要使用相关的数据文件来恢复作业。$ bin/flink run -s :checkpointMetaDataPath [:runArgs]
0
0
0
浏览量156
打酱油的后端

Flink系列之:Over聚合

一、Over聚合OVER 聚合通过排序后的范围数据为每行输入计算出聚合值。和 GROUP BY 聚合不同, OVER 聚合不会把结果通过分组减少到一行,它会为每行输入增加一个聚合值。下面这个查询为每个订单计算前一个小时之内接收到的同一产品所有订单的总金额。SELECT order_id, order_time, amount, SUM(amount) OVER ( PARTITION BY product ORDER BY order_time RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW ) AS one_hour_prod_amount_sum FROM Orders这个Flink SQL查询的目标是,基于给定的订单数据,计算每个订单的订单时间之前一个小时内同一产品的总销售金额。首先,我们选择了"order_id"、"order_time"和"amount"这三个列。然后,我们使用SUM(amount) OVER (PARTITION BY product ORDER BY order_time RANGE BETWEEN INTERVAL ‘1’ HOUR PRECEDING AND CURRENT ROW)函数来计算每个订单的订单时间之前一个小时内同一产品的销售金额总和。在函数中,我们使用了PARTITION BY子句来指定按产品进行分区,即对每个产品分别计算销售金额总和。然后,我们使用ORDER BY子句来指定按订单时间进行排序,以确保计算的是订单时间之前的销售金额。最后,我们使用RANGE BETWEEN INTERVAL ‘1’ HOUR PRECEDING AND CURRENT ROW来指定计算的范围为订单时间之前一个小时到当前行(当前订单)。这个查询的结果将为每个订单提供订单时间之前一个小时内同一产品的销售金额总和。下面总结了 OVER 窗口的语法:SELECT agg_func(agg_col) OVER ( [PARTITION BY col1[, col2, ...]] ORDER BY time_col range_definition), ... FROM ...你可以在一个 SELECT 子句中定义多个 OVER 窗口聚合。然而,对于流式查询,由于目前的限制,所有聚合的 OVER 窗口必须是相同的。二、ORDER BYOVER 窗口需要数据是有序的。因为表没有固定的排序,所以 ORDER BY 子句是强制的。对于流式查询,Flink 目前只支持 OVER 窗口定义在升序(asc)的 时间属性 上。其他的排序不支持。三、PARTITION BYOVER 窗口可以定义在一个分区表上。PARTITION BY 子句代表着每行数据只在其所属的数据分区进行聚合。四、范围(RANGE)定义范围(RANGE)定义指定了聚合中包含了多少行数据。范围通过 BETWEEN 子句定义上下边界,其内的所有行都会聚合。Flink 只支持 CURRENT ROW 作为上边界。有两种方法可以定义范围:ROWS 间隔 和 RANGE 间隔五、RANGE 间隔RANGE 间隔是定义在排序列值上的,在 Flink 里,排序列总是一个时间属性。下面的 RANG 间隔定义了聚合会在比当前行的时间属性小 30 分钟的所有行上进行。RANGE BETWEEN INTERVAL '30' MINUTE PRECEDING AND CURRENT ROW六、ROW 间隔ROWS 间隔基于计数。它定义了聚合操作包含的精确行数。下面的 ROWS 间隔定义了当前行 + 之前的 10 行(也就是11行)都会被聚合。ROWS BETWEEN 10 PRECEDING AND CURRENT ROW WINDOWWINDOW 子句可用于在 SELECT 子句之外定义 OVER 窗口。它让查询可读性更好,也允许多个聚合共用一个窗口定义。SELECT order_id, order_time, amount, SUM(amount) OVER w AS sum_amount, AVG(amount) OVER w AS avg_amount FROM Orders WINDOW w AS ( PARTITION BY product ORDER BY order_time RANGE BETWEEN INTERVAL '1' HOUR PRECEDING AND CURRENT ROW)这个Flink SQL查询的目标是,在给定的订单数据上,计算每个订单的订单时间之前一个小时内同一产品的销售金额总和和平均销售金额。首先,我们选择了"order_id"、"order_time"和"amount"这三个列。然后,我们使用SUM(amount) OVER w和AVG(amount) OVER w函数分别计算每个订单的订单时间之前一个小时内同一产品的销售金额总和和平均销售金额。在函数中,我们定义了一个窗口w,使用WINDOW关键字来指定。窗口w使用了PARTITION BY子句按产品进行分区,并使用ORDER BY子句按订单时间进行排序。这样可以确保计算的是订单时间之前的销售金额。窗口w还使用了RANGE BETWEEN INTERVAL ‘1’ HOUR PRECEDING AND CURRENT ROW来指定计算的范围为订单时间之前一个小时到当前行(当前订单)。最后,通过将计算结果作为新的列名sum_amount和avg_amount,我们将计算的销售金额总和和平均销售金额添加到了查询结果中。这个查询的结果将包含每个订单的订单时间和销售金额,以及每个订单的订单时间之前一个小时内同一产品的销售金额总和和平均销售金额。
0
0
0
浏览量1762
打酱油的后端

Flink系列Table API和SQL之:表和流的转换

一、表和流的转换从创建表环境开始,历经表的创建、查询转换和输出,已经可以使用Table API和SQL进行完整的流处理了。不过在应用的开发过程中,我们测试业务逻辑一般不会直接将结果直接写入到外部系统,而是在本地控制台打印输出。对于DataStream非常容易,直接调用print()方法就可以看到结果数据流的内容了。但对于Table就比较悲剧,没有提供print()方法。在Flink中可以将Table再转换成DataStream,然后进行打印输出。这就涉及了表和流的转换二、将表(Table)转换成流(DataStream)调用toDataStream()方法将一个Table对象转换成DataStream非常简单,只要直接调用表环境的方法toDataStream()就可以了。Table aliceVisitTable = tableEnv.sqlQuery( "SELECT user,url " + "FROM EventTable " + "WHERE user = 'Alice' " );将表转换成数据流,这里需要将要转换的Table对象作为参数传入。tableEnv.toDataStream(aliceVisitTable).print();调用toChangelogStream()方法tableEnv.createTemporaryView("clickTable",eventTable); Table aggResult = tableEnv.sqlQuery("select user,COUNT(url) as cnt from clickTable group by user"); tableEnv.toChangelogStream(aggResult).print("agg");三、将流转换成表调用fromDataStream()方法想要将一个DataStream转换成表也很简单,可以通过调用表环境的fromDataStream()方法来实现,返回的就是一个Table对象。例如,可以直接将事件流eventStream转换成一个表。StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();获取表环境 //创建表执行环境 StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);读取数据源SingleOutputStreamOperator<Event> eventStream = env.addSource(...)将数据流转换成表Table eventTable = tableEnv.fromDataStream(eventStream);由于流中的数据本身就是定义好的POJO类型Event,所以我们将流转换成表之后,每一行数据就对应着一个Event,而表中的列名就对应着Event中的属性。另外,还可以在fromDataStream()方法中增加参数,用来指定提取哪些属性作为表中的字段名,并可以任意指定位置。提取Event中的timestamp和url作为表中的列Table eventTable2 = tableEnv.fromDataStream(eventStream,$("timestamp"),$("url"));需要注意的是,timestamp本身是SQL中的关键字,所以我们在定义表名、列名时要尽量避免。这时可以通过表达式的as()方法对字段进行重命名。Table eventTable2 = tableEnv.fromDataStream(eventStream,$("timestamp").as("ts"),$("url"));调用createTemporaryView()方法调用fromDataStream()方法简单直观,可以直接实现DataStream到Table的转换。不过如果希望直接在SQL中引用这张表,就还需要调用表环境的createTemporaryView()方法来创建虚拟视图。对于这种场景,更简洁的调用方式,可以直接调用createTemporaryView()方法创建虚拟表,传入的两个参数,第一个依然是注册的表名,而第二个可以直接就是DataStream。之后可以传入多个参数,用来指定表中的字段:tableEnv.createTemporaryView("EventTable",eventStream,$("timestamp").as("ts"),$("url"));这样接下来就可以直接在SQL中引用表EventTable了。调用fromChangelogStream()方法表环境还提供了一个方法fromChangelogStream(),可以将一个更新日志流转换成表。这个方法要求流中的数据类型只能是Row,而且每一个数据都需要指定当前航的更新类型(RowKind)。所以一般是由连接器帮我们实现的。四、支持的数据类型DataStream,流中的数据类型都是定义好的POJO类。如果DataStream中的类型是简单的基本类型,还可以直接转换成表么?这就涉及了Table中支持的数据类型。整体来看,DataStream中支持的数据类型,Table中也都是支持的,只不过在进行转换时需要注意一些细节。原子类型:在Flink中,基础数据类型(Integer、Double、String)和通用数据类型(也就是不可再拆分的数据类型)统一称做原子类型。原子类型的DataStream,转换之后就成了只有一列的Table,列字段(field)的数据类型可以由原子类型推断出。另外,还可以在fromDataStream()方法里增加参数,用来重新命名列字段。StreamTableEnvironment tableEnv = ...; DataStream<Long> stream = ...;将数据流转换成动态表,动态表只有一个字段,重命名为myLongTable table = tableEnv.fromDataStream(stream,$("myLong"));Tuple类型当原子类型不做重命名时,默认的字段名就是"f0",容易想到,其实就是将原子类型看做了一元组Tuple1的处理结果。Table支持Flink中定义的元组类型Tuple,对应在表中字段名默认就是元祖中元素的属性名f0、f1、f2…。所有字段都可以被重新排序,也可以提取其中的一部分字段。字段还可以通过调用表达式的as()方法来进行重命名。StreamTableEnvironment tableEnv = ...; DataStream<Tuple2<Long,Integer>> stream = ... ;将数据流转换成只包含f1字段的表Table table = tableEnv.fromDataStream(stream,$("f1"));将数据流转换成包含f0和f1字段的表,在表中f0和f1位置交换Table table = tableEnv.fromDataStream(stream,$("f1"),$("f0"));将f1字段命名为myInt,f0命名为myLongTable table = tableEnv.fromDataStream(stream,$("f1").as("myInt"),$("f0").as("myLong"));Row类型Flink中还定义了一个在关系型表中更加通用的数据类型——行(Row),它是Table中数据的基本组织形式。Row类型也是一种复合类型,它的长度固定,而且无法直接推断出每个字段的类型,所以在使用时必须指明具体的类型信息。在创建Table时调用的CREATE语句就会将所有的字段名称和类型指定,这在Flink中被称为表的模式结构(Schema)。除此之外,Row类型还附加了一个属性RowKind,用来表示当前行在更新操作中的类型。这样,Row就可以用来表示更新日志流(changelog stream)中的数据,从而架起了Flink中流和表的转换桥梁。所以在更新日志流中,元素的类型必须是Row,而且需要调用ofKind(0方法来指定更新类型。下面是一个具体的例子: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",100) );将更新日志流转换为表Table table = tableEnv.fromChangelogStream(dataStream);
0
0
0
浏览量201
打酱油的后端

Flink系列之:使用flink查询数据和插入数据

SELECT 语句和 VALUES 语句是使用 TableEnvironment 的 sqlQuery() 方法指定的。该方法以表的形式返回 SELECT 语句(或 VALUES 语句)的结果。 Table 可以在后续的 SQL 和 Table API 查询中使用、转换为 DataStream 或写入 TableSink。 SQL 和 Table API 查询可以无缝混合,并进行整体优化并转换为单个程序。为了在 SQL 查询中访问表,它必须在 TableEnvironment 中注册。可以通过 TableSource、Table、CREATE TABLE 语句、DataStream 注册表。或者,用户还可以在 TableEnvironment 中注册目录来指定数据源的位置。为了方便起见,Table.toString() 自动在其 TableEnvironment 中以唯一名称注册该表并返回该名称。因此,Table 对象可以直接内联到 SQL 查询中,如下面的示例所示。注意:包含不受支持的 SQL 功能的查询会导致 TableException。以下部分列出了批处理表和流表上 SQL 支持的功能。一、指定查询以下示例显示如何在注册表和内联表上指定 SQL 查询。Java版本StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 从外部源摄取数据流 DataStream<Tuple3<Long, String, Integer>> ds = env.addSource(...); // 使用内联(未注册)表的 SQL 查询 Table table = tableEnv.fromDataStream(ds, $("user"), $("product"), $("amount")); Table result = tableEnv.sqlQuery( "SELECT SUM(amount) FROM " + table + " WHERE product LIKE '%Rubber%'"); // 使用已注册的表进行 SQL 查询 // 将数据流注册为视图“Orders” tableEnv.createTemporaryView("Orders", ds, $("user"), $("product"), $("amount")); // 对表运行 SQL 查询并将结果作为新表检索 Table result2 = tableEnv.sqlQuery( "SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'"); // 创建并注册 TableSink final Schema schema = Schema.newBuilder() .column("product", DataTypes.STRING()) .column("amount", DataTypes.INT()) .build(); final TableDescriptor sinkDescriptor = TableDescriptor.forConnector("filesystem") .schema(schema) .format(FormatDescriptor.forFormat("csv") .option("field-delimiter", ",") .build()) .build(); tableEnv.createTemporaryTable("RubberOrders", sinkDescriptor); // 在表上运行 INSERT SQL 并将结果发送到 TableSink tableEnv.executeSql( "INSERT INTO RubberOrders SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'");Scala版本val env = StreamExecutionEnvironment.getExecutionEnvironment val tableEnv = StreamTableEnvironment.create(env) // 从外部源摄取数据流 val ds: DataStream[(Long, String, Integer)] = env.addSource(...) // 使用内联(未注册)表的 SQL 查询 val table = ds.toTable(tableEnv, $"user", $"product", $"amount") val result = tableEnv.sqlQuery( s"SELECT SUM(amount) FROM $table WHERE product LIKE '%Rubber%'") // 使用已注册的表进行 SQL 查询 // 将数据流注册为视图“Orders” tableEnv.createTemporaryView("Orders", ds, $"user", $"product", $"amount") // 对表运行 SQL 查询并将结果作为新表检索 val result2 = tableEnv.sqlQuery( "SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'") // 创建并注册 TableSink val schema = Schema.newBuilder() .column("product", DataTypes.STRING()) .column("amount", DataTypes.INT()) .build() val sinkDescriptor = TableDescriptor.forConnector("filesystem") .schema(schema) .format(FormatDescriptor.forFormat("csv") .option("field-delimiter", ",") .build()) .build() tableEnv.createTemporaryTable("RubberOrders", sinkDescriptor) // 在表上运行 INSERT SQL 并将结果发送到 TableSink tableEnv.executeSql( "INSERT INTO RubberOrders SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'")pythonenv = StreamExecutionEnvironment.get_execution_environment() table_env = StreamTableEnvironment.create(env) # 使用内联(未注册)表的 SQL 查询 # 元素数据类型:BIGINT、STRING、BIGINT table = table_env.from_elements(..., ['user', 'product', 'amount']) result = table_env \ .sql_query("SELECT SUM(amount) FROM %s WHERE product LIKE '%%Rubber%%'" % table) // 创建并注册 TableSink schema = Schema.new_builder() .column("product", DataTypes.STRING()) .column("amount", DataTypes.INT()) .build() sink_descriptor = TableDescriptor.for_connector("filesystem") .schema(schema) .format(FormatDescriptor.for_format("csv") .option("field-delimiter", ",") .build()) .build() t_env.create_temporary_table("RubberOrders", sink_descriptor) // 在表上运行 INSERT SQL 并将结果发送到 TableSink table_env \ .execute_sql("INSERT INTO RubberOrders SELECT product, amount FROM Orders WHERE product LIKE '%Rubber%'")二、执行查询可以通过TableEnvironment.executeSql()方法执行SELECT语句或VALUES语句将内容收集到本地。该方法将 SELECT 语句(或 VALUES 语句)的结果作为 TableResult 返回。与 SELECT 语句类似,可以使用 Table.execute() 方法执行 Table 对象,以将查询内容收集到本地客户端。 TableResult.collect() 方法返回一个可关闭的行迭代器。除非收集了所有结果数据,否则选择作业将不会完成。我们应该通过 CloseableIterator#close() 方法主动关闭作业以避免资源泄漏。我们还可以通过 TableResult.print() 方法将选择结果打印到客户端控制台。 TableResult 中的结果数据只能访问一次。因此,collect() 和 print() 不能先后调用。TableResult.collect() 和 TableResult.print() 在不同的检查点设置下的行为略有不同(要为流作业启用检查点,请参阅检查点配置)。对于没有检查点的批处理作业或流作业,TableResult.collect() 和 TableResult.print() 既没有精确一次也没有至少一次保证。查询结果一旦生成就可以立即被客户端访问,但是当作业失败并重新启动时将会抛出异常。对于具有一次性检查点的流作业,TableResult.collect() 和 TableResult.print() 保证端到端的一次性记录交付。只有在相应的检查点完成后,客户端才能访问结果。对于具有至少一次检查点的流作业,TableResult.collect() 和 TableResult.print() 保证端到端至少一次记录传递。查询结果一旦生成就可以立即被客户端访问,但相同的结果可能会被多次传递。Java:StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env, settings); tableEnv.executeSql("CREATE TABLE Orders (`user` BIGINT, product STRING, amount INT) WITH (...)"); // 执行 SELECT 语句 TableResult tableResult1 = tableEnv.executeSql("SELECT * FROM Orders"); // 使用 try-with-resources 语句确保迭代器将自动关闭 try (CloseableIterator<Row> it = tableResult1.collect()) { while(it.hasNext()) { Row row = it.next(); // handle row } } // 执行表 TableResult tableResult2 = tableEnv.sqlQuery("SELECT * FROM Orders").execute(); tableResult2.print(); Scala:val env = StreamExecutionEnvironment.getExecutionEnvironment() val tableEnv = StreamTableEnvironment.create(env, settings) // 启用检查点 tableEnv.getConfig.set( ExecutionCheckpointingOptions.CHECKPOINTING_MODE, CheckpointingMode.EXACTLY_ONCE) tableEnv.getConfig.set( ExecutionCheckpointingOptions.CHECKPOINTING_INTERVAL, Duration.ofSeconds(10)) tableEnv.executeSql("CREATE TABLE Orders (`user` BIGINT, product STRING, amount INT) WITH (...)") // 执行 SELECT 语句 val tableResult1 = tableEnv.executeSql("SELECT * FROM Orders") val it = tableResult1.collect() try while (it.hasNext) { val row = it.next // 处理行 } finally it.close() // close the iterator to avoid resource leak // 执行表 val tableResult2 = tableEnv.sqlQuery("SELECT * FROM Orders").execute() tableResult2.print() Python:env = StreamExecutionEnvironment.get_execution_environment() table_env = StreamTableEnvironment.create(env, settings) # 启用检查点 table_env.get_config().set("execution.checkpointing.mode", "EXACTLY_ONCE") table_env.get_config().set("execution.checkpointing.interval", "10s") table_env.execute_sql("CREATE TABLE Orders (`user` BIGINT, product STRING, amount INT) WITH (...)") # 执行 SELECT 语句 table_result1 = table_env.execute_sql("SELECT * FROM Orders") table_result1.print() # 执行表 table_result2 = table_env.sql_query("SELECT * FROM Orders").execute() table_result2.print()三、语法Flink 使用 Apache Calcite 解析 SQL,它支持标准 ANSI SQL。以下 BNF 语法描述了批处理和流式查询中支持的 SQL 功能的超集。操作部分显示了支持的功能的示例,并指出哪些功能仅支持批处理或流查询。query: values | WITH withItem [ , withItem ]* query | { select | selectWithoutFrom | query UNION [ ALL ] query | query EXCEPT query | query INTERSECT query } [ ORDER BY orderItem [, orderItem ]* ] [ LIMIT { count | ALL } ] [ OFFSET start { ROW | ROWS } ] [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY] withItem: name [ '(' column [, column ]* ')' ] AS '(' query ')' orderItem: expression [ ASC | DESC ] select: SELECT [ ALL | DISTINCT ] { * | projectItem [, projectItem ]* } FROM tableExpression [ WHERE booleanExpression ] [ GROUP BY { groupItem [, groupItem ]* } ] [ HAVING booleanExpression ] [ WINDOW windowName AS windowSpec [, windowName AS windowSpec ]* ] selectWithoutFrom: SELECT [ ALL | DISTINCT ] { * | projectItem [, projectItem ]* } projectItem: expression [ [ AS ] columnAlias ] | tableAlias . * tableExpression: tableReference [, tableReference ]* | tableExpression [ NATURAL ] [ LEFT | RIGHT | FULL ] JOIN tableExpression [ joinCondition ] joinCondition: ON booleanExpression | USING '(' column [, column ]* ')' tableReference: tablePrimary [ matchRecognize ] [ [ AS ] alias [ '(' columnAlias [, columnAlias ]* ')' ] ] tablePrimary: [ TABLE ] tablePath [ dynamicTableOptions ] [systemTimePeriod] [[AS] correlationName] | LATERAL TABLE '(' functionName '(' expression [, expression ]* ')' ')' | [ LATERAL ] '(' query ')' | UNNEST '(' expression ')' tablePath: [ [ catalogName . ] databaseName . ] tableName systemTimePeriod: FOR SYSTEM_TIME AS OF dateTimeExpression dynamicTableOptions: /*+ OPTIONS(key=val [, key=val]*) */ key: stringLiteral val: stringLiteral values: VALUES expression [, expression ]* groupItem: expression | '(' ')' | '(' expression [, expression ]* ')' | CUBE '(' expression [, expression ]* ')' | ROLLUP '(' expression [, expression ]* ')' | GROUPING SETS '(' groupItem [, groupItem ]* ')' windowRef: windowName | windowSpec windowSpec: [ windowName ] '(' [ ORDER BY orderItem [, orderItem ]* ] [ PARTITION BY expression [, expression ]* ] [ RANGE numericOrIntervalExpression {PRECEDING} | ROWS numericExpression {PRECEDING} ] ')' matchRecognize: MATCH_RECOGNIZE '(' [ PARTITION BY expression [, expression ]* ] [ ORDER BY orderItem [, orderItem ]* ] [ MEASURES measureColumn [, measureColumn ]* ] [ ONE ROW PER MATCH ] [ AFTER MATCH ( SKIP TO NEXT ROW | SKIP PAST LAST ROW | SKIP TO FIRST variable | SKIP TO LAST variable | SKIP TO variable ) ] PATTERN '(' pattern ')' [ WITHIN intervalLiteral ] DEFINE variable AS condition [, variable AS condition ]* ')' measureColumn: expression AS alias pattern: patternTerm [ '|' patternTerm ]* patternTerm: patternFactor [ patternFactor ]* patternFactor: variable [ patternQuantifier ] patternQuantifier: '*' | '*?' | '+' | '+?' | '?' | '??' | '{' { [ minRepeat ], [ maxRepeat ] } '}' ['?'] | '{' repeat '}'Flink SQL 对标识符(表、属性、函数名)使用类似于 Java 的词法策略:无论标识符是否被引用,其大小写都会被保留。之后,标识符将区分大小写进行匹配。与 Java 不同,反引号允许标识符包含非字母数字字符(例如 SELECT a AS my field FROM t)。字符串文字必须用单引号引起来(例如,SELECT ‘Hello World’)。复制单引号以进行转义(例如,SELECT ‘It’s me’)。Flink SQL> SELECT 'Hello World', 'It''s me'; +-------------+---------+ | EXPR$0 | EXPR$1 | +-------------+---------+ | Hello World | It's me | +-------------+---------+ 1 row in set字符串文字支持 Unicode 字符。如果需要显式 unicode 代码点,请使用以下语法:使用反斜杠 () 作为转义字符(默认):SELECT U&‘\263A’使用自定义转义字符:SELECT U&‘#263A’ UECAPE ‘#’
0
0
0
浏览量1718
打酱油的后端

Flink系列之:背压下的检查点

一、Checkpointing under backpressure通常情况下,对齐 Checkpoint 的时长主要受 Checkpointing 过程中的同步和异步两个部分的影响。 然而,当 Flink 作业正运行在严重的背压下时,Checkpoint 端到端延迟的主要影响因子将会是传递 Checkpoint Barrier 到 所有的算子/子任务的时间。这在 checkpointing process) 的概述中有说明原因。并且可以通过高 alignment time and start delay metrics 观察到。 当这种情况发生并成为一个问题时,有三种方法可以解决这个问题:消除背压源头,通过优化 Flink 作业,通过调整 Flink 或 JVM 参数,抑或是通过扩容。减少 Flink 作业中缓冲在 In-flight 数据的数据量。启用非对齐 Checkpoints。 这些选项并不是互斥的,可以组合在一起。本文档重点介绍后两个选项。二、缓冲区 DebloatingFlink 1.14 引入了一个新的工具,用于自动控制在 Flink 算子/子任务之间缓冲的 In-flight 数据的数据量。缓冲区 Debloating 机 制可以通过将属性taskmanager.network.memory.buffer-debloat.enabled设置为true来启用。此特性对对齐和非对齐 Checkpoint 都生效,并且在这两种情况下都能缩短 Checkpointing 的时间,不过 Debloating 的效果对于 对齐 Checkpoint 最明显。 当在非对齐 Checkpoint 情况下使用缓冲区 Debloating 时,额外的好处是 Checkpoint 大小会更小,并且恢复时间更快 (需要保存 和恢复的 In-flight 数据更少)。有关缓冲区 Debloating 功能如何工作以及如何配置的更多信息,可以参考 network memory tuning guide。 请注意,您仍然可以继续使用在前面调优指南中介绍过的方式来手动减少缓冲在 In-flight 数据的数据量。三、非对齐 Checkpoints从Flink 1.11开始,Checkpoint 可以是非对齐的。 Unaligned checkpoints 包含 In-flight 数据(例如,存储在缓冲区中的数据)作为 Checkpoint State的一部分,允许 Checkpoint Barrier 跨越这些缓冲区。因此, Checkpoint 时长变得与当前吞吐量无关,因为 Checkpoint Barrier 实际上已经不再嵌入到数据流当中。如果您的 Checkpointing 由于背压导致周期非常的长,您应该使用非对齐 Checkpoint。这样,Checkpointing 时间基本上就与 端到端延迟无关。请注意,非对齐 Checkpointing 会增加状态存储的 I/O,因此当状态存储的 I/O 是 整个 Checkpointing 过程当中真 正的瓶颈时,您不应当使用非对齐 Checkpointing。为了启用非对齐 Checkpoint,您可以:Java代码StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 启用非对齐 Checkpoint env.getCheckpointConfig().enableUnalignedCheckpoints();Scala代码:val env = StreamExecutionEnvironment.getExecutionEnvironment() // 启用非对齐 Checkpoint env.getCheckpointConfig.enableUnalignedCheckpoints()Python代码:env = StreamExecutionEnvironment.get_execution_environment() # 启用非对齐 Checkpoint env.get_checkpoint_config().enable_unaligned_checkpoints()或者在 flink-conf.yml 配置文件中增加配置:execution.checkpointing.unaligned: true四、对齐 Checkpoint 的超时在启用非对齐 Checkpoint 后,你依然可以通过编程的方式指定对齐 Checkpoint 的超时:StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.getCheckpointConfig().setAlignedCheckpointTimeout(Duration.ofSeconds(30));或是在 flink-conf.yml 配置文件中配置:execution.checkpointing.aligned-checkpoint-timeout: 30 s在启动时,每个 Checkpoint 仍然是 aligned checkpoint,但是当全局 Checkpoint 持续时间超过 aligned-checkpoint-timeout 时, 如果 aligned checkpoint 还没完成,那么 Checkpoint 将会转换为 Unaligned Checkpoint。五、限制并发 CheckpointFlink 当前并不支持并发的非对齐 Checkpoint。然而,由于更可预测的和更短的 Checkpointing 时长,可能也根本就不需要并发的 Checkpoint。此外,Savepoint 也不能与非对齐 Checkpoint 同时发生,因此它们将会花费稍长的时间。与 Watermark 的相互影响非对齐 Checkpoint 在恢复的过程中改变了关于 Watermark 的一个隐式保证。目前,Flink 确保了 Watermark 作为恢复的第一步, 而不是将最近的 Watermark 存放在 Operator 中,以方便扩缩容。在非对齐 Checkpoint 中,这意味着当恢复时,Flink 会在恢复 In-flight 数据后再生成 Watermark。如果您的 Pipeline 中使用了对每条记录都应用最新的 Watermark 的算子将会相对于 使用对齐 Checkpoint产生不同的结果。如果您的 Operator 依赖于最新的 Watermark 始终可用,解决办法是将 Watermark 存放在 OperatorState 中。在这种情况下,Watermark 应该使用单键 group 存放在 UnionState 以方便扩缩容。与长时间运行的记录处理的相互作用尽管未对齐的检查点障碍仍然能够超越队列中的所有其他记录。如果当前记录需要花费大量时间来处理,则此屏障的处理仍然可能会被延迟。当同时触发多个计时器时(例如在窗口操作中),可能会发生这种情况。当系统在处理单个输入记录时被阻塞等待多个网络缓冲区可用性时,可能会出现第二种有问题的情况。 Flink 无法中断单个输入记录的处理,未对齐的检查点必须等待当前处理的记录被完全处理。这可能会在两种情况下导致问题。由于不适合单个网络缓冲区的大记录的序列化或在 flatMap 操作中,会为一个输入记录生成许多输出记录。在这种情况下,背压可能会阻止未对齐的检查点,直到处理单个输入记录所需的所有网络缓冲区都可用。当处理单个记录需要一段时间时,它也可能发生在任何其他情况下。因此,检查点的时间可能会比预期的时间长,或者可能会有所不同。某些数据分布模式没有检查点有些属性包含的连接无法与 Channel 中的数据一样保存在 Checkpoint 中。为了保留这些功能并确保没有状态冲突或非预期的行为,非同一 Checkpoint 对于这些类型的连接是禁用的。所有其他的交换仍然执行非单色检查点。点对点连接我们目前没有任何对于点对点连接中有关数据有序性的强保证。然而,由于数据已经被以前置的 Source 或是 KeyBy 相同的方式隐式 组织,一些用户会依靠这种特性在提供的有序性保证的同时将计算敏感型的任务划分为更小的块。只要并行度不变,非对齐 Checkpoint(UC) 将会保留这些特性。但是如果加上UC的伸缩容,这些特性将会被改变。针对如下任务如果我们想将并行度从 p=2 扩容到 p=3,那么需要根据 KeyGroup 将 KeyBy 的 Channel 中的数据突然的划分到3个 Channel 中去。这 很容易做到,通过使用 Operator 的 KeyGroup 范围和确定记录属于某个 Key(group) 的方法(不管实际使用的是什么方法)。对于 Forward 的 Channel,我们根本没有 KeyContext。Forward Channel 里也没有任何记录被分配了任何 KeyGroup;也无法计算它,因为无法保证 Key仍然存在。广播 Connections广播 Connection 带来了另一个问题。无法保证所有 Channel 中的记录都以相同的速率被消费。这可能导致某些 Task 已经应用了与 特定广播事件对应的状态变更,而其他任务则没有,如图所示。广播分区通常用于实现广播状态,它应该跨所有 Operator 都相同。Flink 实现广播状态,通过仅 Checkpointing 有状态算子的 SubTask 0 中状态的单份副本。在恢复时,我们将该份副本发往所有的 Operator。因此,可能会发生以下情况:某个算子将很快从它的 Checkpointed Channel 消费数据并将修改应有于记录来获得状态。六、故障排除Corrupted in-flight data以下描述的操作是最后采取的手段,因为它们将会导致数据的丢失。为了防止 In-flight 数据损坏,或者由于其他原因导致作业应该在没有 In-flight 数据的情况下恢复,可以使用 recover-without-channel-state.checkpoint-id 属性。该属性需要指定一个 Checkpoint Id,对它来说 In-flight 中的数据将会被忽略。除非已经持久化的 In-flight 数据内部的损坏导致无 法恢复的情况,否则不要设置该属性。只有在重新部署作业后该属性才会生效,这就意味着只有启用 externalized checkpoint时,此操作才有意义。
0
0
0
浏览量1927
打酱油的后端

Flink系列之:窗口函数Windowing table-valued functions

适用流、批Windows 是处理无限流的核心。 Windows 将流分割成有限大小的“桶”,我们可以在其中应用计算。本文档重点介绍 Flink SQL 中如何执行窗口以及程序员如何从其提供的功能中获得最大收益。Apache Flink 提供了多个窗口表值函数 (TVF) 将表的元素划分为窗口,包括:Tumble WindowsHop WindowsCumulate Windows会话窗口(即将支持)请注意,每个元素在逻辑上可以属于多个窗口,具体取决于您使用的窗口表值函数。例如,HOP 开窗创建重叠窗口,其中单个元素可以分配给多个窗口。窗口 TVF 是 Fl​​ink 定义的多态表函数(缩写为 PTF)。 PTF 是 SQL 2016 标准的一部分,是一种特殊的表函数,但可以将表作为参数。 PTF 是一个强大的功能,可以改变表格的形状。由于 PTF 在语义上的使用与表类似,因此它们的调用发生在 SELECT 语句的 FROM 子句中。窗口 TVF 是传统分组窗口函数的替代品。窗口 TVF 更符合 SQL 标准,并且更强大,可以支持复杂的基于窗口的计算,例如窗口 TopN、窗口连接。但是,分组窗口函数只能支持窗口聚合。了解更多如何基于加窗 TVF 应用进一步计算:窗口聚合窗口顶部N窗口连接窗口重复数据删除一、窗口函数Apache Flink 提供了 3 个内置窗口 TVF:TUMBLE、HOP 和 CUMULATE。窗口TVF的返回值是一个新的关系,包括原始关系的所有列以及额外的3列,名为“window_start”,“window_end”,“window_time”以指示分配的窗口。在流模式下,“window_time”字段是窗口的时间属性。在批处理模式下,“window_time”字段是基于输入时间字段类型的 TIMESTAMP 或 TIMESTAMP_LTZ 类型的属性。 “window_time”字段可用于后续基于时间的操作,例如另一个基于聚合的窗口 TVF 或间隔连接。 window_time 的值始终等于 window_end - 1ms。二、Tumble WindowsTUMBLE 函数将每个元素分配给指定窗口大小的窗口。翻滚窗口具有固定大小且不重叠。例如,假设您指定大小为 5 分钟的滚动窗口。在这种情况下,Flink 将评估当前窗口,并每五分钟启动一个新窗口,如下图所示。TUMBLE 函数根据时间属性字段为关系的每一行分配一个窗口。在流模式下,时间属性字段必须是事件或处理时间属性。在批处理模式下,窗口表函数的时间属性字段必须是TIMESTAMP或TIMESTAMP_LTZ类型的属性。 TUMBLE 的返回值是一个新的关系,其中包括原始关系的所有列以及额外的 3 列,名为“window_start”、“window_end”、“window_time”以指示分配的窗口。原始时间属性“timecol”将是窗口TVF之后的常规时间戳列。TUMBLE 函数采用 3 个必需参数和 1 个可选参数:TUMBLE(TABLE data, DESCRIPTOR(timecol), size [, offset ])data:是表参数,可以是与时间属性列的任意关系。timecol:是一个列描述符,指示数据的哪些时间属性列应映射到滚动窗口。size:是指定翻滚窗口宽度的持续时间。offset:是一个可选参数,用于指定窗口开始移动的偏移量。以下是对 Bid 表的调用示例:-- 表必须具有时间属性,例如该表中的“bidtime” Flink SQL> desc Bid; +-------------+------------------------+------+-----+--------+---------------------------------+ | name | type | null | key | extras | watermark | +-------------+------------------------+------+-----+--------+---------------------------------+ | bidtime | TIMESTAMP(3) *ROWTIME* | true | | | `bidtime` - INTERVAL '1' SECOND | | price | DECIMAL(10, 2) | true | | | | | item | STRING | true | | | | +-------------+------------------------+------+-----+--------+---------------------------------+ Flink SQL> SELECT * FROM Bid; +------------------+-------+------+ | bidtime | price | item | +------------------+-------+------+ | 2020-04-15 08:05 | 4.00 | C | | 2020-04-15 08:07 | 2.00 | A | | 2020-04-15 08:09 | 5.00 | D | | 2020-04-15 08:11 | 3.00 | B | | 2020-04-15 08:13 | 1.00 | E | | 2020-04-15 08:17 | 6.00 | F | +------------------+-------+------+ Flink SQL> SELECT * FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)); -- 或使用命名参数 -- 注意:DATA 参数必须是第一个 Flink SQL> SELECT * FROM TABLE( TUMBLE( DATA => TABLE Bid, TIMECOL => DESCRIPTOR(bidtime), SIZE => INTERVAL '10' MINUTES)); +------------------+-------+------+------------------+------------------+-------------------------+ | bidtime | price | item | window_start | window_end | window_time | +------------------+-------+------+------------------+------------------+-------------------------+ | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:09 | 5.00 | D | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | +------------------+-------+------+------------------+------------------+-------------------------+ -- 在翻滚窗口表上应用聚合 Flink SQL> SELECT window_start, window_end, SUM(price) FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | +------------------+------------------+-------+注意:为了更好地理解窗口的行为,我们简化了时间戳值的显示,不显示尾随零,例如如果类型为 TIMESTAMP(3),2020-04-15 08:05 在 Flink SQL Client 中应显示为 2020-04-15 08:05:00.000。三、Hop WindowsHOP 函数将元素分配给固定长度的窗口。与 TUMBLE 窗口函数一样,窗口的大小由窗口大小参数配置。附加的窗口滑动参数控制跳跃窗口启动的频率。因此,如果幻灯片小于窗口大小,则跳跃窗口可能会重叠。在这种情况下,元素被分配给多个窗口。跳跃窗口也称为“滑动窗口”。例如,您可以将大小为 10 分钟的窗口滑动 5 分钟。这样,您每 5 分钟就会获得一个窗口,其中包含过去 10 分钟内到达的事件,如下图所示。HOP 函数分配覆盖大小间隔内的行的窗口,并根据时间属性字段移动每张幻灯片。在流模式下,时间属性字段必须是事件或处理时间属性。在批处理模式下,窗口表函数的时间属性字段必须是TIMESTAMP或TIMESTAMP_LTZ类型的属性。 HOP的返回值是一个新的关系,包括原始关系的所有列以及额外的3列,名为“window_start”,“window_end”,“window_time”以指示分配的窗口。原始时间属性“timecol”将是加窗TVF后的常规时间戳列。HOP 采用 4 个必需参数和 1 个可选参数:HOP(TABLE data, DESCRIPTOR(timecol), slide, size [, offset ])data:是一个表参数,可以是与时间属性列的任意关系。timecol:是一个列描述符,指示数据的哪些时间属性列应映射到跳跃窗口。Slide:是指定连续跳跃窗口开始之间的持续时间的持续时间size:是指定跳跃窗口宽度的持续时间。offset:是一个可选参数,用于指定窗口开始移动的偏移量。以下是对 Bid 表的调用示例:> SELECT * FROM TABLE( HOP(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '5' MINUTES, INTERVAL '10' MINUTES)); -- 或使用命名参数 -- 注意:DATA 参数必须是第一个 > SELECT * FROM TABLE( HOP( DATA => TABLE Bid, TIMECOL => DESCRIPTOR(bidtime), SLIDE => INTERVAL '5' MINUTES, SIZE => INTERVAL '10' MINUTES)); +------------------+-------+------+------------------+------------------+-------------------------+ | bidtime | price | item | window_start | window_end | window_time | +------------------+-------+------+------------------+------------------+-------------------------+ | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:05 | 2020-04-15 08:15 | 2020-04-15 08:14:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:05 | 2020-04-15 08:15 | 2020-04-15 08:14:59.999 | | 2020-04-15 08:09 | 5.00 | D | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:09 | 5.00 | D | 2020-04-15 08:05 | 2020-04-15 08:15 | 2020-04-15 08:14:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:05 | 2020-04-15 08:15 | 2020-04-15 08:14:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:05 | 2020-04-15 08:15 | 2020-04-15 08:14:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:15 | 2020-04-15 08:25 | 2020-04-15 08:24:59.999 | +------------------+-------+------+------------------+------------------+-------------------------+ -- 在跳跃窗口表上应用聚合 > SELECT window_start, window_end, SUM(price) FROM TABLE( HOP(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '5' MINUTES, INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:05 | 2020-04-15 08:15 | 15.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | | 2020-04-15 08:15 | 2020-04-15 08:25 | 6.00 | +------------------+------------------+-------+四、Cumulate Windows累积窗口在某些场景中非常有用,例如在固定窗口间隔内提前触发的翻滚窗口。例如,每日仪表板绘制从 00:00 到每分钟的累积 UV,10:00 的 UV 代表从 00:00 到 10:00 的 UV 总数。这可以通过累积窗口轻松有效地实现。CUMULATE 函数将元素分配给覆盖初始步长间隔内的行的窗口,并每一步扩展为一个步长(保持窗口起始位置固定),直到达到最大窗口大小。您可以将 CUMULATE 函数视为首先应用最大窗口大小的 TUMBLE 窗口,并将每个翻滚窗口拆分为具有相同窗口开始和窗口结束步长差异的多个窗口。因此累积窗口确实会重叠并且没有固定的大小。例如,您可以有一个 1 小时步长和 1 天最大大小的累积窗口,您将获得窗口:[00:00, 01:00)、[00:00, 02:00)、[00:00, 03:00), …, [00:00, 24:00) 每天。CUMULATE 函数根据时间属性列分配窗口。在流模式下,时间属性字段必须是事件或处理时间属性。在批处理模式下,窗口表函数的时间属性字段必须是TIMESTAMP或TIMESTAMP_LTZ类型的属性。 CUMULATE 的返回值是一个新的关系,其中包括原始关系的所有列以及额外的 3 个名为“window_start”、“window_end”、“window_time”的列来指示分配的窗口。原始时间属性“timecol”将是窗口TVF之后的常规时间戳列。CUMULATE 采用四个必需参数和一个可选参数:CUMULATE(TABLE data, DESCRIPTOR(timecol), step, size)data:是一个表参数,可以是与时间属性列的任意关系。timecol:是一个列描述符,指示数据的哪些时间属性列应映射到累积窗口。步骤:是指定连续累积窗口末尾之间增加的窗口大小的持续时间。size:是指定累积窗口最大宽度的持续时间。 size 必须是step 的整数倍。offset:是一个可选参数,用于指定窗口开始移动的偏移量。以下是对 Bid 表的调用示例:> SELECT * FROM TABLE( CUMULATE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '2' MINUTES, INTERVAL '10' MINUTES)); -- 或使用命名参数 -- 注意:DATA 参数必须是第一个 > SELECT * FROM TABLE( CUMULATE( DATA => TABLE Bid, TIMECOL => DESCRIPTOR(bidtime), STEP => INTERVAL '2' MINUTES, SIZE => INTERVAL '10' MINUTES)); +------------------+-------+------+------------------+------------------+-------------------------+ | bidtime | price | item | window_start | window_end | window_time | +------------------+-------+------+------------------+------------------+-------------------------+ | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:00 | 2020-04-15 08:06 | 2020-04-15 08:05:59.999 | | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:00 | 2020-04-15 08:08 | 2020-04-15 08:07:59.999 | | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:00 | 2020-04-15 08:08 | 2020-04-15 08:07:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:09 | 5.00 | D | 2020-04-15 08:00 | 2020-04-15 08:10 | 2020-04-15 08:09:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:12 | 2020-04-15 08:11:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:14 | 2020-04-15 08:13:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:16 | 2020-04-15 08:15:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:18 | 2020-04-15 08:17:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:14 | 2020-04-15 08:13:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:16 | 2020-04-15 08:15:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:18 | 2020-04-15 08:17:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:10 | 2020-04-15 08:18 | 2020-04-15 08:17:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:10 | 2020-04-15 08:20 | 2020-04-15 08:19:59.999 | +------------------+-------+------+------------------+------------------+-------------------------+ -- 对累积窗口表应用聚合 > SELECT window_start, window_end, SUM(price) FROM TABLE( CUMULATE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '2' MINUTES, INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:06 | 4.00 | | 2020-04-15 08:00 | 2020-04-15 08:08 | 6.00 | | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:10 | 2020-04-15 08:12 | 3.00 | | 2020-04-15 08:10 | 2020-04-15 08:14 | 4.00 | | 2020-04-15 08:10 | 2020-04-15 08:16 | 4.00 | | 2020-04-15 08:10 | 2020-04-15 08:18 | 10.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | +------------------+------------------+-------+四、Window OffsetOffset 是一个可选参数,可用于更改窗口分配。它可以是正持续时间和负持续时间。窗口偏移的默认值为0。如果设置不同的偏移值,同一条记录可能会分配到不同的窗口。例如,对于大小为 10 MINUTE 的 Tumble 窗口,时间戳为 2021-06-30 00:00:04 的记录将分配给哪个窗口?如果偏移值为-16 MINUTE,则记录分配给窗口[2021-06-29 23:54:00, 2021-06-30 00:04:00)。如果偏移值为-6 MINUTE,则记录分配给窗口[2021-06-29 23:54:00, 2021-06-30 00:04:00)。如果偏移量为-4 MINUTE,则记录分配给窗口[2021-06-29 23:56:00, 2021-06-30 00:06:00)。如果 offset 为 0,则记录分配给窗口 [2021-06-30 00:00:00, 2021-06-30 00:10:00)。如果偏移量为 4 MINUTE,则记录分配给窗口 [2021-06-29 23:54:00, 2021-06-30 00:04:00)。如果偏移量为 6 MINUTE,则记录分配给窗口 [2021-06-29 23:56:00, 2021-06-30 00:06:00)。如果偏移量为 16 MINUTE,则记录分配给窗口 [2021-06-29 23:56:00, 2021-06-30 00:06:00)。我们可以发现,一些窗口偏移参数可能对窗口的分配有同样的影响。在上述情况下,-16 MINUTE、-6 MINUTE 和 4 MINUTE 对于大小为 10 MINUTE 的翻滚窗口具有相同的效果。注意:窗口偏移的作用只是更新窗口分配,对 Watermark 没有影响。我们通过一个例子来描述如何在下面的 SQL 中使用 Tumble 窗口中的偏移量。-- 注意:目前 Flink 不支持评估单个窗口表值函数, -- 窗口表值函数应与聚合操作一起使用, -- 此示例仅用于解释表值函数的语法和产生的数据。 Flink SQL> SELECT * FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES, INTERVAL '1' MINUTES)); -- 或使用命名参数 -- 注意:DATA 参数必须是第一个 Flink SQL> SELECT * FROM TABLE( TUMBLE( DATA => TABLE Bid, TIMECOL => DESCRIPTOR(bidtime), SIZE => INTERVAL '10' MINUTES, OFFSET => INTERVAL '1' MINUTES)); +------------------+-------+------+------------------+------------------+-------------------------+ | bidtime | price | item | window_start | window_end | window_time | +------------------+-------+------+------------------+------------------+-------------------------+ | 2020-04-15 08:05 | 4.00 | C | 2020-04-15 08:01 | 2020-04-15 08:11 | 2020-04-15 08:10:59.999 | | 2020-04-15 08:07 | 2.00 | A | 2020-04-15 08:01 | 2020-04-15 08:11 | 2020-04-15 08:10:59.999 | | 2020-04-15 08:09 | 5.00 | D | 2020-04-15 08:01 | 2020-04-15 08:11 | 2020-04-15 08:10:59.999 | | 2020-04-15 08:11 | 3.00 | B | 2020-04-15 08:11 | 2020-04-15 08:21 | 2020-04-15 08:20:59.999 | | 2020-04-15 08:13 | 1.00 | E | 2020-04-15 08:11 | 2020-04-15 08:21 | 2020-04-15 08:20:59.999 | | 2020-04-15 08:17 | 6.00 | F | 2020-04-15 08:11 | 2020-04-15 08:21 | 2020-04-15 08:20:59.999 | +------------------+-------+------+------------------+------------------+-------------------------+ -- 在翻滚窗口表上应用聚合 Flink SQL> SELECT window_start, window_end, SUM(price) FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES, INTERVAL '1' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:01 | 2020-04-15 08:11 | 11.00 | | 2020-04-15 08:11 | 2020-04-15 08:21 | 10.00 | +------------------+------------------+-------+注意:为了更好地理解窗口的行为,我们简化了时间戳值的显示,不显示尾随零,例如如果类型为 TIMESTAMP(3),2020-04-15 08:05 在 Flink SQL Client 中应显示为 2020-04-15 08:05:00.000。
0
0
0
浏览量1425
打酱油的后端

Flink系列之:Joins

一、Joins适用于流、批一体Flink SQL支持对动态表进行复杂而灵活的连接操作。 为了处理不同的场景,需要多种查询语义,因此有几种不同类型的 Join。默认情况下,joins 的顺序是没有优化的。表的 join 顺序是在 FROM 从句指定的。可以通过把更新频率最低的表放在第一个、频率最高的放在最后这种方式来微调 join 查询的性能。需要确保表的顺序不会产生笛卡尔积,因为不支持这样的操作并且会导致查询失败。二、Regular JoinsRegular join 是最通用的 join 类型。在这种 join 下,join 两侧表的任何新记录或变更都是可见的,并会影响整个 join 的结果。 例如:如果左边有一条新纪录,在 Product.id 相等的情况下,它将和右边表的之前和之后的所有记录进行 join。SELECT * FROM Orders INNER JOIN Product ON Orders.productId = Product.id对于流式查询,regular join 的语法是最灵活的,允许任何类型的更新(插入、更新、删除)输入表。 然而,这种操作具有重要的操作意义:Flink 需要将 Join 输    入的两边数据永远保持在状态中。 因此,计算查询结果所需的状态可能会无限增长,这取决于所有输入表的输入数据量。你可以提供一个合适的状态 time-to-live (TTL) 配置来防止状态过大。注意:这样做可能会影响查询的正确性。三、INNER Equi-JOIN根据 join 限制条件返回一个简单的笛卡尔积。目前只支持 equi-joins,即:至少有一个等值条件。不支持任意的 cross join 和 theta join。(cross join 指的是类似 SELECT * FROM table_a CROSS JOIN table_b,theta join 指的是类似 SELECT * FROM table_a, table_b)SELECT * FROM Orders INNER JOIN Product ON Orders.product_id = Product.id四、OUTER Equi-JOIN返回所有符合条件的笛卡尔积(即:所有通过 join 条件连接的行),加上所有外表没有匹配到的行。Flink 支持 LEFT、RIGHT 和 FULL outer joins。目前只支持 equi-joins,即:至少有一个等值条件。不支持任意的 cross join 和 theta join。SELECT * FROM Orders LEFT JOIN Product ON Orders.product_id = Product.id SELECT * FROM Orders RIGHT JOIN Product ON Orders.product_id = Product.idSELECT * FROM Orders FULL OUTER JOIN Product ON Orders.product_id = Product.id这个Flink SQL查询的目标是通过将Orders表与Product表进行FULL OUTER JOIN,获取所有订单和产品的信息。使用SELECT *来选择所有列。使用FULL OUTER JOIN将Orders表和Product表连接起来,连接条件是Orders表的product_id列与Product表的id列相等。FULL OUTER JOIN操作将返回所有匹配和不匹配的行,因此查询结果将包括Orders表和Product表中的所有数据。这个查询的结果将包含所有订单和产品的信息。五、Interval Joins返回一个符合 join 条件和时间限制的简单笛卡尔积。Interval join 需要至少一个 equi-join 条件和一个 join 两边都包含的时间限定 join 条件。范围判断可以定义成就像一个条件(<, <=, >=, >),也可以是一个 BETWEEN 条件,或者两边表的一个相同类型(即:处理时间 或 事件时间)的时间属性 的等式判断。例如:如果订单是在被接收到4小时后发货,这个查询会把所有订单和它们相应的 shipments join 起来。SELECT * FROM Orders o, Shipments s WHERE o.id = s.order_id AND o.order_time BETWEEN s.ship_time - INTERVAL '4' HOUR AND s.ship_time这个SQL查询的目标是,从订单数据表(Orders)和发货数据表(Shipments)中选择符合条件的所有列。首先,我们使用FROM关键字将Orders表和Shipments表进行连接(使用逗号表示进行内连接)。然后,我们使用WHERE子句来指定连接条件,即订单(Orders)和发货(Shipments)的订单ID必须相等。除此之外,我们还使用AND子句来指定一个条件,即订单时间(o.order_time)必须在发货时间(s.ship_time)之前的4小时内。最后,使用SELECT *语句选择所有的列。这个查询的结果将返回满足连接条件和时间范围条件的所有订单和发货数据。下面列举了一些有效的 interval join 时间条件:ltime = rtimeltime >= rtime AND ltime < rtime + INTERVAL ‘10’ MINUTEltime BETWEEN rtime - INTERVAL ‘10’ SECOND AND rtime + INTERVAL ‘5’ SECOND对于流式查询,对比 regular join,interval join 只支持有时间属性的非更新表。 由于时间属性是递增的,Flink 从状态中移除旧值也不会影响结果的正确性。六、Temporal Joins时态表(Temporal table)是一个随时间变化的表:在 Flink 中被称为动态表。时态表中的行与一个或多个时间段相关联,所有 Flink 中的表都是时态的(Temporal)。 时态表包含一个或多个版本的表快照,它可以是一个变化的历史表,跟踪变化(例如,数据库变化日志,包含所有快照)或一个变化的维度表,也可以是一个将变更物化的维表(例如,存放最终快照的数据表)。七、事件时间 Temporal Join基于事件时间的 Temporal join 允许对版本表进行 join。 这意味着一个表可以使用变化的元数据来丰富,并在某个时间点检索其具体值。Temporal Joins 使用任意表(左侧输入/探测端)的每一行与版本表中对应的行进行关联(右侧输入/构建端)。 Flink 使用 SQL:2011标准 中的 FOR SYSTEM_TIME AS OF 语法去执行操作。 Temporal join 的语法如下:SELECT [column_list] FROM table1 [AS <alias1>] [LEFT] JOIN table2 FOR SYSTEM_TIME AS OF table1.{ proctime | rowtime } [AS <alias2>] ON table1.column-name1 = table2.column-name1有了事件时间属性(即:rowtime 属性),就能检索到过去某个时间点的值。 这允许在一个共同的时间点上连接这两个表。 版本表将存储自最后一个 watermark 以来的所有版本(按时间标识)。例如,假设我们有一个订单表,每个订单都有不同货币的价格。 为了正确地将该表统一为单一货币(如美元),每个订单都需要与下单时相应的汇率相关联。-- 创建订单表。这是一个标准 -- 仅追加动态表。 CREATE TABLE orders ( order_id STRING, price DECIMAL(32,2), currency STRING, order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '15' SECOND ) WITH (/* ... */); -- 定义版本化的货币汇率表。 -- 这可能来自变更数据捕获 -- 例如 Debezium、压缩的 Kafka 主题或任何其他 -- 定义版本化表的方法。 CREATE TABLE currency_rates ( currency STRING, conversion_rate DECIMAL(32, 2), update_time TIMESTAMP(3) METADATA FROM `values.source.timestamp` VIRTUAL, WATERMARK FOR update_time AS update_time - INTERVAL '15' SECOND, PRIMARY KEY(currency) NOT ENFORCED ) WITH ( 'connector' = 'kafka', 'value.format' = 'debezium-json', /* ... */ ); SELECT order_id, price, orders.currency, conversion_rate, order_time FROM orders LEFT JOIN currency_rates FOR SYSTEM_TIME AS OF orders.order_time ON orders.currency = currency_rates.currency; order_id price currency conversion_rate order_time ======== ===== ======== =============== ========= o_001 11.11 EUR 1.14 12:00:00 o_002 12.51 EUR 1.10 12:06:00这个Flink SQL查询的目标是将订单数据和货币汇率数据进行连接,并在连接后的结果中选择特定的列。首先,我们使用CREATE TABLE语句创建了一个名为orders的表。该表包含了订单的相关信息,如订单ID、价格、货币类型和订单时间。在定义该表时,我们还为订单时间添加了一个WATERMARK。WATERMARK用于指定事件时间,以便Flink能够按照事件时间进行处理。接下来,我们使用CREATE TABLE语句创建了一个名为currency_rates的表。该表用于存储货币汇率的相关信息,包括货币类型、兑换率和更新时间。这个表是一个版本化的表,可以通过变更数据捕获(如Debezium)、压缩的Kafka主题或其他方式创建。最后,我们使用SELECT语句从orders表中选择了订单ID、价格、订单货币类型、兑换率和订单时间这几个列。在选择这些列时,我们还使用了LEFT JOIN关键字将orders表和currency_rates表进行连接。连接条件是订单的货币类型(orders.currency)和货币汇率表中的货币类型(currency_rates.currency)必须相等。此外,我们还使用了FOR SYSTEM_TIME AS OF orders.order_time指定使用订单时间来选择货币汇率表中的数据。这样,查询的结果将返回订单数据和相应的货币汇率信息。查询结果中的order_id、price、currency、conversion_rate和order_time列分别表示订单ID、价格、货币类型、兑换率和订单时间。每一行表示一个订单及其相应的货币汇率。注意: 事件时间 temporal join 是通过左和右两侧的 watermark 触发的; 这里的 INTERVAL 时间减法用于等待后续事件,以确保 join 满足预期。 请确保 join 两边设置了正确的 watermark 。注意: 事件时间 temporal join 需要包含主键相等的条件,即:currency_rates 表的主键 currency_rates.currency 包含在条件 orders.currency = currency_rates.currency 中。与 regular joins 相比,就算 build side(例子中的 currency_rates 表)发生变更了,之前的 temporal table 的结果也不会被影响。 与 interval joins 对比,temporal join没有定义join的时间窗口。 Probe side (例子中的 orders 表)的记录总是在 time 属性指定的时间与 build side 的版本行进行连接。因此,build side 表的行可能已经过时了。 随着时间的推移,不再被需要的记录版本(对于给定的主键)将从状态中删除。八、处理时间 Temporal Join基于处理时间的 temporal join 使用处理时间属性将数据与外部版本表(例如 mysql、hbase)的最新版本相关联。通过定义一个处理时间属性,这个 join 总是返回最新的值。可以将 build side 中被查找的表想象成一个存储所有记录简单的 HashMap<K,V>。 这种 join 的强大之处在于,当无法在 Flink 中将表具体化为动态表时,它允许 Flink 直接针对外部系统工作。下面这个处理时间 temporal join 示例展示了一个追加表 orders 与 LatestRates 表进行 join。 LatestRates 是一个最新汇率的维表,比如 HBase 表,在 10:15,10:30,10:52这些时间,LatestRates 表的数据看起来是这样的:10:15> SELECT * FROM LatestRates; currency rate ======== ====== US Dollar 102 Euro 114 Yen 1 10:30> SELECT * FROM LatestRates; currency rate ======== ====== US Dollar 102 Euro 114 Yen 1 10:52> SELECT * FROM LatestRates; currency rate ======== ====== US Dollar 102 Euro 116 <==== changed from 114 to 116 Yen 1LastestRates 表的数据在 10:15 和 10:30 是相同的。 欧元(Euro)的汇率(rate)在 10:52 从 114 变更为 116。Orders 表示支付金额的 amount 和currency的追加表。 例如:在 10:15 ,有一个金额为 2 Euro 的 order。SELECT * FROM Orders; amount currency ====== ========= 2 Euro <== arrived at time 10:15 1 US Dollar <== arrived at time 10:30 2 Euro <== arrived at time 10:52给出下面这些表,我们希望所有 Orders 表的记录转换为一个统一的货币。amount currency rate amount*rate ====== ========= ======= ============ 2 Euro 114 228 <== arrived at time 10:15 1 US Dollar 102 102 <== arrived at time 10:30 2 Euro 116 232 <== arrived at time 10:52目前,temporal join 还不支持与任意 view/table 的最新版本 join 时使用 FOR SYSTEM_TIME AS OF 语法。可以像下面这样使用 temporal table function 语法来实现(时态表函数):SELECT o_amount, r_rate FROM Orders, LATERAL TABLE (Rates(o_proctime)) WHERE r_currency = o_currency这个Flink SQL查询的目标是将订单数据和货币汇率数据进行连接,并在连接后的结果中选择特定的列。首先,通过FROM子句指定了两个输入表,即Orders和LATERAL TABLE (Rates(o_proctime))。Orders表示订单数据表,而LATERAL TABLE (Rates(o_proctime))表示根据订单处理时间获取的货币汇率表。接下来,在WHERE子句中指定了连接条件,即货币汇率表中的货币类型(r_currency)必须与订单表中的货币类型(o_currency)相等。最后,在SELECT子句中选择了o_amount和r_rate这两个列,分别表示订单金额和货币汇率。查询结果将返回订单金额和对应的货币汇率。注意 Temporal join 不支持与 table/view 的最新版本进行 join 时使用 FOR SYSTEM_TIME AS OF 语法是出于语义考虑,因为左流的连接处理不会等待 temporal table 的完整快照,这可能会误导生产环境中的用户。处理时间 temporal join 使用 temporal table function 也存在相同的语义问题,但它已经存在了很长时间,因此我们从兼容性的角度支持它。processing-time 的结果是不确定的。 processing-time temporal join 常常用在使用外部系统来丰富流的数据。(例如维表)与 regular joins 的差异,就算 build side(例子中的 currency_rates 表)发生变更了,之前的 temporal table 结果也不会被影响。 与 interval joins 的差异,temporal join 没有定义数据连接的时间窗口。即:旧数据没存储在状态中。九、时态表函数连接使用 temporal table function 去 join 表的语法和 Table Function 相同。注意:目前只支持 inner join 和 left outer join。假设Rates是一个 temporal table function,这个 join 在 SQL 中可以被表达为:SELECT o_amount, r_rate FROM Orders, LATERAL TABLE (Rates(o_proctime)) WHERE r_currency = o_currency上述 temporal table DDL 和 temporal table function 的主要区别在于:SQL 中可以定义 temporal table DDL,但不能定义 temporal table 函数;temporal table DDL 和 temporal table function 都支持 temporal join 版本表,但只有 temporal table function 可以 temporal join 任何表/视图的最新版本(即"处理时间 Temporal Join")。十、Lookup Joinlookup join 通常用于使用从外部系统查询的数据来丰富表。join 要求一个表具有处理时间属性,另一个表由查找源连接器(lookup source connnector)支持。lookup join 和上面的 处理时间 Temporal Join 语法相同,右表使用查找源连接器支持。下面的例子展示了 lookup join 的语法。-- Customers 由 JDBC 连接器支持,可用于查找连接 CREATE TEMPORARY TABLE Customers ( id INT, name STRING, country STRING, zip STRING ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://mysqlhost:3306/customerdb', 'table-name' = 'customers' ); -- 用客户信息丰富每个订单 SELECT o.order_id, o.total, c.country, c.zip FROM Orders AS o JOIN Customers FOR SYSTEM_TIME AS OF o.proc_time AS c ON o.customer_id = c.id;在上面的示例中,Orders 表由保存在 MySQL 数据库中的 Customers 表数据来丰富。带有后续 process time 属性的 FOR SYSTEM_TIME AS OF 子句确保在联接运算符处理 Orders 行时,Orders 的每一行都与 join 条件匹配的 Customer 行连接。它还防止连接的 Customer 表在未来发生更新时变更连接结果。lookup join 还需要一个强制的相等连接条件,在上面的示例中是 o.customer_id = c.id。十一、数组展开返回给定数组中每个元素的新行。尚不支持 WITH ORDINALITY(会额外生成一个标识顺序的整数列)展开。SELECT order_id, tag FROM Orders CROSS JOIN UNNEST(tags) AS t (tag)十二、表功能将表与表函数的结果联接。左侧(外部)表的每一行都与表函数的相应调用产生的所有行相连接。用户自定义表函数 必须在使用前注册。INNER JOIN如果表函数调用返回一个空结果,那么左表的这行数据将不会输出。SELECT order_id, res FROM Orders, LATERAL TABLE(table_func(order_id)) t(res)这个Flink SQL查询的目标是从订单数据表中获取订单ID和通过自定义表函数根据订单ID计算得出的结果。首先,通过FROM子句指定了输入表Orders。然后,通过LATERAL TABLE子句调用了自定义的表函数table_func,并将订单ID作为参数传递给函数。表函数的作用是根据订单ID计算得出一个结果。接下来,在SELECT子句中选择了order_id和res这两个列,分别表示订单ID和通过表函数计算得到的结果。查询结果将返回订单ID和对应的表函数计算结果。LEFT OUTER JOIN如果表函数调用返回了一个空结果,则保留相应的行,并用空值填充未关联到的结果。当前,针对 lateral table 的 left outer join 需要 ON 子句中有一个固定的 TRUE 连接条件。SELECT order_id, res FROM Orders LEFT OUTER JOIN LATERAL TABLE(table_func(order_id)) t(res) ON TRUE这个Flink SQL查询的目标是从订单数据表中获取订单ID和通过自定义表函数根据订单ID计算得出的结果,并使用左外连接将计算结果与订单数据进行关联。首先,通过FROM子句指定了输入表Orders。然后,通过LATERAL TABLE子句调用了自定义的表函数table_func,并将订单ID作为参数传递给函数。表函数的作用是根据订单ID计算得出一个结果。接下来,使用LEFT OUTER JOIN关键字将计算结果与订单数据进行连接。使用ON TRUE来指定连接条件,这意味着对所有的订单都执行连接操作。最后,在SELECT子句中选择了order_id和res这两个列,分别表示订单ID和通过表函数计算得到的结果。查询结果将返回订单ID和对应的表函数计算结果,如果没有计算结果的话,会返回NULL值。
0
0
0
浏览量1602
打酱油的后端

Flink系列之:State Time-To-Live (TTL)

一、TTLFlink的TTL(Time-To-Live)是一种数据过期策略,用于指定数据在流处理中的存活时间。TTL可以应用于Flink中的状态或事件时间窗口,以控制数据的保留时间。当应用程序使用状态进行计算时,状态可能会消耗存储资源。TTL可以用来设置状态的最大生存时间,超过该时间的状态将被自动清理,以释放存储资源。这可以帮助应对状态数据的增长和资源限制问题。对于事件时间窗口,TTL可以用来指定窗口的持续时间。当到达窗口结束时间后,该窗口的结果将被输出,并且窗口中的所有数据将被清理。这可以确保计算结果及时输出,并释放计算资源。通过设置适当的TTL值,可以控制数据的保留时间,避免资源浪费和计算延迟。TTL的使用可以根据具体应用场景和需求进行配置,以实现数据管理的灵活性和效率。可以将生存时间 (TTL) 分配给任何类型的键控状态。如果配置了 TTL 并且状态值已过期,则将尽最大努力清除存储的值,这将在下面更详细地讨论。所有状态集合类型都支持每条目 TTL。这意味着列表元素和映射条目独立过期。为了使用状态 TTL,必须首先构建一个 StateTtlConfig 配置对象。然后可以通过传递配置在任何状态描述符中启用 TTL 功能:二、TTL实现代码java代码:import org.apache.flink.api.common.state.StateTtlConfig; import org.apache.flink.api.common.state.ValueStateDescriptor; import org.apache.flink.api.common.time.Time; StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired) .build(); ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("text state", String.class); stateDescriptor.enableTimeToLive(ttlConfig);这段代码使用Apache Flink提供的StateTtlConfig来设置状态的TTL(Time-To-Live)配置。首先,导入必要的包org.apache.flink.api.common.state.StateTtlConfig和org.apache.flink.api.common.state.ValueStateDescriptor。然后,创建StateTtlConfig对象ttlConfig,并使用StateTtlConfig.newBuilder(Time.seconds(1))来指定TTL的时间长度为1秒。这意味着状态数据的最大生存时间为1秒。接下来,调用ttlConfig的setUpdateType方法,将UpdateType设置为StateTtlConfig.UpdateType.OnCreateAndWrite。这表示在创建和写入状态时更新TTL。然后,调用ttlConfig的setStateVisibility方法,将StateVisibility设置为StateTtlConfig.StateVisibility.NeverReturnExpired。这表示状态在过期后永远不会返回,也就是被清理后不会再被读取。最后,使用ValueStateDescriptor创建一个名为"text state"的状态描述符stateDescriptor,并调用stateDescriptor的enableTimeToLive方法,将ttlConfig传递给它。这将启用状态的TTL配置。通过配置TTL,可以控制状态的生存时间,以及何时更新和清理状态。这有助于管理状态数据的存储和性能。Scala代码:import org.apache.flink.api.common.state.StateTtlConfig import org.apache.flink.api.common.state.ValueStateDescriptor import org.apache.flink.api.common.time.Time val ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired) .build val stateDescriptor = new ValueStateDescriptor[String]("text state", classOf[String]) stateDescriptor.enableTimeToLive(ttlConfig)Python代码:from pyflink.common.time import Time from pyflink.common.typeinfo import Types from pyflink.datastream.state import ValueStateDescriptor, StateTtlConfig ttl_config = StateTtlConfig \ .new_builder(Time.seconds(1)) \ .set_update_type(StateTtlConfig.UpdateType.OnCreateAndWrite) \ .set_state_visibility(StateTtlConfig.StateVisibility.NeverReturnExpired) \ .build() state_descriptor = ValueStateDescriptor("text state", Types.STRING()) state_descriptor.enable_time_to_live(ttl_config) 该配置有几个选项需要考虑:newBuilder方法的第一个参数是必需的,它是生存时间值。更新类型配置何时刷新状态 TTL(默认为 OnCreateAndWrite):StateTtlConfig.UpdateType.OnCreateAndWrite - 仅在创建和写入访问时StateTtlConfig.UpdateType.OnReadAndWrite - 也用于读取访问(注:如果同时将状态可见性设置为StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp,状态读缓存将被禁用,这会导致PyFlink中的一些性能损失)状态可见性配置如果尚未清除过期值,是否在读取访问时返回过期值(默认为 NeverReturnExpired):StateTtlConfig.StateVisibility.NeverReturnExpired - 永远不会返回过期值(注:状态读/写缓存将被禁用,这会导致 PyFlink 中的一些性能损失)StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp - 如果仍然可用则返回在 NeverReturnExpired 的情况下,过期状态的行为就好像它不再存在一样,即使它仍然需要被删除。该选项对于数据必须在 TTL 之后严格无法进行读取访问的用例很有用,例如处理隐私敏感数据的应用程序。另一个选项 ReturnExpiredIfNotCleanedUp 允许在清理之前返回过期状态。笔记:状态后端存储上次修改的时间戳以及用户值,这意味着启用此功能会增加状态存储的消耗。堆状态后端存储一个附加的 Java 对象,其中包含对用户状态对象的引用和内存中的原始 long 值。 RocksDB 状态后端为每个存储值、列表条目或映射条目添加 8 个字节。当前仅支持涉及处理时间的 TTL。尝试使用启用 TTL 的描述符恢复之前未配置 TTL 的状态,反之亦然,将导致兼容性失败和 StateMigrationException。TTL 配置不是检查点或保存点的一部分,而是 Flink 在当前运行的作业中处理它的一种方式。不建议通过将 ttl 从短值调整为长值来恢复检查点状态,这可能会导致潜在的数据错误。目前,仅当用户值序列化程序可以处理空值时,具有 TTL 的映射状态才支持空用户值。如果序列化器不支持 null 值,则可以使用 NullableSerializer 对其进行包装,但需要在序列化形式中增加一个额外字节。启用 TTL 的配置后,StateDescriptor 中的 defaultValue 实际上已被弃用,将不再生效。这样做的目的是使语义更加清晰,并让用户在状态内容为空或过期时手动管理默认值。三、过期状态的清理默认情况下,过期值会在读取时显式删除,例如 ValueState#value,并在配置的状态后端支持的情况下定期在后台进行垃圾收集。可以在 StateTtlConfig 中禁用后台清理:Java代码:import org.apache.flink.api.common.state.StateTtlConfig; StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .disableCleanupInBackground() .build();Scala代码:import org.apache.flink.api.common.state.StateTtlConfig val ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .disableCleanupInBackground .buildPython代码:from pyflink.common.time import Time from pyflink.datastream.state import StateTtlConfig ttl_config = StateTtlConfig \ .new_builder(Time.seconds(1)) \ .cleanup_in_rocksdb_compact_filter(1000, Time.hours(1)) \ .build()为了对后台的一些特殊清理进行更细粒度的控制,您可以如下所述单独配置它。目前,堆状态后端依赖于增量清理,RocksDB 后端使用压缩过滤器进行后台清理。完整快照中的清理此外,您可以在拍摄完整状态快照时激活清理,这将减少其大小。在当前实现下,本地状态不会被清除,但在从以前的快照恢复时,它不会包括删除的过期状态。可以在StateTtlConfig中配置:Java代码:import org.apache.flink.api.common.state.StateTtlConfig; import org.apache.flink.api.common.time.Time; StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupFullSnapshot() .build();Scala代码:import org.apache.flink.api.common.state.StateTtlConfig import org.apache.flink.api.common.time.Time val ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupFullSnapshot .buildPython代码:from pyflink.common.time import Time from pyflink.datastream.state import StateTtlConfig ttl_config = StateTtlConfig \ .new_builder(Time.seconds(1)) \ .cleanup_full_snapshot() \ .build()此选项不适用于 RocksDB 状态后端中的增量检查点。对于现有作业,可以在 StateTtlConfig 中随时激活或停用此清理策略,例如从保存点重新启动后。增量清理另一种选择是逐步触发某些状态条目的清理。触发器可以是来自每个状态访问或/和每个记录处理的回调。如果此清理策略对于某些状态是活动的,则存储后端会在其所有条目上为此状态保留一个惰性全局迭代器。每次触发增量清理时,迭代器都会前进。检查遍历的状态条目并清除过期的状态条目。该功能可以在 StateTtlConfig 中配置:Java代码:import org.apache.flink.api.common.state.StateTtlConfig; StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupIncrementally(10, true) .build();Scala代码:import org.apache.flink.api.common.state.StateTtlConfig val ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupIncrementally(10, true) .buildPython:from pyflink.common.time import Time from pyflink.datastream.state import StateTtlConfig ttl_config = StateTtlConfig \ .new_builder(Time.seconds(1)) \ .cleanup_incrementally(10, True) \ .build()该策略有两个参数。第一个是每次清理触发的检查状态条目数。它总是在每次状态访问时触发。第二个参数定义是否在每次记录处理时额外触发清理。堆后端的默认后台清理会检查 5 个条目,而不会针对每个记录处理进行清理。笔记:如果没有对状态进行访问或没有处理任何记录,则过期状态将持续存在。增量清理所花费的时间会增加记录处理延迟。目前增量清理仅针对堆状态后端实现。对 RocksDB 设置它不会有任何效果。如果堆状态后端与同步快照一起使用,则全局迭代器在迭代时会保留所有键的副本,因为其特定实现不支持并发修改。启用此功能将增加内存消耗。异步快照则不存在这个问题。对于现有作业,可以在 StateTtlConfig 中随时激活或停用此清理策略,例如从保存点重新启动后。RocksDB 压缩期间的清理如果使用 RocksDB 状态后端,将调用 Flink 特定的压缩过滤器进行后台清理。 RocksDB 定期运行异步压缩来合并状态更新并减少存储。 Flink 压缩过滤器使用 TTL 检查状态条目的过期时间戳并排除过期值。该功能可以在 StateTtlConfig 中配置:Java代码:import org.apache.flink.api.common.state.StateTtlConfig; StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupInRocksdbCompactFilter(1000, Time.hours(1)) .build();Scala代码:import org.apache.flink.api.common.state.StateTtlConfig val ttlConfig = StateTtlConfig .newBuilder(Time.seconds(1)) .cleanupInRocksdbCompactFilter(1000, Time.hours(1)) .buildPython代码:from pyflink.common.time import Time from pyflink.datastream.state import StateTtlConfig ttl_config = StateTtlConfig \ .new_builder(Time.seconds(1)) \ .cleanup_in_rocksdb_compact_filter(1000, Time.hours(1)) \ .build()RocksDB 压缩过滤器每次处理一定数量的状态条目后都会从 Flink 查询当前时间戳,用于检查过期情况。您可以更改它并将自定义值传递给 StateTtlConfig.newBuilder(…).cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries) 方法。更频繁地更新时间戳可以提高清理速度,但会降低压缩性能,因为它使用来自本机代码的 JNI 调用。 RocksDB 后端的默认后台清理会在每次处理 1000 个条目时查询当前时间戳。定期压缩可以加快过期状态条目的清理速度,特别是对于很少访问的状态条目。早于该值的文件将被拾取进行压缩,并重新写入到与之前相同的级别。它确保文件定期通过压缩过滤器。您可以更改它并将自定义值传递给 StateTtlConfig.newBuilder(…).cleanupInRocksdbCompactFilter(long queryTimeAfterNumEntries, Time periodicalCompactionTime) 方法。定期压缩秒数的默认值为 30 天。您可以将其设置为 0 以关闭定期压缩,或设置一个较小的值以加速过期状态条目清理,但它会触发更多压缩。您可以通过激活 FlinkCompactionFilter 的调试级别来从 RocksDB 过滤器的本机代码激活调试日志:log4j.logger.org.rocksdb.FlinkCompactionFilter=DEBUG笔记:在压缩过程中调用 TTL 过滤器会减慢速度。 TTL 过滤器必须解析上次访问的时间戳,并检查正在压缩的每个键的每个存储状态条目的过期时间。如果是集合状态类型(列表或映射),还会针对每个存储的元素调用检查。如果此功能与包含非固定字节长度元素的列表状态一起使用,则本机 TTL 过滤器必须在每个状态条目(其中至少第一个元素已过期)额外通过 JNI 调用该元素的 Flink java 类型序列化器确定下一个未过期元素的偏移量。对于现有作业,可以在 StateTtlConfig 中随时激活或停用此清理策略,例如从保存点重新启动后。周期性压缩仅在启用 TTL 时才起作用。
0
0
0
浏览量1256
打酱油的后端

Flink系列之:JDBC SQL 连接器

一、JDBC SQL 连接器Scan Source: BoundedLookup Source: Sync ModeSink: BatchSink: Streaming Append & Upsert ModeJDBC 连接器允许使用 JDBC 驱动向任意类型的关系型数据库读取或者写入数据。本文档描述了针对关系型数据库如何通过建立 JDBC 连接器来执行 SQL 查询。如果在 DDL 中定义了主键,JDBC sink 将以 upsert 模式与外部系统交换 UPDATE/DELETE 消息;否则,它将以 append 模式与外部系统交换消息且不支持消费 UPDATE/DELETE 消息。二、依赖在连接到具体数据库时,也需要对应的驱动依赖,目前支持的驱动如下:三、创建 JDBC 表JDBC table 可以按如下定义:-- 在 Flink SQL 中注册一张 MySQL 表 'users' CREATE TABLE MyUserTable ( id BIGINT, name STRING, age INT, status BOOLEAN, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://localhost:3306/mydatabase', 'table-name' = 'users' ); -- 从另一张表 "T" 将数据写入到 JDBC 表中 INSERT INTO MyUserTable SELECT id, name, age, status FROM T; -- 查看 JDBC 表中的数据 SELECT id, name, age, status FROM MyUserTable; -- JDBC 表在时态表关联中作为维表 SELECT * FROM myTopic LEFT JOIN MyUserTable FOR SYSTEM_TIME AS OF myTopic.proctime ON myTopic.key = MyUserTable.id;这段代码是在Flink SQL中注册一个MySQL表"users",表结构包括id、name、age、status等字段,并设置id作为主键。接下来通过INSERT INTO语句,从名为"T"的表中将数据写入到JDBC表"MyUserTable"中。INSERT INTO语句用于将"T"表中的id、name、age、status字段的值插入到"MyUserTable"表中。然后使用SELECT语句查看JDBC表中的数据,返回id、name、age、status字段的值。最后一段语句是将JDBC表"MyUserTable"作为维表,与实时流数据源"myTopic"进行时态表关联。通过FOR SYSTEM_TIME AS OF子句指定以"myTopic.proctime"字段的时间为基准,将"myTopic"的key字段与"MyUserTable"的id字段进行关联查询,返回所有字段的值。通过这段代码,可以实现将数据从一个表写入到MySQL表中,并在流处理中进行关联查询,从而实现时态表的操作。四、连接器参数五、键处理当写入数据到外部数据库时,Flink 会使用 DDL 中定义的主键。如果定义了主键,则连接器将以 upsert 模式工作,否则连接器将以 append 模式工作。在 upsert 模式下,Flink 将根据主键判断插入新行或者更新已存在的行,这种方式可以确保幂等性。为了确保输出结果是符合预期的,推荐为表定义主键并且确保主键是底层数据库中表的唯一键或主键。在 append 模式下,Flink 会把所有记录解释为 INSERT 消息,如果违反了底层数据库中主键或者唯一约束,INSERT 插入可能会失败。六、分区扫描为了在并行 Source task 实例中加速读取数据,Flink 为 JDBC table 提供了分区扫描的特性。如果下述分区扫描参数中的任一项被指定,则下述所有的分区扫描参数必须都被指定。这些参数描述了在多个 task 并行读取数据时如何对表进行分区。 scan.partition.column 必须是相关表中的数字、日期或时间戳列。注意,scan.partition.lower-bound 和 scan.partition.upper-bound 用于决定分区的起始位置和过滤表中的数据。如果是批处理作业,也可以在提交 flink 作业之前获取最大值和最小值。scan.partition.column:输入用于进行分区的列名。scan.partition.num:分区数。scan.partition.lower-bound:第一个分区的最小值。scan.partition.upper-bound:最后一个分区的最大值。七、Lookup CacheJDBC 连接器可以用在时态表关联中作为一个可 lookup 的 source (又称为维表),当前只支持同步的查找模式。默认情况下,lookup cache 是未启用的,你可以将 lookup.cache 设置为 PARTIAL 参数来启用。lookup cache 的主要目的是用于提高时态表关联 JDBC 连接器的性能。默认情况下,lookup cache 不开启,所以所有请求都会发送到外部数据库。 当 lookup cache 被启用时,每个进程(即 TaskManager)将维护一个缓存。Flink 将优先查找缓存,只有当缓存未查找到时才向外部数据库发送请求,并使用返回的数据更新缓存。 当缓存命中最大缓存行 lookup.partial-cache.max-rows 或当行超过 lookup.partial-cache.expire-after-write 或 lookup.partial-cache.expire-after-access 指定的最大存活时间时,缓存中的行将被设置为已过期。 缓存中的记录可能不是最新的,用户可以将缓存记录超时设置为一个更小的值以获得更好的刷新数据,但这可能会增加发送到数据库的请求数。所以要做好吞吐量和正确性之间的平衡。默认情况下,flink 会缓存主键的空查询结果,你可以通过将 lookup.partial-cache.cache-missing-key 设置为 false 来切换行为。八、幂等写入如果在 DDL 中定义了主键,JDBC sink 将使用 upsert 语义而不是普通的 INSERT 语句。upsert 语义指的是如果底层数据库中存在违反唯一性约束,则原子地添加新行或更新现有行,这种方式确保了幂等性。如果出现故障,Flink 作业会从上次成功的 checkpoint 恢复并重新处理,这可能导致在恢复过程中重复处理消息。强烈推荐使用 upsert 模式,因为如果需要重复处理记录,它有助于避免违反数据库主键约束和产生重复数据。除了故障恢复场景外,数据源(kafka topic)也可能随着时间的推移自然地包含多个具有相同主键的记录,这使得 upsert 模式是用户期待的。由于 upsert 没有标准的语法,因此下表描述了不同数据库的 DML 语法:九、JDBC CatalogJdbcCatalog 允许用户通过 JDBC 协议将 Flink 连接到关系数据库。目前,JDBC Catalog 有两个实现,即 Postgres Catalog 和 MySQL Catalog。目前支持如下 catalog 方法。其他方法目前尚不支持。// Postgres Catalog & MySQL Catalog 支持的方法 databaseExists(String databaseName); listDatabases(); getDatabase(String databaseName); listTables(String databaseName); getTable(ObjectPath tablePath); tableExists(ObjectPath tablePath);其他的 Catalog 方法现在尚不支持。十、JDBC Catalog 的使用本小节主要描述如果创建并使用 Postgres Catalog 或 MySQL Catalog。 请参阅 Dependencies 部分了解如何配置 JDBC 连接器和相应的驱动。JDBC catalog 支持以下参数:name:必填,catalog 的名称。default-database:必填,默认要连接的数据库。username:必填,Postgres/MySQL 账户的用户名。password:必填,账户的密码。base-url:必填,(不应该包含数据库名)对于 Postgres Catalog base-url 应为 “jdbc:postgresql://:” 的格式。对于 MySQL Catalog base-url 应为 “jdbc:mysql://:” 的格式。SQL:CREATE CATALOG my_catalog WITH( 'type' = 'jdbc', 'default-database' = '...', 'username' = '...', 'password' = '...', 'base-url' = '...' ); USE CATALOG my_catalog;Java:EnvironmentSettings settings = EnvironmentSettings.inStreamingMode(); TableEnvironment tableEnv = TableEnvironment.create(settings); String name = "my_catalog"; String defaultDatabase = "mydb"; String username = "..."; String password = "..."; String baseUrl = "..." JdbcCatalog catalog = new JdbcCatalog(name, defaultDatabase, username, password, baseUrl); tableEnv.registerCatalog("my_catalog", catalog); // 设置 JdbcCatalog 为会话的当前 catalog tableEnv.useCatalog("my_catalog");Scala:val settings = EnvironmentSettings.inStreamingMode() val tableEnv = TableEnvironment.create(settings) val name = "my_catalog" val defaultDatabase = "mydb" val username = "..." val password = "..." val baseUrl = "..." val catalog = new JdbcCatalog(name, defaultDatabase, username, password, baseUrl) tableEnv.registerCatalog("my_catalog", catalog) // 设置 JdbcCatalog 为会话的当前 catalog tableEnv.useCatalog("my_catalog")Python:from pyflink.table.catalog import JdbcCatalog environment_settings = EnvironmentSettings.in_streaming_mode() t_env = TableEnvironment.create(environment_settings) name = "my_catalog" default_database = "mydb" username = "..." password = "..." base_url = "..." catalog = JdbcCatalog(name, default_database, username, password, base_url) t_env.register_catalog("my_catalog", catalog) # 设置 JdbcCatalog 为会话的当前 catalog t_env.use_catalog("my_catalog")YAMLexecution: ... current-catalog: my_catalog # 设置目标 JdbcCatalog 为会话的当前 catalog current-database: mydb catalogs: - name: my_catalog type: jdbc default-database: mydb username: ... password: ... base-url: ...十一、JDBC Catalog for PostgreSQLPostgreSQL 元空间映射除了数据库之外,postgreSQL 还有一个额外的命名空间 schema。一个 Postgres 实例可以拥有多个数据库,每个数据库可以拥有多个 schema,其中一个 schema 默认名为 “public”,每个 schema 可以包含多张表。 在 Flink 中,当查询由 Postgres catalog 注册的表时,用户可以使用 schema_name.table_name 或只有 table_name,其中 schema_name 是可选的,默认值为 “public”。因此,Flink Catalog 和 Postgres 之间的元空间映射如下:Flink 目录元空间结构Postgres 元空间结构catalog name (defined in Flink only)N/Adatabase namedatabase nametable name[schema_name.]table_nameFlink 中的 Postgres 表的完整路径应该是 “..<schema.table>”。如果指定了 schema,请注意需要转义 <schema.table>。这里提供了一些访问 Postgres 表的例子:-- 扫描 'public' schema(即默认 schema)中的 'test_table' 表,schema 名称可以省略 SELECT * FROM mypg.mydb.test_table; SELECT * FROM mydb.test_table; SELECT * FROM test_table; -- 扫描 'custom_schema' schema 中的 'test_table2' 表, -- 自定义 schema 不能省略,并且必须与表一起转义。 SELECT * FROM mypg.mydb.`custom_schema.test_table2` SELECT * FROM mydb.`custom_schema.test_table2`; SELECT * FROM `custom_schema.test_table2`;十二、JDBC Catalog for MySQLMySQL 元空间映射MySQL 实例中的数据库与 MySQL Catalog 注册的 catalog 下的数据库处于同一个映射层级。一个 MySQL 实例可以拥有多个数据库,每个数据库可以包含多张表。 在 Flink 中,当查询由 MySQL catalog 注册的表时,用户可以使用 database.table_name 或只使用 table_name,其中 database 是可选的,默认值为创建 MySQL Catalog 时指定的默认数据库。因此,Flink Catalog 和 MySQL catalog 之间的元空间映射如下:Flink 目录元空间结构Mysql 元空间结构catalog name (defined in Flink only)N/Adatabase namedatabase nametable nametable_nameFlink 中的 MySQL 表的完整路径应该是 “<catalog>.<db>.<table>”。这里提供了一些访问 MySQL 表的例子:-- 扫描 默认数据库中的 'test_table' 表 SELECT * FROM mysql_catalog.mydb.test_table; SELECT * FROM mydb.test_table; SELECT * FROM test_table; -- 扫描 'given_database' 数据库中的 'test_table2' 表, SELECT * FROM mysql_catalog.given_database.test_table2; SELECT * FROM given_database.test_table2;十三、数据类型映射Flink 支持连接到多个使用方言(dialect)的数据库,如 MySQL、Oracle、PostgreSQL、Derby 等。其中,Derby 通常是用于测试目的。下表列出了从关系数据库数据类型到 Flink SQL 数据类型的类型映射,映射表可以使得在 Flink 中定义 JDBC 表更加简单。
0
0
0
浏览量1793
打酱油的后端

Flink系列之:集合操作

一、集合操作适用于流、批操作二、UNIONUNION 和 UNION ALL 返回两个表中的数据。 UNION 会去重,UNION ALL 不会去重。Flink SQL> create view t1(s) as values ('c'), ('a'), ('b'), ('b'), ('c'); Flink SQL> create view t2(s) as values ('d'), ('e'), ('a'), ('b'), ('b'); Flink SQL> (SELECT s FROM t1) UNION (SELECT s FROM t2); +---+ | s| +---+ | c| | a| | b| | d| | e| +---+ Flink SQL> (SELECT s FROM t1) UNION ALL (SELECT s FROM t2); +---+ | c| +---+ | c| | a| | b| | b| | c| | d| | e| | a| | b| | b| +---+三、INTERSECTINTERSECT 和 INTERSECT ALL 返回两个表中共有的数据。 INTERSECT 会去重,INTERSECT ALL 不会去重。Flink SQL> (SELECT s FROM t1) INTERSECT (SELECT s FROM t2); +---+ | s| +---+ | a| | b| +---+ Flink SQL> (SELECT s FROM t1) INTERSECT ALL (SELECT s FROM t2); +---+ | s| +---+ | a| | b| | b| +---+四、EXCEPTEXCEPT 和 EXCEPT ALL 返回在一个表中存在,但在另一个表中不存在数据。 EXCEPT 会去重,EXCEPT ALL不会去重。Flink SQL> (SELECT s FROM t1) EXCEPT (SELECT s FROM t2); +---+ | s | +---+ | c | +---+ Flink SQL> (SELECT s FROM t1) EXCEPT ALL (SELECT s FROM t2); +---+ | s | +---+ | c | | c | +---+五、IN如果表达式(可以是列,也可以是函数等)存在于子查询的结果中,则返回 true。子查询的表结果必须由一列组成。此列必须与表达式具有相同的数据类型。SELECT user, amount FROM Orders WHERE product IN ( SELECT product FROM NewProducts )优化器会把 IN 条件重写为 join 和 group 操作。对于流式查询,计算查询结果所需的状态可能会根据输入行数而无限增长。你可以设置一个合适的状态 time-to-live(TTL)来淘汰过期数据以防止状态过大。注意:这可能会影响查询结果的正确性。六、XISTSSELECT user, amount FROM Orders WHERE product EXISTS ( SELECT product FROM NewProducts )如果子查询返回至少一行,则为 true。只支持能被重写为 join 和 group 的操作。优化器会把 EXIST 重写为 join 和 group 操作.对于流式查询,计算查询结果所需的状态可能会根据输入行数而无限增长。你可以设置一个合适的状态 time-to-live(TTL)来淘汰过期数据以防止状态过大。注意:这可能会影响查询结果的正确性。
0
0
0
浏览量1989
打酱油的后端

Flink系列之:监控反压

一、反压如果你看到一个 task 发生 反压警告(例如: High),意味着它生产数据的速率比下游 task 消费数据的速率要快。 在工作流中数据记录是从上游向下游流动的(例如:从 Source 到 Sink)。反压沿着相反的方向传播,沿着数据流向上游传播。以一个简单的 Source -> Sink job 为例。如果看到 Source 发生了警告,意味着 Sink 消费数据的速率比 Source 生产数据的速率要慢。 Sink 正在向上游的 Source 算子产生反压。二、Task 性能指标Task(SubTask)的每个并行实例都可以用三个一组的指标评价:backPressuredTimeMsPerSecond,subtask 被反压的时间idleTimeMsPerSecond,subtask 等待某类处理的时间busyTimeMsPerSecond,subtask 实际工作时间 在任何时间点,这三个指标相加都约等于1000ms。这些指标每两秒更新一次,上报的值表示 subtask 在最近两秒被反压(或闲或忙)的平均时长。 当你的工作负荷是变化的时需要尤其引起注意。比如,一个以恒定50%负载工作的 subtask 和另一个每秒钟在满负载和闲置切换的 subtask 的busyTimeMsPerSecond值相同,都是500ms。在内部,反压根据输出 buffers 的可用性来进行判断的。 如果一个 task 没有可用的输出 buffers,那么这个 task 就被认定是在被反压。 相反,如果有可用的输入,则可认定为闲置,三、示例WebUI 集合了所有 subTasks 的反压和繁忙指标的最大值,并在 JobGraph 中将集合的值进行显示。除了显示原始的数值,tasks 也用颜色进行了标记,使检查更加容易。闲置的 tasks 为蓝色,完全被反压的 tasks 为黑色,完全繁忙的 tasks 被标记为红色。 中间的所有值都表示为这三种颜色之间的过渡色四、反压状态在 Job Overview 旁的 Back Pressure 选项卡中,你可以找到更多细节指标。如果你看到 subtasks 的状态为 OK 表示没有反压。HIGH 表示这个 subtask 被反压。状态用如下定义:OK: 0% <= 反压比例 <= 10%LOW: 10% < 反压比例 <= 50%HIGH: 50% < 反压比例 <= 100%除此之外,你还可以找到每一个 subtask 被反压、闲置或是繁忙的时间百分比。
0
0
0
浏览量1450
打酱油的后端

Flink系列之:Table API Connectors之JSON Format

一、JSON FormatJSON Format 能读写 JSON 格式的数据。当前,JSON schema 是从 table schema 中自动推导而得的。二、依赖为了使用 Json 格式,使用构建自动化工具(例如 Maven 或 SBT)的项目和带有 SQL JAR 包的 SQL 客户端都需要以下依赖项。<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-json</artifactId> <version>1.18.0</version> </dependency>三、创建一张基于 JSON Format 的表以下是一个利用 Kafka 以及 JSON Format 构建表的例子。CREATE TABLE user_behavior ( user_id BIGINT, item_id BIGINT, category_id BIGINT, behavior STRING, ts TIMESTAMP(3) ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'format' = 'json', 'json.fail-on-missing-field' = 'false', 'json.ignore-parse-errors' = 'true' )四、Format 参数五、数据类型映射关系当前,JSON schema 将会自动从 table schema 之中自动推导得到。不支持显式地定义 JSON schema。在 Flink 中,JSON Format 使用 jackson databind API 去解析和生成 JSON。下表列出了 Flink 中的数据类型与 JSON 中的数据类型的映射关系。
0
0
0
浏览量358
打酱油的后端

Flink系列之:Table API Connectors之Debezium

一、DebeziumDebezium 是一个 CDC(Changelog Data Capture,变更数据捕获)的工具,可以把来自 MySQL、PostgreSQL、Oracle、Microsoft SQL Server 和许多其他数据库的更改实时流式传输到 Kafka 中。 Debezium 为变更日志提供了统一的格式结构,并支持使用 JSON 和 Apache Avro 序列化消息。Flink 支持将 Debezium JSON 和 Avro 消息解析为 INSERT / UPDATE / DELETE 消息到 Flink SQL 系统中。在很多情况下,利用这个特性非常的有用,例如将增量数据从数据库同步到其他系统日志审计数据库的实时物化视图关联维度数据库的变更历史,等等。Flink 还支持将 Flink SQL 中的 INSERT / UPDATE / DELETE 消息编码为 Debezium 格式的 JSON 或 Avro 消息,输出到 Kafka 等存储中。 但需要注意的是,目前 Flink 还不支持将 UPDATE_BEFORE 和 UPDATE_AFTER 合并为一条 UPDATE 消息。因此,Flink 将 UPDATE_BEFORE 和 UPDATE_AFTER 分别编码为 DELETE 和 INSERT 类型的 Debezium 消息。二、依赖Debezium Avro为了使用 Debezium 格式,使用构建自动化工具(例如 Maven 或 SBT)的项目和带有 SQL JAR 包的 SQL 客户端都需要以下依赖项。<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-avro-confluent-registry</artifactId> <version>1.18.0</version> </dependency>Debezium Json为了使用 Debezium 格式,使用构建自动化工具(例如 Maven 或 SBT)的项目和带有 SQL JAR 包的 SQL 客户端都需要以下依赖项。<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-json</artifactId> <version>1.18.0</version> </dependency> 三、使用Debezium FormatDebezium 为变更日志提供了统一的格式,这是一个 JSON 格式的从 MySQL product 表捕获的更新操作的简单示例:{ "before": { "id": 111, "name": "scooter", "description": "Big 2-wheel scooter", "weight": 5.18 }, "after": { "id": 111, "name": "scooter", "description": "Big 2-wheel scooter", "weight": 5.15 }, "source": {...}, "op": "u", "ts_ms": 1589362330904, "transaction": null }MySQL 产品表有4列(id、name、description、weight)。上面的 JSON 消息是 products 表上的一条更新事件,其中 id = 111 的行的 weight 值从 5.18 更改为 5.15。假设此消息已同步到 Kafka 主题 products_binlog,则可以使用以下 DDL 来使用此主题并解析更改事件。CREATE TABLE topic_products ( -- schema 与 MySQL 的 products 表完全相同 id BIGINT, name STRING, description STRING, weight DECIMAL(10, 2) ) WITH ( 'connector' = 'kafka', 'topic' = 'products_binlog', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', -- 使用 'debezium-json' format 来解析 Debezium 的 JSON 消息 -- 如果 Debezium 用 Avro 编码消息,请使用 'debezium-avro-confluent' 'format' = 'debezium-json' -- 如果 Debezium 用 Avro 编码消息,请使用 'debezium-avro-confluent' )在某些情况下,用户在设置 Debezium Kafka Connect 时,可能会开启 Kafka 的配置 ‘value.converter.schemas.enable’,用来在消息体中包含 schema 信息。然后,Debezium JSON 消息可能如下所示:{ "schema": {...}, "payload": { "before": { "id": 111, "name": "scooter", "description": "Big 2-wheel scooter", "weight": 5.18 }, "after": { "id": 111, "name": "scooter", "description": "Big 2-wheel scooter", "weight": 5.15 }, "source": {...}, "op": "u", "ts_ms": 1589362330904, "transaction": null } }为了解析这一类信息,你需要在上述 DDL WITH 子句中添加选项 ‘debezium-json.schema-include’ = ‘true’(默认为 false)。通常情况下,建议不要包含 schema 的描述,因为这样会使消息变得非常冗长,并降低解析性能。在将主题注册为 Flink 表之后,可以将 Debezium 消息用作变更日志源。-- MySQL "products" 的实时物化视图 -- 计算相同产品的最新平均重量 SELECT name, AVG(weight) FROM topic_products GROUP BY name; -- 将 MySQL "products" 表的所有数据和增量更改同步到 -- Elasticsearch "products" 索引,供将来查找 INSERT INTO elasticsearch_products SELECT * FROM topic_products;四、可用元数据以下格式元数据可以在表定义中公开为只读(虚拟)列。注意 仅当相应的连接器转发格式元数据时,格式元数据字段才可用。目前,只有 Kafka 连接器能够公开其值格式的元数据字段。以下示例展示了如何访问 Kafka 中的 Debezium 元数据字段:CREATE TABLE KafkaTable ( origin_ts TIMESTAMP(3) METADATA FROM 'value.ingestion-timestamp' VIRTUAL, event_time TIMESTAMP(3) METADATA FROM 'value.source.timestamp' VIRTUAL, origin_database STRING METADATA FROM 'value.source.database' VIRTUAL, origin_schema STRING METADATA FROM 'value.source.schema' VIRTUAL, origin_table STRING METADATA FROM 'value.source.table' VIRTUAL, origin_properties MAP<STRING, STRING> METADATA FROM 'value.source.properties' VIRTUAL, user_id BIGINT, item_id BIGINT, behavior STRING ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'value.format' = 'debezium-json' );五、Format参数Flink 提供了 debezium-avro-confluent 和 debezium-json 两种 format 来解析 Debezium 生成的 JSON 格式和 Avro 格式的消息。 请使用 debezium-avro-confluent 来解析 Debezium 的 Avro 消息,使用 debezium-json 来解析 Debezium 的 JSON 消息。Debezium AvroDebezium Json六、重复的变更事件在正常的操作环境下,Debezium 应用能以 exactly-once 的语义投递每条变更事件。在这种情况下,Flink 消费 Debezium 产生的变更事件能够工作得很好。 然而,当有故障发生时,Debezium 应用只能保证 at-least-once 的投递语义。 这也意味着,在非正常情况下,Debezium 可能会投递重复的变更事件到 Kafka 中,当 Flink 从 Kafka 中消费的时候就会得到重复的事件。 这可能会导致 Flink query 的运行得到错误的结果或者非预期的异常。因此,建议在这种情况下,将作业参数 table.exec.source.cdc-events-duplicate 设置成 true,并在该 source 上定义 PRIMARY KEY。 框架会生成一个额外的有状态算子,使用该 primary key 来对变更事件去重并生成一个规范化的 changelog 流。“table.exec.source.cdc-events-duplicate” 是 Flink 的一个配置选项,用于处理 Change Data Capture (CDC) 事件的重复数据。CDC 是一种常见的数据库技术,用于捕获数据库中的数据更改,并将其作为事件流进行处理。在 Flink 中,“table.exec.source.cdc-events-duplicate” 选项用于指定如何处理 CDC 事件流中的重复数据。具体来说,它可以接受以下两个值:“strict”:严格模式。如果启用了严格模式,Flink 会严格检查事件流中是否存在重复的 CDC 事件。如果检测到重复事件出现,Flink 会抛出一个异常并停止任务执行。“lenient”:宽松模式。如果启用了宽松模式,Flink 会使用一个简单的算法来检测重复事件,并尽量过滤掉这些重复事件。但是,它不能保证完全消除重复事件的可能性。使用时,可以在 Flink 的配置文件或在代码中通过设置相应的属性来指定 “table.exec.source.cdc-events-duplicate” 的值。例如,在 Flink 的配置文件中,可以添加以下行来启用宽松模式:table.exec.source.cdc-events-duplicate: lenient七、消费 Debezium Postgres Connector 产生的数据如果你正在使用 Debezium PostgreSQL Connector 捕获变更到 Kafka,请确保被监控表的 REPLICA IDENTITY 已经被配置成 FULL 了,默认值是 DEFAULT。 否则,Flink SQL 将无法正确解析 Debezium 数据。当配置为 FULL 时,更新和删除事件将完整包含所有列的之前的值。当为其他配置时,更新和删除事件的 “before” 字段将只包含 primary key 字段的值,或者为 null(没有 primary key)。 你可以通过运行 ALTER TABLE REPLICA IDENTITY FULL 来更改 REPLICA IDENTITY 的配置。八、数据类型映射目前,Debezium Format 使用 JSON Format 进行序列化和反序列化。当前,JSON schema 将会自动从 table schema 之中自动推导得到。不支持显式地定义 JSON schema。在 Flink 中,JSON Format 使用 jackson databind API 去解析和生成 JSON。下表列出了 Flink 中的数据类型与 JSON 中的数据类型的映射关系。
0
0
0
浏览量1299
打酱油的后端

Flink系列之:窗口Top-N

一、窗口Top-N适用于流、批一体窗口 Top-N 是特殊的 Top-N,它返回每个分区键的每个窗口的N个最小或最大值。与普通Top-N不同,窗口Top-N只在窗口最后返回汇总的Top-N数据,不会产生中间结果。窗口 Top-N 会在窗口结束后清除不需要的中间状态。 因此,窗口 Top-N 适用于用户不需要每条数据都更新Top-N结果的场景,相对普通Top-N来说性能更好。通常,窗口 Top-N 直接用于 窗口表值函数上。 另外,窗口 Top-N 可以用于基于 窗口表值函数 的操作之上,比如 窗口聚合,窗口 Top-N 和 窗口关联。窗口 Top-N 的语法和普通的 Top-N 相同。除此之外,窗口 Top-N 需要 PARTITION BY 子句包含 窗口表值函数 或 窗口聚合 产生的 window_start 和 window_end。 否则优化器无法翻译。下面展示了窗口 Top-N 的语法:SELECT [column_list] FROM ( SELECT [column_list], ROW_NUMBER() OVER (PARTITION BY window_start, window_end [, col_key1...] ORDER BY col1 [asc|desc][, col2 [asc|desc]...]) AS rownum FROM table_name) -- relation applied windowing TVF WHERE rownum <= N [AND conditions]二、示例:在窗口聚合后进行窗口 Top-N下面的示例展示了在10分钟的滚动窗口上计算销售额位列前三的供应商。-- tables must have time attribute, e.g. `bidtime` in this table Flink SQL> desc Bid; +-------------+------------------------+------+-----+--------+---------------------------------+ | name | type | null | key | extras | watermark | +-------------+------------------------+------+-----+--------+---------------------------------+ | bidtime | TIMESTAMP(3) *ROWTIME* | true | | | `bidtime` - INTERVAL '1' SECOND | | price | DECIMAL(10, 2) | true | | | | | item | STRING | true | | | | | supplier_id | STRING | true | | | | +-------------+------------------------+------+-----+--------+---------------------------------+ Flink SQL> SELECT * FROM Bid; +------------------+-------+------+-------------+ | bidtime | price | item | supplier_id | +------------------+-------+------+-------------+ | 2020-04-15 08:05 | 4.00 | A | supplier1 | | 2020-04-15 08:06 | 4.00 | C | supplier2 | | 2020-04-15 08:07 | 2.00 | G | supplier1 | | 2020-04-15 08:08 | 2.00 | B | supplier3 | | 2020-04-15 08:09 | 5.00 | D | supplier4 | | 2020-04-15 08:11 | 2.00 | B | supplier3 | | 2020-04-15 08:13 | 1.00 | E | supplier1 | | 2020-04-15 08:15 | 3.00 | H | supplier2 | | 2020-04-15 08:17 | 6.00 | F | supplier5 | +------------------+-------+------+-------------+ Flink SQL> SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY window_start, window_end ORDER BY price DESC) as rownum FROM ( SELECT window_start, window_end, supplier_id, SUM(price) as price, COUNT(*) as cnt FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end, supplier_id ) ) WHERE rownum <= 3; +------------------+------------------+-------------+-------+-----+--------+ | window_start | window_end | supplier_id | price | cnt | rownum | +------------------+------------------+-------------+-------+-----+--------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | supplier1 | 6.00 | 2 | 1 | | 2020-04-15 08:00 | 2020-04-15 08:10 | supplier4 | 5.00 | 1 | 2 | | 2020-04-15 08:00 | 2020-04-15 08:10 | supplier2 | 4.00 | 1 | 3 | | 2020-04-15 08:10 | 2020-04-15 08:20 | supplier5 | 6.00 | 1 | 1 | | 2020-04-15 08:10 | 2020-04-15 08:20 | supplier2 | 3.00 | 1 | 2 | | 2020-04-15 08:10 | 2020-04-15 08:20 | supplier3 | 2.00 | 1 | 3 | +------------------+------------------+-------------+-------+-----+--------+注意: 为了更好地理解窗口行为,这里把 timestamp 值后面的0去掉了。例如:在 Flink SQL Client 中,如果类型是 TIMESTAMP(3) ,2020-04-15 08:05 应该显示成 2020-04-15 08:05:00.000 。这条Flink SQL查询的目标是在表Bid中根据时间窗口对数据进行分组,并找出每个窗口内价格最高的三个供应商。首先,在FROM子句中,使用TUMBLE函数对Bid表进行分区,每个分区的时间窗口大小为10分钟,并指定bidtime作为分区依据。然后,将其结果作为内部查询的输入表。在内部查询中,使用GROUP BY子句将数据按窗口的开始时间(window_start)、结束时间(window_end)和供应商ID(supplier_id)进行分组。并计算每个分组的价格总和(SUM(price))和行数(COUNT(*))。同时,使用ROW_NUMBER()函数在每个窗口分组内按价格降序排列,并为每行分配一个行号(rownum)。最后,在外部查询中,筛选出行号(rownum)小于等于3的记录,并返回窗口的开始时间、结束时间、供应商ID、价格总和、行数和行号。最终的查询结果将包括每个窗口内价格最高的三个供应商的信息。三、在窗口表值函数后进行窗口 Top-N下面的示例展示了在10分钟的滚动窗口上计算价格位列前三的数据。Flink SQL> SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY window_start, window_end ORDER BY price DESC) as rownum FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) ) WHERE rownum <= 3; +------------------+-------+------+-------------+------------------+------------------+--------+ | bidtime | price | item | supplier_id | window_start | window_end | rownum | +------------------+-------+------+-------------+------------------+------------------+--------+ | 2020-04-15 08:05 | 4.00 | A | supplier1 | 2020-04-15 08:00 | 2020-04-15 08:10 | 2 | | 2020-04-15 08:06 | 4.00 | C | supplier2 | 2020-04-15 08:00 | 2020-04-15 08:10 | 3 | | 2020-04-15 08:09 | 5.00 | D | supplier4 | 2020-04-15 08:00 | 2020-04-15 08:10 | 1 | | 2020-04-15 08:11 | 2.00 | B | supplier3 | 2020-04-15 08:10 | 2020-04-15 08:20 | 3 | | 2020-04-15 08:15 | 3.00 | H | supplier2 | 2020-04-15 08:10 | 2020-04-15 08:20 | 2 | | 2020-04-15 08:17 | 6.00 | F | supplier5 | 2020-04-15 08:10 | 2020-04-15 08:20 | 1 | +------------------+-------+------+-------------+------------------+------------------+--------+注意: 为了更好地理解窗口行为,这里把 timestamp 值后面的0去掉了。例如:在 Flink SQL Client 中,如果类型是 TIMESTAMP(3) ,2020-04-15 08:05 应该显示成 2020-04-15 08:05:00.000 。这个Flink SQL语句的目标是从表Bid中选择特定的列,并为每个时间窗口内的数据分配一个行号(rownum)。行号的分配是基于每个窗口内数据的价格进行降序排列。首先,在内部查询中,使用TUMBLE函数将Bid表按照bidtime进行分区,每个分区代表一个时间窗口,窗口大小为10分钟。然后将分区结果作为输入表。接下来,在内部查询中,使用ROW_NUMBER()函数为每个窗口分组内的数据分配行号。PARTITION BY子句指定按窗口开始时间(window_start)和结束时间(window_end)进行分组,ORDER BY子句指定按价格(price)降序排列。这样,每个时间窗口内的数据就会被分配一个行号。最后,在外部查询中,筛选出行号(rownum)小于等于3的记录,这意味着只保留每个时间窗口内前三个价格最高的数据。最终的查询结果将包括原始表中的所有列,以及每个时间窗口的开始时间、结束时间和行号。这样就可以查看每个窗口内价格最高的前三个数据,同时保留其他列的信息。四、限制目前,Flink只支持在滚动,滑动和累计 窗口表值函数后进行窗口 Top-N。基于会话窗口的Top-N将在将来版本中支持。
0
0
0
浏览量527
打酱油的后端

Flink系列Table API和SQL之:动态表、持续查询、将流转换成动态表、更新查询、追加查询

Flink系列Table API和SQL之:动态表、持续查询、将流转换成动态表、更新查询、追加查询、将动态表转换为流、更新插入流一、表和流的转换Flink中使用表和SQL基本上跟其他场景是一样的。不过对于表和流的转换,却稍显复杂。当我们将一个Table转换成DataStream时,有"仅插入流"(Insert-Only Streams)和"更新日志流"(Changelog Streams)两种不同的方式,具体使用哪种方式取决于表中是否存在更新操作。这种麻烦其实是不可避免的。Table API和SQL本质上都是基于关系型表的操作方式。关系型表本身是有界的,更适合批处理的场景。所以在MySQL、Hive这样的固定数据集中进行查询,使用SQL就会显得得心应手。而对于Flink这样的流处理框架来说,要处理的是源源不断到来的无界数据流,无法等到暑假都到齐再做查询,每来一条数据就应该更新一次结果。这时如果一定要使用表和SQL进行处理,就会显得别扭,需要引入一些特殊的概念。可以将关系型表/SQL与流处理做一个对比。关系型表/SQL流处理流处理字段元组的有界集合字段元祖的无限序列查询可以访问完整的数据输入无法访问到所有数据,必须持续等待流式输入对数据的访问查询终止条件生成固定大小的结果集合终止永不停止,根据持续收到的数据不断更新查询结果流处理面对的数据是连续不断的,这导致了流处理中的表跟我们熟悉的关系型数据库中的表完全不同。基于表的查询操作,也就有了新的含义。希望把流数据换成表的形式,那么这表中的数据就会不断增长。如果进一步基于表执行SQL查询,那么得到的结果就不是一成不变的,而是会随着新数据的到来持续更新。二、动态表当流中有新数据到来,初始的表中会查入一行,而基于这个表定义的SQL查询,就应该在之前的基础上更新结果。这样得到的表就会不断地动态变化,被称为动态表(Dynamic Tables)。动态表是Flink在Table API和SQL中的核心概念,为流数据处理提供了表和SQL支持。所熟悉的表一般用来做批处理,面向的是固定的数据集,可以认为是"静态表"。而动态表则完全不同,它里面的数据会随时间变化。其实动态表的概念,在传统的关系型数据库中已经有所接触。数据库中的表,其实是一系列INSERT、UPDATE和DELETE语句执行的结果。在关系型数据库中,我们一般把它称为更新日志流(changelog stream)。如果我们保存了表在某一时刻的快照(snapshot),那么接下来只要读取更新日志流,就可以得到表之后的变化过程和最终结果了。在很多高级关系型数据库(比如Oracle、DB2)中都有物化视图的概念,可以用来缓存SQL查询的结果。它的更新其实就是不停地处理更新日志流的过程。Flink中的动态表,就借鉴了物化视图的思想。三、持续查询动态表可以像静态的批处理一样进行查询操作。由于数据在不断变化,因此基于它定义的SQL查询也不可能执行一次就得到最终结果。这样依一来,对动态表的查询也就永远不会停止,一直在随着新数据的到来而继续执行。这样的查询就被称作持续查询。对动态表定义的查询操作,都是持续查询,而持续查询的结果也会是一个动态表。由于每次暑假到来都会触发查询操作,因此可以认为一次查询面对的数据集,就是当前输入动态表中收到的所有数据。这相当于是对输入动态表做了一个快照(snapshot),当作有限数据集进行批处理。流式数据的到来会触发连续不断的快照查询,像动画一样连贯起来,就构成了持续查询。持续查询的过程,可以清晰地看到流、动态表和持续查询的关系:持续查询的步骤如下:流stream被转换为动态表dynamic table对动态表进行持续查询(continuous query),生成新的动态表生成的动态表被转换成流。这样,只要API将流和动态表的转换封装起来,就可以直接在数据流上执行SQL查询,用处理表的方式来做流处理了。四、将流转换成动态表为了能够使用SQL来做流处理,必须先把流(stream)转换成动态表如果把流看作一张表,那么流中每个数据的到来,都应该看作是对表的一次插入(Insert)操作,会在表的末尾添加一行数据。因为流是连续不断的,而且之前的输出结果无法改变、只能在后面追加。所以我们其实是通过一个只有插入操作(insert-only)的更新日志(changelog)流,来构建一个表。流转换成动态表的过程: StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp; } }) ); //创建表执行环境 StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); //将dataStream转换成table Table eventTable = tableEnv.fromDataStream(eventStream); //直接写sql进行转换 Table resultTable = tableEnv.sqlQuery("select user,url from " + eventTable); //基于table直接转换 Table resultTable2 = eventTable.select($("user"), $("url")) .where($("user").isEqual("Alice")); //转换成流打印输出 tableEnv.toDataStream(resultTable2).print("result2"); tableEnv.toDataStream(resultTable).print("result"); env.execute();五、更新查询在代码中定义了一个SQL查询Table urlCountTable = tableEnv.sqlQuery("SELECT user,COUNT(url) as cnt FROM EnentTable GROUP BY user")这个查询很简答,主要是分组聚合统计每个用户点击次数。把原始的动态表注册为EventTable,经过查询转换后得到urlCountTable。这个结果动态表中包含两个字断,具体定义如下:[ user: VARCHAR, //用户名 cnt: BIGINT。//用户访问url的次数 ]当原始动态表不停地插入新的数据时,查询得到的urlCountTable会持续地进行更改。由于count数量可能会叠加增长,因此这里的更改操作可以是简单的插入(Insert),也可以是对之前数据的更新(Update)。换句话说,用来定义结果表的更新日志(changelog)流中,包含了INSERT和UPDATE两种操作。这种持续查询被称为更新查询(Update Query),更新查询得到的结果表如果想要转换成DataStream,必须调用toChangelogStream()方法。六、追加查询查询过程用到了分组聚合,结果表中就会产生更新操作。如果我们执行一个简单的条件查询,结果表中就会像原始表EventTable一样,只有插入Insert操作了。Table aliceVisitTable = tableEnv.sqlQuery("SELECT url,user FROM EventTable WHERE user = 'Cary' ");这样的持续查询,就被称为追加查询(Append Query),定义的结果表的更新日志(changelog)流中只有INSERT操作。追加查询得到的结果表,转换成DataStream调用方法没有限制,可以直接用toDataStream(),也可以像更新查询一样调用toChangeStream()。这样看来似乎可以   总结一个规律:只要用到了聚合,在之前的结果上有叠加,就会产生更新操作,就是一个更新查询。但是事实上,更新查询的判断标准是结果表中的数据是否会有UPDATE操作,如果聚合的结果不再改变,那么同样也不是更新查询。可以考虑开一个滚动窗口,统计每一小时内所有用户的点击次数,并在接过表中增加一个endT字段,表示当前统计窗口的结束时间。这时结果表的字段定义如下:[ user: VARCHAR, //用户名 endT: TIMESTAMP, //窗口结束时间 cnt: BIGINT. //用户访问url的次数 ]与之前的分组聚合一样,当原始动态表不停地插入新的数据时,查询得到的结果result会持续地进行更改。比如时间戳在12:00:00到12:59:59之间有四条数据,其中Alice三次点击、Bob一次点击,所以当水位线达到13:00:00时窗口关闭,输出到结果表中的就是新增两条数据[Alice,13:00:00,3]和[Bob,13:00:00,1]。同理,当下一个小时的窗口关闭时,也会将统计结果追加到result表后面,而不会更新之前的数据。我们发现,由于窗口的统计结果是一次性写入结果表的,所以结果表的更新日志流中只会包含插入INSERT操作,而没有更新UPDATE操作,所以这里的持续查询,依然是一个追加(Append)查询。结果表result如果转换成DataStream,可以直接调用toDataStream()方法。七、将动态表转换为流与关系型数据库中的表一样,动态表也可以通过插入(Insert)、更新(Update)、删除(Delete)操作,进行持续的更改。将动态表转换为流或将其写入外部系统时,就需要对这些更改操作进行编码,通过发送编码消息的方式告诉外部系统要执行的操作。在Flink中,Table API和SQL支持三种编码方式: 仅追加流(Append-only):仅通过插入Insert更改来修改的动态表,可以直接转换为仅追加流。这个流中发出的数据,其实就是动态表中新增的每一行。 撤回流:撤回流是包含两类消息的流,添加(add)消息和撤回(retract)消息。 具体的编码规则是:INSERT插入操作编码为add消息。DELETE删除操作编码为retract消息。而UPDATE更新操作则编码为被更改行的retract消息,和更新后行的add消息。这样,我们可以通过编码后的消息指明所有的增删改操作,一个动态表就可以转换为撤回流了。 可以看到,更新操作对于撤回流来说,对应着两个消息,之前数据的撤回(删除)和新数据的插入。将动态表转换为撤回流的过程:用+代表add消息(对应插入INSERT操作)用-代表retract消息(对应删除DELETE操作)当Alice的第一个点击事件到来时,结果表新增一条数据[Alice,1]当Alice的第二个点击事件到来时,结果表会将[Alice,1]更新为[Alice,2],对应的编码就是删除[Alice,1]、插入[Alice,2]。这样当一个外部系统收到这样的两条消息时,就知道要对Alice的点击统计次数进行更新了。八、更新插入流(Upsert)更新插入流只包含两种类型的消息:更新插入(upsert)和删除(delete)消息。所谓upsert其实就是update和insert的合成词,所以对于更新插入流来说,INSERT插入操作和UPDATE更新操作,统一被编码为upsert消息。DELETE删除操作则被编码为delete消息。既然更新插入流中不区分插入(insert)和更新(update),自然会想到一个问题,如果希望更新一行数据时,怎么保证最后做的操作不是插入呢?这就需要动态表中必须有唯一的键(key)。通过这个key进行查询,如果存在对应的数据就做更新(update),如果不存在就直接插入(insert)。这是一个动态表可以转换为更新插入流的必要条件。当然,收到这条流中数据的外部系统,也需要知道这唯一的键(key),这样才能正确地处理消息。将动态表转换为更新插入流的过程:可以看到,更新插入流跟撤回流的主要区别在于,更新操作由于有key的存在,
0
0
0
浏览量1697
打酱油的后端

Flink系列之:SELECT & WHERE clause

一、SELECT & WHERE clauseSELECT 语句的一般语法是:SELECT select_list FROM table_expression [ WHERE boolean_expression ]table_expression 引用任何数据源。它可以是现有表、视图或 VALUES 子句、多个现有表的联接结果或子查询。假设该表在目录中可用,以下代码将从 Orders 中读取所有行。SELECT * FROM Ordersselect_list 规范 * 表示查询将解析所有列。但是,在生产中不鼓励使用 *,因为它会使查询对目录更改的鲁棒性降低。相反,select_list 可以指定可用列的子集或使用所述列进行计算。例如,如果 Orders 具有名为 order_id、price 和 Tax 的列,您可以编写以下查询:SELECT order_id, price + tax FROM Orders查询还可以使用 VALUES 子句使用内联数据。每个元组对应于一行,并且可以提供别名来为每一列分配名称。SELECT order_id, price FROM (VALUES (1, 2.0), (2, 3.1)) AS t (order_id, price)可以基于 WHERE 子句过滤行。SELECT price + tax FROM Orders WHERE id = 10此外,可以在单行的列上调用内置和用户定义的标量函数。用户定义的函数在使用前必须注册到目录中。SELECT PRETTY_PRINT(order_id) FROM Orders二、SELECT DISTINCT如果指定 SELECT DISTINCT,则将从结果集中删除所有重复行(每组重复项中保留一行)。SELECT DISTINCT id FROM Orders对于流查询,计算查询结果所需的状态可能会无限增长。状态大小取决于不同行的数量。您可以为查询配置提供适当的状态生存时间 (TTL),以防止状态大小过大。请注意,这可能会影响查询结果的正确性。
0
0
0
浏览量1287
打酱油的后端

Flink系列Table API和SQL之:时间属性

一、时间属性和窗口基于时间的操作(比如时间窗口),需要定义相关的时间语义和时间数据来源的信息。在Table API和SQL中,会给表单独提供一个逻辑上的时间字段,专门用来在表处理程序中指示时间。所谓的时间属性(time attributes),就是每个表模式结构(schema)的一部分。可以在创建表的DDL里直接定义一个字段,也可以在DataStream转换成表时定义。一旦定义了时间属性,它就可以作为一个普通字段引用,并且可以在基于时间的操作中使用。时间属性的数据类型为TIMESTAMP,它的行为类似于常规时间戳,可以直接访问并且进行计算。按照时间语义的不同,可以把时间属性的定义分成事件时间(event time)和处理时间(processing time)两种情况。二、事件时间在实际应用中,最常用的就是事件时间。在事件时间语义下,允许表处理程序根据每个数据中包含的时间戳(也就是事件发生的时间)来生成结果。事件时间语义最大的用途就是处理乱序事件或者延迟事件的场景。通过设置水位线(watermark)来表示事件时间的进展,而水位线可以根据数据的最大事件戳设置一个延迟时间。这样即使在出现乱序的情况下,对数据的处理也可以获得正确的结果。为了处理无序时间,并区分流中的迟到事件。Flink需要从事件数据中提取时间戳,并生成水位线,用来推进事件时间的进展。事件时间属性可以在创建表DDL中定义,也可以在数据流和表的转换中定义。1.在创建表的DDL中定义在创建表的DDL(CREATE TABLE语句)中,可以增加一个字段,通过WATERMARK语句来定义事件时间属性。WATERMARK语句主要用来定义水位线(watermark)的生成表达式,这个表达式会将带有事件时间戳到字段标记为事件时间属性,并在它基础上给出水位线的延迟时间。具体定义方式如下:CREATE TABLE EventTable( user STRING, url STRING, ts TIMESTAMP(3), WATERMARK FOR ts AS ts - INTERVAL '5' SECOND ) WITH ( ... );把ts字段定义为事件时间属性,而且基于ts设置了5秒的水位线延迟。这里的5秒是以时间间隔的形式定义的,格式是INTERVAL<数值><时间单位>INTERVAL ‘5’ SECOND这里的数值必须用单引号引起来,而单位用SECOND和SECONDS是等效的。Flink中支持的事件时间属性数据类型必须为TIMESTAMP或者TIMESTAMP_LTZ这里的TIMESTAMP_LTZ是指带有本地时区信息的时间戳(TIMESTAMP WITH LOCAL TIMEZONE)一般情况下如果数据中的时间戳是"年-月-日-时-分-秒"的形式,那就是不带时区信息的,可以将事件时间属性定义为TIMESTAMP类型。而如果原始的时间戳就是一个长整型的毫秒数,这时就需要另外定义一个字段来表示事件时间属性。类型定义为TIMESTAMP_LTZ会更方便。CREATE TABLE events( user STRING, url STRING, ts BIGINT, ts_ltz AS TO_TIMESTAMP_LTZ(ts,3) WATERMARK FOR ts_ltz AS time_ltz - INTERVAL '5' SECOND ) WITH ( ... ); 另外定义了一个字段ts_ltz,是把长整型的ts转换为TIMESTAMP_LTZ得到的进而使用WATERMARK语句将它设为事件时间属性,并设置5秒的水位线延迟 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); 2.在数据流转换为表时定义事件时间属性也可以在将DataStream转换为表的时候来定义,调用fromDataStream()方法创建表时,可以追加参数来定义表中的字段结构。这时可以给某个字段加上.rowtime()后缀,就表示将当前字段指定为事件时间属性。这个字段可以是数据中本不存在、额外追加上去的逻辑字段,也可以是本身固有的字段,那么这个字段就会被事件时间属性所覆盖,类型也会被转换为TIMESTAMP。不论哪种方式,时间属性字段中保存的都是事件的时间戳(TIMESTAMP类型)。需要注意的是,这种方式只负责指定时间属性,而时间戳的提取和水位线的生成应该之前就在DataStream上定义好了,由于DataStream中没有时区概念,因此Flink会将事件时间属性解析成不带时区的TIMESTAMP类型,所有的时间值都被当作UTC标准时间。在代码中定义方式如下:方法一:流中数据类型为二元组Tuple2,包含两个字断,需要自定义提取时间戳并生成水位线DataStream<Tuple2<String,String>> stream = inputStream.assignTimestampsAndWatermarks(...);声明一个额外的逻辑字段作为事件时间属性Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").rowtime());方法二:流中数据类型为三元组Tuple3,最后一个字段就是事件时间戳DataStream<Tuple3<String,String,Long>> stream = inputStream.assignTimestampsAndWatermarks(...);不再声明额外字段,直接用最后一个字段作为事件时间属性Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").rowtime()); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema();( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* )三、处理时间相比之下,处理时间就比较简单了,就是我们的系统时间,使用时不需要提取时间戳(timestamp)和生成水位线(watermark)。因此在定义处理时间属性时,必须要额外声明一个字段,专门用来保存当前的处理时间。类似地,处理时间属性的定义也有两种方式:创建表DDL中定义,或者在数据流转换成表时定义。1.在创建表的DDL中定义在创建表的DDL(CREATE TABLE语句中),可以增加一个额外的字段,通过调用系统内置的PROCTIME()函数来指定当前的处理时间属性,返回的类型是TIMESTAMP_LTZ。CREATE TABLE EventTable( user STRING, url STRING, ts AS PROCTIME() ) WITH ( ... );这里的时间属性,其实是以计算列(computed column)的形式定义出来的。所谓的计算列是Flink SQL中引入的特殊概念,可以用一个AS语句来在表中产生数据中不存在的列,并且可以利用原有的列、各种运算符及内置函数。在前面事件时间属性的定义中,将ts字段转换成TIMESTAMP_LTZ类型的ts_ltz,也是计算列的定义方式。2.在数据流转换为表时定义处理时间属性同样可以在将DataStream转换为表的时候来定义。调用fromDataStream()方法创建表时,可以用.proctime()后缀来指定处理时间属性字段。由于处理时间是系统时间,原始数据中并没有这个字段,所以处理时间属性一定不能定义在一个已有字段上,只能定义在表结构所有字段的最后,作为额外的逻辑字段出现。代码中定义处理时间属性的方法如下:DataStream<Tuple2<String,String>> stream = ...;声明一个额外的字段作为处理时间属性字段Table table = tEnv.fromDataStream(stream,$("user"),$("url"),$("ts").proctime());
0
0
0
浏览量73
打酱油的后端

Flink系列Table API和SQL之:表的查询

一、表的查询创建好了表,接下来就是对表进行查询转换。对一个表的查询(Query)操作,就对应着流数据的转换(Transform)处理。Flink为我们提供了两种查询方式:SQL和Table API。二、执行SQL进行查询基于表执行SQL语句,是我们最为熟悉的查询方式。Flink基于Apache Calcite来提供对SQL的支持,Calcite是一个为不同的计算平台提供标准SQL查询的底层工具,很多大数据框架如Apache Hive、Apache Kylin中的SQL支持都是通过集成Calcite来实现的。在代码中,我们只要调用表环境sqlQuery()方法,传入一个字符串形式的SQL查询语句就可以了。执行得到的结果,是一个Table对象。创建表环境TableEnvironment tableEnv = ...;创建表tableEnv.executeSql("CREATE TABLE EventTable ... WITH ('connector' = ...)");查询用户Alice的点击事件,并提取表中前两个字段Table aliceVisitTable = tableEnv.sqlQuery( "SELECT user,url " + "FROM EventTable " + "WHERE user = 'Alice' " );目前Flink支持标准SQL中的绝大部分用法,并提供了丰富的计算函数。这样我们就可以把已有的技术迁移过来,像在MySQL、Hive中那样直接通过编写SQL实现自己的处理需求,从而大大降低了Flink上手的难度。例如,我们可以通过GROUP BY关键字定义分组聚合,调用COUNT()、SUM()这样的函数来进行统计计算:Table urlCountTable = tableEnv.sqlQuery( "SELECT user,COUNT(url) " + "FROM EventTable " + "GROUP BY user " );上面的例子得到的是一个新的Table对象,我们可以再次将它注册为虚拟表继续在SQL中调用。另外,我们也可以直接将查询的结果写入到已经注册的表中,这需要调用表环境的executeSql()方法来执行DDL,传入的是一个INSERT语句:注册表:tableEnv.executeSql("CREATE TABLE EventTable ... WITH ('connector' = ...)"); tableEnv.executeSql("CREATE TABLE OutputTable ... WITH ('connector' = ...)");将查询结果输出到OutputTable中tableEnv.executeSql( "INSERT INTO OutputTable "+ "SELECT user,url " + "FROM EventTable " + "WHERE user = 'Alice' " );将得到的结果写入输出表TableResult tableResult = table1.executeInsert("outputTable");三、调用Table API进行查询另外一种查询方式就是调用Table API。这是嵌入在Java和Scala语言内的查询API,核心就是Table接口类,通过一步步链式调用Table的方法,就可以定义出所有的查询转换操作。每一步方法调用的返回结果,都是一个Table。由于Table API是基于Table的Java实例进行调用的,因此我们首先要得到表的Java对象。基于环境中已经注册的表,可以通过表环境的from()方法非常容易地得到一个Table对象:Table eventTable = tableEnv.from("EventTable");传入的参数就是注册好的表名。注意这里eventTable是一个Table对象,而EventTable是在环境中注册的表名。得到Table对象之后,就可以调用API进行各种转换操作了,得到的是一个新的Table对象:Table maryClickTable = eventTable.where($("user").isEqual("Alice")) .select($("url"),$("user"));这里每个方法的参数都是一个表达式,用方法调用的形式直观地说明了想要表达的内容。“$”符号用来指定表中的一个字段。上面的代码和直接执行SQL是等效的。Table API是嵌入编程语言中的DSL,SQL中的很多特性和功能必须要有对应的实现才可以使用,因此跟直接写SQL比起来肯定就要麻烦些。目前Table API支持的功能相对更少,可以预见未来Flink社区也会以扩展SQL为主,为大家提供更加通用的接口方式。所以接下来也会以介绍SQL为主。调用Table API进行表的查询转换 Table clickTable = tableEnv.from("clickTable"); Table resultTable = clickTable.where($("user_name").isEqual("bom")).select($("username"), $( "url")); tableEnv.createTemporaryView("result",resultTable);
0
0
0
浏览量1446
打酱油的后端

基于 Flink CDC 构建 MySQL 和 Postgres 的 Streaming ETL

一、技术路线二、MySQL数据库建表mysql数据库创建数据库和表 products,orders创建products表-- MySQL CREATE DATABASE mydb;USE mydb; CREATE TABLE products (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,name VARCHAR(255) NOT NULL,description VARCHAR(512)); ALTER TABLE products AUTO_INCREMENT = 101;创建orders表CREATE TABLE orders ( order_id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, order_date DATETIME NOT NULL, customer_name VARCHAR(255) NOT NULL, price DECIMAL(10, 5) NOT NULL, product_id INTEGER NOT NULL, order_status BOOLEAN NOT NULL -- Whether order has been placed ) AUTO_INCREMENT = 10001;products表插入数据INSERT INTO products VALUES (default,"scooter","Small 2-wheel scooter"), (default,"car battery","12V car battery"), (default,"12-pack drill bits","12-pack of drill bits with sizes ranging from #40 to #3"), (default,"hammer","12oz carpenter's hammer"), (default,"hammer","14oz carpenter's hammer"), (default,"hammer","16oz carpenter's hammer"), (default,"rocks","box of assorted rocks"), (default,"jacket","water resistent black wind breaker"), (default,"spare tire","24 inch spare tire");orders表插入数据INSERT INTO orders VALUES (default, '2020-07-30 10:08:22', 'Jark', 50.50, 102, false), (default, '2020-07-30 10:11:09', 'Sally', 15.00, 105, false), (default, '2020-07-30 12:00:30', 'Edward', 25.25, 106, false);三、PostgreSQL数据库建表创建表 shipments-- PG CREATE TABLE shipments ( shipment_id SERIAL NOT NULL PRIMARY KEY, order_id SERIAL NOT NULL, origin VARCHAR(255) NOT NULL, destination VARCHAR(255) NOT NULL, is_arrived BOOLEAN NOT NULL);插入数据ALTER SEQUENCE public.shipments_shipment_id_seq RESTART WITH 1001; ALTER TABLE public.shipments REPLICA IDENTITY FULL; INSERT INTO shipments VALUES (default,10001,'Beijing','Shanghai',false), (default,10002,'Hangzhou','Shanghai',false), (default,10003,'Shanghai','Hangzhou',false);四、在 Flink SQL CLI 中使用 Flink DDL 创建表首先,开启 checkpoint,每隔3秒做一次 checkpoint-- Flink SQL Flink SQL> SET execution.checkpointing.interval = 3s;然后, 对于数据库中的表 products, orders, shipments, 使用 Flink SQL CLI 创建对应的表,用于同步这些底层数据库表的数据-- Flink SQL Flink SQL> CREATE TABLE products ( id INT, name STRING, description STRING, PRIMARY KEY (id) NOT ENFORCED) WITH ( 'connector' = 'mysql-cdc', 'hostname' = 'localhost', 'port' = '3306', 'username' = 'root', 'password' = '123456', 'database-name' = 'mydb', 'table-name' = 'products' );Flink SQL> CREATE TABLE orders ( order_id INT, order_date TIMESTAMP(0), customer_name STRING, price DECIMAL(10, 5), product_id INT, order_status BOOLEAN, PRIMARY KEY (order_id) NOT ENFORCED) WITH ( 'connector' = 'mysql-cdc', 'hostname' = 'localhost', 'port' = '3306', 'username' = 'root', 'password' = '123456', 'database-name' = 'mydb', 'table-name' = 'orders');最后,创建 enriched_orders 表, 用来将关联后的订单数据写入 Elasticsearch 中-- Flink SQL Flink SQL> CREATE TABLE enriched_orders ( order_id INT, order_date TIMESTAMP(0), customer_name STRING, price DECIMAL(10, 5), product_id INT, order_status BOOLEAN, product_name STRING, product_description STRING, shipment_id INT, origin STRING, destination STRING, is_arrived BOOLEAN, PRIMARY KEY (order_id) NOT ENFORCED) WITH ( 'connector' = 'elasticsearch-7', 'hosts' = 'http://localhost:9200', 'index' = 'enriched_orders' );五、关联订单数据并且将其写入 Elasticsearch 中使用 Flink SQL 将订单表 order 与 商品表 products,物流信息表 shipments 关联,并将关联后的订单信息写入 Elasticsearch 中-- Flink SQL Flink SQL> INSERT INTO enriched_orders SELECT o.*, p.name, p.description, s.shipment_id, s.origin, s.destination, s.is_arrived FROM orders AS o LEFT JOIN products AS p ON o.product_id = p.id LEFT JOIN shipments AS s ON o.order_id = s.order_id; 六、Kibana查看商品和物流信息的订单数据创建 index pattern enriched_orders查看写入的数据七、修改数据库中表的数据,Kibana查看更新修改 MySQL 和 Postgres 数据库中表的数据,Kibana中显示的订单数据也将实时更新:在 MySQL 的 orders 表中插入一条数据--MySQL INSERT INTO orders VALUES (default, '2020-07-30 15:22:00', 'Jark', 29.71, 104, false);在 Postgres 的 shipment 表中插入一条数据--PG INSERT INTO shipmentsVALUES (default,10004,'Shanghai','Beijing',false);在 MySQL 的 orders 表中更新订单的状态--MySQL UPDATE orders SET order_status = true WHERE order_id = 10004;在 Postgres 的 shipment 表中更新物流的状态--PG UPDATE shipments SET is_arrived = true WHERE shipment_id = 1004;在 MYSQL 的 orders 表中删除一条数据--MySQL DELETE FROM orders WHERE order_id = 10004;每执行一步就刷新一次 Kibana,可以看到 Kibana 中显示的订单数据将实时更新,如下所示:
0
0
0
浏览量1033
打酱油的后端

Flink系列之:Elasticsearch SQL 连接器

一、Elasticsearch SQL 连接器Sink: BatchSink: Streaming Append & Upsert ModeElasticsearch 连接器允许将数据写入到 Elasticsearch 引擎的索引中。本文档描述运行 SQL 查询时如何设置 Elasticsearch 连接器。连接器可以工作在 upsert 模式,使用 DDL 中定义的主键与外部系统交换 UPDATE/DELETE 消息。如果 DDL 中没有定义主键,那么连接器只能工作在 append 模式,只能与外部系统交换 INSERT 消息。二、创建 Elasticsearch表以下示例展示了如何创建 Elasticsearch sink 表:CREATE TABLE myUserTable ( user_id STRING, user_name STRING, uv BIGINT, pv BIGINT, PRIMARY KEY (user_id) NOT ENFORCED ) WITH ( 'connector' = 'elasticsearch-7', 'hosts' = 'http://localhost:9200', 'index' = 'users' );三、连接器参数四、Key 处理Elasticsearch sink 可以根据是否定义了一个主键来确定是在 upsert 模式还是 append 模式下工作。 如果定义了主键,Elasticsearch sink 将以 upsert 模式工作,该模式可以消费包含 UPDATE/DELETE 消息的查询。 如果未定义主键,Elasticsearch sink 将以 append 模式工作,该模式只能消费包含 INSERT 消息的查询。在 Elasticsearch 连接器中,主键用于计算 Elasticsearch 的文档 id,文档 id 为最多 512 字节且不包含空格的字符串。 Elasticsearch 连接器通过使用 document-id.key-delimiter 指定的键分隔符按照 DDL 中定义的顺序连接所有主键字段,为每一行记录生成一个文档 ID 字符串。 某些类型不允许作为主键字段,因为它们没有对应的字符串表示形式,例如,BYTES,ROW,ARRAY,MAP 等。 如果未指定主键,Elasticsearch 将自动生成文档 id。五、动态索引Elasticsearch sink 同时支持静态索引和动态索引。如果你想使用静态索引,则 index 选项值应为纯字符串,例如 ‘myusers’,所有记录都将被写入到 “myusers” 索引中。如果你想使用动态索引,你可以使用 {field_name} 来引用记录中的字段值来动态生成目标索引。 你也可以使用 ‘{field_name|date_format_string}’ 将 TIMESTAMP/DATE/TIME 类型的字段值转换为 date_format_string 指定的格式。 date_format_string 与 Java 的 DateTimeFormatter 兼容。 例如,如果选项值设置为 ‘myusers-{log_ts|yyyy-MM-dd}’,则 log_ts 字段值为 2020-03-27 12:25:55 的记录将被写入到 “myusers-2020-03-27” 索引中。你也可以使用 ‘{now()|date_format_string}’ 将当前的系统时间转换为 date_format_string 指定的格式。now() 对应的时间类型是 TIMESTAMP_WITH_LTZ 。 在将系统时间格式化为字符串时会使用 session 中通过 table.local-time-zone 中配置的时区。 使用 NOW(), now(), CURRENT_TIMESTAMP, current_timestamp 均可以。注意: 使用当前系统时间生成的动态索引时, 对于 changelog 的流,无法保证同一主键对应的记录能产生相同的索引名, 因此使用基于系统时间的动态索引,只能支持 append only 的流。六、数据类型映射Elasticsearch 将文档存储在 JSON 字符串中。因此数据类型映射介于 Flink 数据类型和 JSON 数据类型之间。 Flink 为 Elasticsearch 连接器使用内置的 ‘json’ 格式。下表列出了 Flink 中的数据类型与 JSON 中的数据类型的映射关系。
0
0
0
浏览量1587
打酱油的后端

PyFlink系列之一:PyFlink安装和PyFlink使用的详细技术

一、下载PyFlink命令行下载PyFlink:pip install apache-flinkPycharm下载PyFlink:二、创建TableEnvironment创建 TableEnvironment 的推荐方式是通过 EnvironmentSettings 对象创建:from pyflink.table import EnvironmentSettings, TableEnvironment # create a streaming TableEnvironment env_settings = EnvironmentSettings.in_streaming_mode() # or a batch TableEnvironment # env_settings = EnvironmentSettings.in_batch_mode() table_env = TableEnvironment.create(env_settings) 或者,用户可以从现有的 StreamExecutionEnvironment 创建 StreamTableEnvironment,以与 DataStream API 进行互操作。from pyflink.datastream import StreamExecutionEnvironment from pyflink.table import StreamTableEnvironment # create a streaming TableEnvironment from a StreamExecutionEnvironment env = StreamExecutionEnvironment.get_execution_environment() table_env = StreamTableEnvironment.create(env) 三、TableEnvironment API1.Table/SQL 操作这些 APIs 用来创建或者删除 Table API/SQL 表和写查询:APIs描述from_elements(elements, schema=None, verify_schema=True)通过元素集合来创建表。from_pandas(pdf, schema=None, split_num=1)通过 pandas DataFrame 来创建表。from_path(path)通过指定路径下已注册的表来创建一个表,例如通过 create_temporary_view 注册表。sql_query(query)执行一条 SQL 查询,并将查询的结果作为一个 Table 对象。create_temporary_view(view_path, table) 将一个 Table 对象注册为一张临时表,类似于 SQL 的临时表。drop_temporary_view(view_path) 删除指定路径下已注册的临时表drop_temporary_table(table_path)删除指定路径下已注册的临时表。 你可以使用这个接口来删除临时 source 表和临时 sink 表。execute_sql(stmt)执行指定的语句并返回执行结果。 执行语句可以是 DDL/DML/DQL/SHOW/DESCRIBE/EXPLAIN/USE。注意,对于 “INSERT INTO” 语句,这是一个异步操作,通常在向远程集群提交作业时才需要使用。 但是,如果在本地集群或者 IDE 中执行作业时,你需要等待作业执行完成2.执行/解释作业这些 APIs 是用来执行/解释作业。注意,execute_sql API 也可以用于执行作业。APIs 描述explain_sql(stmt, *extra_details) 返回指定语句的抽象语法树和执行计划。create_statement_set() 创建一个可接受 DML 语句或表的 StatementSet 实例。 它可用于执行包含多个 sink 的作业。3.创建/删除用户自定义函数这些 APIs 用来注册 UDFs 或者 删除已注册的 UDFs。 注意,execute_sql API 也可以用于注册/删除 UDFs。APIs描述create_temporary_function(path, function)将一个 Python 用户自定义函数注册为临时 catalog 函数。create_temporary_system_function(name, function) 将一个 Python 用户自定义函数注册为临时系统函数。 如果临时系统函数的名称与临时 catalog 函数名称相同,优先使用临时系统函数。create_java_function(path, function_class_name, ignore_if_exists=None)将 Java 用户自定义函数注册为指定路径下的 catalog 函数。 如果 catalog 是持久化的,则可以跨多个 Flink 会话和集群使用已注册的 catalog 函数。create_java_temporary_function(path, function_class_name)将 Java 用户自定义函数注册为临时 catalog 函数。create_java_temporary_system_function(name, function_class_name)将 Java 用户定义的函数注册为临时系统函数。drop_function(path)删除指定路径下已注册的 catalog 函数。drop_temporary_function(path)删除指定名称下已注册的临时系统函数。drop_temporary_system_function(name)删除指定名称下已注册的临时系统函数。4.依赖管理这些 APIs 用来管理 Python UDFs 所需要的 Python 依赖。APIs 描述add_python_file(file_path)添加 Python 依赖,可以是 Python 文件,Python 包或者本地目录。 它们将会被添加到 Python UDF 工作程序的 PYTHONPATH 中。set_python_requirements(requirements_file_path, requirements_cache_dir=None) 指定一个 requirements.txt 文件,该文件定义了第三方依赖关系。 这些依赖项将安装到一个临时 catalog 中,并添加到 Python UDF 工作程序的 PYTHONPATH 中。add_python_archive(archive_path, target_dir=None) 添加 Python 归档文件。该文件将被解压到 Python UDF 程序的工作目录中。5.配置APIs描述get_config()返回 table config,可以通过 table config 来定义 Table API 的运行时行为。下面的代码示例展示了如何通过这个 API 来设置配置选项:# set the parallelism to 8 table_env.get_config().get_configuration().set_string("parallelism.default", "8") 四、Catalog APIs这些 APIs 用于访问 catalog 和模块五、Statebackend,Checkpoint 以及重启策略在 Flink 1.10 之前,你可以通过 StreamExecutionEnvironment 来配置 statebackend,checkpointing 以及重启策略。 现在你可以通过在 TableConfig 中,通过设置键值选项来配置它们。下面代码示例展示了如何通过 Table API 来配置 statebackend,checkpoint 以及重启策略:# 设置重启策略为 "fixed-delay" table_env.get_config().get_configuration().set_string("restart-strategy", "fixed-delay") table_env.get_config().get_configuration().set_string("restart-strategy.fixed-delay.attempts", "3") table_env.get_config().get_configuration().set_string("restart-strategy.fixed-delay.delay", "30s") # 设置 checkpoint 模式为 EXACTLY_ONCE table_env.get_config().get_configuration().set_string("execution.checkpointing.mode", "EXACTLY_ONCE") table_env.get_config().get_configuration().set_string("execution.checkpointing.interval", "3min") # 设置 statebackend 类型为 "rocksdb",其他可选项有 "filesystem" 和 "jobmanager" # 你也可以将这个属性设置为 StateBackendFactory 的完整类名 # e.g. org.apache.flink.contrib.streaming.state.RocksDBStateBackendFactory table_env.get_config().get_configuration().set_string("state.backend", "rocksdb") # 设置 RocksDB statebackend 所需要的 checkpoint 目录 table_env.get_config().get_configuration().set_string("state.checkpoints.dir", "file:///tmp/checkpoints/") 六、Table APITable API 是批处理和流处理的统一的关系型 API。Table API 的查询不需要修改代码就可以采用批输入或流输入来运行。Table API 是 SQL 语言的超集,并且是针对 Apache Flink 专门设计的。Table API 集成了 Scala,Java 和 Python 语言的 API。Table API 的查询是使用 Java,Scala 或 Python 语言嵌入的风格定义的,有诸如自动补全和语法校验的 IDE 支持,而不是像普通 SQL 一样使用字符串类型的值来指定查询。Table API 和 Flink SQL 共享许多概念以及部分集成的 API。通过查看公共概念 & API来学习如何注册表或如何创建一个表对象。流概念页面讨论了诸如动态表和时间属性等流特有的概念。下面的例子中假定有一张叫 Orders 的表,表中有属性 (a, b, c, rowtime) 。rowtime 字段是流任务中的逻辑时间属性或是批任务中的普通时间戳字段。概述 & 示例Table API 支持 Scala, Java 和 Python 语言。Scala 语言的 Table API 利用了 Scala 表达式,Java 语言的 Table API 支持 DSL 表达式和解析并转换为等价表达式的字符串,Python 语言的 Table API 仅支持解析并转换为等价表达式的字符串。下面的例子展示了 Scala、Java 和 Python 语言的 Table API 的不同之处。表程序是在批环境下执行的。程序扫描了 Orders 表,通过字段 a 进行分组,并计算了每组结果的行数。JavaJava 的 Table API 通过引入 org.apache.flink.table.api.java.* 来使用。下面的例子展示了如何创建一个 Java 的 Table API 程序,以及表达式是如何指定为字符串的。 使用DSL表达式时也需要引入静态的 org.apache.flink.table.api.Expressions.*。import org.apache.flink.table.api.*; import static org.apache.flink.table.api.Expressions.*; EnvironmentSettings settings = EnvironmentSettings .newInstance() .inStreamingMode() .build(); TableEnvironment tEnv = TableEnvironment.create(settings); // 在表环境中注册 Orders 表 // ... // 指定表程序 Table orders = tEnv.from("Orders"); // schema (a, b, c, rowtime) Table counts = orders .groupBy($("a")) .select($("a"), $("b").count().as("cnt")); // 打印 counts.execute().print(); ScalaScala 的 Table API 通过引入 org.apache.flink.table.api.、org.apache.flink.api.scala. 和 org.apache.flink.table.api.bridge.scala._(开启数据流的桥接支持)来使用。下面的例子展示了如何创建一个 Scala 的 Table API 程序。通过 Scala 的带美元符号($)的字符串插值来实现表字段引用。import org.apache.flink.api.scala._ import org.apache.flink.table.api._ import org.apache.flink.table.api.bridge.scala._ // 环境配置 val settings = EnvironmentSettings .newInstance() .inStreamingMode() .build(); val tEnv = TableEnvironment.create(settings); // 在表环境中注册 Orders 表 // ... // 指定表程序 val orders = tEnv.from("Orders") // schema (a, b, c, rowtime) val result = orders .groupBy($"a") .select($"a", $"b".count as "cnt") .execute() .print() Python下面的例子展示了如何创建一个 Python 的 Table API 程序,以及表达式是如何指定为字符串的。from pyflink.table import * # 环境配置 t_env = TableEnvironment.create( environment_settings=EnvironmentSettings.in_batch_mode()) # 在表环境中注册 Orders 表和结果 sink 表 source_data_path = "/path/to/source/directory/" result_data_path = "/path/to/result/directory/" source_ddl = f""" create table Orders( a VARCHAR, b BIGINT, c BIGINT, rowtime TIMESTAMP(3), WATERMARK FOR rowtime AS rowtime - INTERVAL '1' SECOND ) with ( 'connector' = 'filesystem', 'format' = 'csv', 'path' = '{source_data_path}' ) """ t_env.execute_sql(source_ddl) sink_ddl = f""" create table `Result`( a VARCHAR, cnt BIGINT ) with ( 'connector' = 'filesystem', 'format' = 'csv', 'path' = '{result_data_path}' ) """ t_env.execute_sql(sink_ddl) # 指定表程序 orders = t_env.from_path("Orders") # schema (a, b, c, rowtime) orders.group_by("a").select(orders.a, orders.b.count.alias('cnt')).execute_insert("result").wait() 下一个例子展示了一个更加复杂的 Table API 程序。这个程序也扫描 Orders 表。程序过滤了空值,使字符串类型的字段 a 标准化,并且每个小时进行一次计算并返回 a 的平均账单金额 b。Java// 环境配置 // ... // 指定表程序 Table orders = tEnv.from("Orders"); // schema (a, b, c, rowtime) Table result = orders .filter( and( $("a").isNotNull(), $("b").isNotNull(), $("c").isNotNull() )) .select($("a").lowerCase().as("a"), $("b"), $("rowtime")) .window(Tumble.over(lit(1).hours()).on($("rowtime")).as("hourlyWindow")) .groupBy($("hourlyWindow"), $("a")) .select($("a"), $("hourlyWindow").end().as("hour"), $("b").avg().as("avgBillingAmount")); Scala// 环境配置 // ... // 指定表程序 val orders: Table = tEnv.from("Orders") // schema (a, b, c, rowtime) val result: Table = orders .filter($"a".isNotNull && $"b".isNotNull && $"c".isNotNull) .select($"a".lowerCase() as "a", $"b", $"rowtime") .window(Tumble over 1.hour on $"rowtime" as "hourlyWindow") .groupBy($"hourlyWindow", $"a") .select($"a", $"hourlyWindow".end as "hour", $"b".avg as "avgBillingAmount") Python# 指定表程序 from pyflink.table.expressions import col, lit orders = t_env.from_path("Orders") # schema (a, b, c, rowtime) result = orders.filter(orders.a.is_not_null & orders.b.is_not_null & orders.c.is_not_null) \ .select(orders.a.lower_case.alias('a'), orders.b, orders.rowtime) \ .window(Tumble.over(lit(1).hour).on(orders.rowtime).alias("hourly_window")) \ .group_by(col('hourly_window'), col('a')) \ .select(col('a'), col('hourly_window').end.alias('hour'), b.avg.alias('avg_billing_amount')) 因为 Table API 的批数据 API 和流数据 API 是统一的,所以这两个例子程序不需要修改代码就可以运行在流输入或批输入上。在这两种情况下,只要流任务没有数据延时,程序将会输出相同的结果。七、OperationsTable API支持如下操作。请注意不是所有的操作都可以既支持流也支持批;这些操作都具有相应的标记。1.From:Batch Streaming和 SQL 查询的 FROM 子句类似。 执行一个注册过的表的扫描。Java:Table orders = tableEnv.from("Orders");Scala:val orders = tableEnv.from("Orders")Python:orders = t_env.from_path("Orders")2.FromValues:Batch Streaming和 SQL 查询中的 VALUES 子句类似。 基于提供的行生成一张内联表。你可以使用 row(…) 表达式创建复合行:Java:Table table = tEnv.fromValues( row(1, "ABC"), row(2L, "ABCDE") ); Scala:table = tEnv.fromValues( row(1, "ABC"), row(2L, "ABCDE") ) Python:table = t_env.from_elements([(1, 'ABC'), (2, 'ABCDE')]) 这将生成一张结构如下的表:root |-- f0: BIGINT NOT NULL // original types INT and BIGINT are generalized to BIGINT |-- f1: VARCHAR(5) NOT NULL // original types CHAR(3) and CHAR(5) are generalized // to VARCHAR(5). VARCHAR is used instead of CHAR so that // no padding is applied 这个方法会根据输入的表达式自动获取类型。如果在某一个特定位置的类型不一致,该方法会尝试寻找一个所有类型的公共超类型。如果公共超类型不存在,则会抛出异常。你也可以明确指定所需的类型。指定如 DECIMAL 这样的一般类型或者给列命名可能是有帮助的。Java:Table table = tEnv.fromValues( DataTypes.ROW( DataTypes.FIELD("id", DataTypes.DECIMAL(10, 2)), DataTypes.FIELD("name", DataTypes.STRING()) ), row(1, "ABC"), row(2L, "ABCDE") ); Scala:val table = tEnv.fromValues( DataTypes.ROW( DataTypes.FIELD("id", DataTypes.DECIMAL(10, 2)), DataTypes.FIELD("name", DataTypes.STRING()) ), row(1, "ABC"), row(2L, "ABCDE") ) Python:table = t_env.from_elements( [(1, 'ABC'), (2, 'ABCDE')], schema=DataTypes.Row([DataTypes.FIELD('id', DataTypes.DECIMAL(10, 2)), DataTypes.FIELD('name', DataTypes.STRING())]))这将生成一张结构如下的表:root |-- id: DECIMAL(10, 2) |-- name: STRING 3.Select:Batch Streaming和 SQL 的 SELECT 子句类似。 执行一个 select 操作。JavaTable orders = tableEnv.from("Orders"); Table result = orders.select($("a"), $("c").as("d"));Scalaval orders = tableEnv.from("Orders") Table result = orders.select($"a", $"c" as "d");Pythonorders = t_env.from_path("Orders") result = orders.select(orders.a, orders.c.alias('d'))可以选择星号(*)作为通配符,select 表中的所有列。JavaTable result = orders.select($("*")); ScalaTable result = orders.select($"*")Pythonfrom pyflink.table.expressions import col result = orders.select(col("*"))4.As:Batch Streaming重命名字段。JavaTable orders = tableEnv.from("Orders"); Table result = orders.as("x, y, z, t");scalaval orders: Table = tableEnv.from("Orders").as("x", "y", "z", "t")Pythonorders = t_env.from_path("Orders") result = orders.alias("x, y, z, t")5.Where / Filter:Batch Streaming和 SQL 的 WHERE 子句类似。 过滤掉未验证通过过滤谓词的行。Java:Table orders = tableEnv.from("Orders"); Table result = orders.where($("b").isEqual("red")); Table orders = tableEnv.from("Orders"); Table result = orders.filter($("b").isEqual("red")); Scala:val orders: Table = tableEnv.from("Orders") val result = orders.filter($"a" % 2 === 0) val orders: Table = tableEnv.from("Orders") val result = orders.filter($"a" % 2 === 0) Python:orders = t_env.from_path("Orders") result = orders.where(orders.a == 'red') orders = t_env.from_path("Orders") result = orders.filter(orders.a == 'red') 八、列操作1.AddColumns:Batch Streaming执行字段添加操作。 如果所添加的字段已经存在,将抛出异常。JavaTable orders = tableEnv.from("Orders"); Table result = orders.addColumns(concat($("c"), "sunny")); Scalaval orders = tableEnv.from("Orders"); val result = orders.addColumns(concat($"c", "Sunny")) Pythonfrom pyflink.table.expressions import concat orders = t_env.from_path("Orders") result = orders.add_columns(concat(orders.c, 'sunny')) 2.AddOrReplaceColumns:Batch Streaming执行字段添加操作。 如果添加的列名称和已存在的列名称相同,则已存在的字段将被替换。 此外,如果添加的字段里面有重复的字段名,则会使用最后一个字段。JavaTable orders = tableEnv.from("Orders"); Table result = orders.addOrReplaceColumns(concat($("c"), "sunny").as("desc")); Scalaval orders = tableEnv.from("Orders"); val result = orders.addOrReplaceColumns(concat($"c", "Sunny") as "desc") Pythonfrom pyflink.table.expressions import concat orders = t_env.from_path("Orders") result = orders.add_or_replace_columns(concat(orders.c, 'sunny').alias('desc')) 3.DropColumns:Batch StreamingJavaable orders = tableEnv.from("Orders"); Table result = orders.dropColumns($("b"), $("c")); Scalaval orders = tableEnv.from("Orders"); val result = orders.dropColumns($"b", $"c") Pythonorders = t_env.from_path("Orders") result = orders.drop_columns(orders.b, orders.c) 4.RenameColumns:Batch Streaming执行字段重命名操作。 字段表达式应该是别名表达式,并且仅当字段已存在时才能被重命名。JavaTable orders = tableEnv.from("Orders"); Table result = orders.renameColumns($("b").as("b2"), $("c").as("c2")); Scalaval orders = tableEnv.from("Orders"); val result = orders.renameColumns($"b" as "b2", $"c" as "c2") Pythonorders = t_env.from_path("Orders") result = orders.rename_columns(orders.b.alias('b2'), orders.c.alias('c2')) 九、Aggregations1.GroupBy Aggregation:Batch Streaming和 SQL 的 GROUP BY 子句类似。 使用分组键对行进行分组,使用伴随的聚合算子来按照组进行聚合行。JavaTable orders = tableEnv.from("Orders"); Table result = orders.groupBy($("a")).select($("a"), $("b").sum().as("d")); Scalaval orders: Table = tableEnv.from("Orders") val result = orders.groupBy($"a").select($"a", $"b".sum().as("d")) Pythonorders = t_env.from_path("Orders") result = orders.group_by(orders.a).select(orders.a, orders.b.sum.alias('d')) 2.GroupBy Window Aggregation:Batch Streaming使用分组窗口结合单个或者多个分组键对表进行分组和聚合。JavaTable orders = tableEnv.from("Orders"); Table result = orders .window(Tumble.over(lit(5).minutes()).on($("rowtime")).as("w")) // 定义窗口 .groupBy($("a"), $("w")) // 按窗口和键分组 // 访问窗口属性并聚合 .select( $("a"), $("w").start(), $("w").end(), $("w").rowtime(), $("b").sum().as("d") ); Scalaval orders: Table = tableEnv.from("Orders") val result: Table = orders .window(Tumble over 5.minutes on $"rowtime" as "w") // 定义窗口 .groupBy($"a", $"w") // 按窗口和键分组 .select($"a", $"w".start, $"w".end, $"w".rowtime, $"b".sum as "d") // 访问窗口属性并聚合 Pythonfrom pyflink.table.window import Tumble from pyflink.table.expressions import lit, col orders = t_env.from_path("Orders") result = orders.window(Tumble.over(lit(5).minutes).on(orders.rowtime).alias("w")) \ .group_by(orders.a, col('w')) \ .select(orders.a, col('w').start, col('w').end, orders.b.sum.alias('d')) 3.Over Window Aggregation和 SQL 的 OVER 子句类似。JavaTable orders = tableEnv.from("Orders"); Table result = orders // 定义窗口 .window( Over .partitionBy($("a")) .orderBy($("rowtime")) .preceding(UNBOUNDED_RANGE) .following(CURRENT_RANGE) .as("w")) // 滑动聚合 .select( $("a"), $("b").avg().over($("w")), $("b").max().over($("w")), $("b").min().over($("w")) ); Scalaval orders: Table = tableEnv.from("Orders") val result: Table = orders // 定义窗口 .window( Over partitionBy $"a" orderBy $"rowtime" preceding UNBOUNDED_RANGE following CURRENT_RANGE as "w") .select($"a", $"b".avg over $"w", $"b".max().over($"w"), $"b".min().over($"w")) // 滑动聚合 Pythonfrom pyflink.table.window import Over from pyflink.table.expressions import col, UNBOUNDED_RANGE, CURRENT_RANGE orders = t_env.from_path("Orders") result = orders.over_window(Over.partition_by(orders.a).order_by(orders.rowtime) .preceding(UNBOUNDED_RANGE).following(CURRENT_RANGE) .alias("w")) \ .select(orders.a, orders.b.avg.over(col('w')), orders.b.max.over(col('w')), orders.b.min.over(col('w'))) 所有的聚合必须定义在同一个窗口上,比如同一个分区、排序和范围内。目前只支持 PRECEDING 到当前行范围(无界或有界)的窗口。尚不支持 FOLLOWING 范围的窗口。ORDER BY 操作必须指定一个单一的时间属性。4.Distinct Aggregation:Batch Streaming和 SQL DISTINCT 聚合子句类似,例如 COUNT(DISTINCT a)。 Distinct 聚合声明的聚合函数(内置或用户定义的)仅应用于互不相同的输入值。 Distinct 可以应用于 GroupBy Aggregation、GroupBy Window Aggregation 和 Over Window Aggregation。Java:Table orders = tableEnv.from("Orders"); // 按属性分组后的的互异(互不相同、去重)聚合 Table groupByDistinctResult = orders .groupBy($("a")) .select($("a"), $("b").sum().distinct().as("d")); // 按属性、时间窗口分组后的互异(互不相同、去重)聚合 Table groupByWindowDistinctResult = orders .window(Tumble .over(lit(5).minutes()) .on($("rowtime")) .as("w") ) .groupBy($("a"), $("w")) .select($("a"), $("b").sum().distinct().as("d")); // over window 上的互异(互不相同、去重)聚合 Table result = orders .window(Over .partitionBy($("a")) .orderBy($("rowtime")) .preceding(UNBOUNDED_RANGE) .as("w")) .select( $("a"), $("b").avg().distinct().over($("w")), $("b").max().over($("w")), $("b").min().over($("w")) ); Scala:val orders: Table = tableEnv.from("Orders"); // 按属性分组后的的互异(互不相同、去重)聚合 val groupByDistinctResult = orders .groupBy($"a") .select($"a", $"b".sum.distinct as "d") // 按属性、时间窗口分组后的互异(互不相同、去重)聚合 val groupByWindowDistinctResult = orders .window(Tumble over 5.minutes on $"rowtime" as "w").groupBy($"a", $"w") .select($"a", $"b".sum.distinct as "d") // over window 上的互异(互不相同、去重)聚合 val result = orders .window(Over partitionBy $"a" orderBy $"rowtime" preceding UNBOUNDED_RANGE as $"w") .select($"a", $"b".avg.distinct over $"w", $"b".max over $"w", $"b".min over $"w") Python:from pyflink.table.expressions import col, lit, UNBOUNDED_RANGE orders = t_env.from_path("Orders") # 按属性分组后的的互异(互不相同、去重)聚合 group_by_distinct_result = orders.group_by(orders.a) \ .select(orders.a, orders.b.sum.distinct.alias('d')) # 按属性、时间窗口分组后的互异(互不相同、去重)聚合 group_by_window_distinct_result = orders.window( Tumble.over(lit(5).minutes).on(orders.rowtime).alias("w")).group_by(orders.a, col('w')) \ .select(orders.a, orders.b.sum.distinct.alias('d')) # over window 上的互异(互不相同、去重)聚合 result = orders.over_window(Over .partition_by(orders.a) .order_by(orders.rowtime) .preceding(UNBOUNDED_RANGE) .alias("w")) \ .select(orders.a, orders.b.avg.distinct.over(col('w')), orders.b.max.over(col('w')), orders.b.min.over(col('w'))) 用户定义的聚合函数也可以与 DISTINCT 修饰符一起使用。如果计算不同(互异、去重的)值的聚合结果,则只需向聚合函数添加 distinct 修饰符即可。JavaTable orders = tEnv.from("Orders"); // 对 user-defined aggregate functions 使用互异(互不相同、去重)聚合 tEnv.registerFunction("myUdagg", new MyUdagg()); orders.groupBy("users") .select( $("users"), call("myUdagg", $("points")).distinct().as("myDistinctResult") ); Scalaval orders: Table = tableEnv.from("Orders") val result = orders.distinct() Pythonorders = t_env.from_path("Orders") result = orders.distinct() 十、Joins1.Inner Join:Batch Streaming和 SQL 的 JOIN 子句类似。关联两张表。两张表必须有不同的字段名,并且必须通过 join 算子或者使用 where 或 filter 算子定义至少一个 join 等式连接谓词。Java:Table orders = tableEnv.from("Orders"); Table result = orders.distinct(); Scala:val left = tableEnv.from("MyTable").select($"a", $"b", $"c") val right = tableEnv.from("MyTable").select($"d", $"e", $"f") val result = left.join(right).where($"a" === $"d").select($"a", $"b", $"e") Python:from pyflink.table.expressions import col left = t_env.from_path("Source1").select(col('a'), col('b'), col('c')) right = t_env.from_path("Source2").select(col('d'), col('e'), col('f')) result = left.join(right).where(left.a == right.d).select(left.a, left.b, right.e) 2.Outer Join:Batch Streaming和 SQL LEFT/RIGHT/FULL OUTER JOIN 子句类似。 关联两张表。 两张表必须有不同的字段名,并且必须定义至少一个等式连接谓词。Java:Table left = tableEnv.from("MyTable).select($("a"), $("b"), $("c")); Table right = tableEnv.from("MyTable).select($("d"), $("e"), $("f")); Table leftOuterResult = left.leftOuterJoin(right, $("a").isEqual($("d"))) .select($("a"), $("b"), $("e")); Table rightOuterResult = left.rightOuterJoin(right, $("a").isEqual($("d"))) .select($("a"), $("b"), $("e")); Table fullOuterResult = left.fullOuterJoin(right, $("a").isEqual($("d"))) .select($("a"), $("b"), $("e")); Scala:val left = tableEnv.from("MyTable").select($"a", $"b", $"c") val right = tableEnv.from("MyTable").select($"d", $"e", $"f") val leftOuterResult = left.leftOuterJoin(right, $"a" === $"d").select($"a", $"b", $"e") val rightOuterResult = left.rightOuterJoin(right, $"a" === $"d").select($"a", $"b", $"e") val fullOuterResult = left.fullOuterJoin(right, $"a" === $"d").select($"a", $"b", $"e") Python:from pyflink.table.expressions import col left = t_env.from_path("Source1").select(col('a'), col('b'), col('c')) right = t_env.from_path("Source2").select(col('d'), col('e'), col('f')) left_outer_result = left.left_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) right_outer_result = left.right_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) full_outer_result = left.full_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) 3.Interval Join:Batch StreamingInterval join 是可以通过流模式处理的常规 join 的子集。Interval join 至少需要一个 equi-join 谓词和一个限制双方时间界限的 join 条件。这种条件可以由两个合适的范围谓词(<、<=、>=、>)或一个比较两个输入表相同时间属性(即处理时间或事件时间)的等值谓词来定义。Java:Table left = tableEnv.from("MyTable).select($("a"), $("b"), $("c"), $("ltime")); Table right = tableEnv.from("MyTable).select($("d"), $("e"), $("f"), $("rtime")); Table result = left.join(right) .where( and( $("a").isEqual($("d")), $("ltime").isGreaterOrEqual($("rtime").minus(lit(5).minutes())), $("ltime").isLess($("rtime").plus(lit(10).minutes())) )) .select($("a"), $("b"), $("e"), $("ltime")); Scala:val left = tableEnv.from("MyTable").select($"a", $"b", $"c", $"ltime") val right = tableEnv.from("MyTable").select($"d", $"e", $"f", $"rtime") val result = left.join(right) .where($"a" === $"d" && $"ltime" >= $"rtime" - 5.minutes && $"ltime" < $"rtime" + 10.minutes) .select($"a", $"b", $"e", $"ltime") Python:from pyflink.table.expressions import col left = t_env.from_path("Source1").select(col('a'), col('b'), col('c'), col('rowtime1')) right = t_env.from_path("Source2").select(col('d'), col('e'), col('f'), col('rowtime2')) joined_table = left.join(right).where((left.a == right.d) & (left.rowtime1 >= right.rowtime2 - lit(1).second) & (left.rowtime1 <= right.rowtime2 + lit(2).seconds)) result = joined_table.select(joined_table.a, joined_table.b, joined_table.e, joined_table.rowtime1) 4.Inner Join with Table Function (UDTF):Batch Streamingjoin 表和表函数的结果。左(外部)表的每一行都会 join 表函数相应调用产生的所有行。 如果表函数调用返回空结果,则删除左侧(外部)表的一行。Java:Table orders = tableEnv.from("Orders"); Table result = orders.groupBy($("a")).select($("a"), $("b").sum().as("d")); Scala Python Scala:// 实例化 User-Defined Table Function val split: TableFunction[_] = new MySplitUDTF() // join val result: Table = table .joinLateral(split($"c") as ("s", "t", "v")) .select($"a", $"b", $"s", $"t", $"v") Python:# 注册 User-Defined Table Function @udtf(result_types=[DataTypes.BIGINT(), DataTypes.BIGINT(), DataTypes.BIGINT()]) def split(x): return [Row(1, 2, 3)] # join orders = t_env.from_path("Orders") joined_table = orders.left_outer_join_lateral(split(orders.c).alias("s, t, v")) result = joined_table.select(joined_table.a, joined_table.b, joined_table.s, joined_table.t, joined_table.v) 5.Join with Temporal TableTemporal table 是跟踪随时间变化的表。Temporal table 函数提供对特定时间点 temporal table 状态的访问。表与 temporal table 函数进行 join 的语法和使用表函数进行 inner join 的语法相同。目前仅支持与 temporal table 的 inner join。Java:Table ratesHistory = tableEnv.from("RatesHistory"); // 注册带有时间属性和主键的 temporal table function TemporalTableFunction rates = ratesHistory.createTemporalTableFunction( "r_proctime", "r_currency"); tableEnv.registerFunction("rates", rates); // 基于时间属性和键与“Orders”表关联 Table orders = tableEnv.from("Orders"); Table result = orders .joinLateral(call("rates", $("o_proctime")), $("o_currency").isEqual($("r_currency"))); Python:目前不支持 Python 的 Table API。 十一、Set Operations1.Union:Batch和 SQL UNION 子句类似。Union 两张表会删除重复记录。两张表必须具有相同的字段类型。Java:Table left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.union(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.union(right) 2.UnionAll:Batch Streaming和 SQL UNION ALL 子句类似。Union 两张表。 两张表必须具有相同的字段类型。Java:Table left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.unionAll(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.unionAll(right) 3.Intersect:Batch和 SQL INTERSECT 子句类似。Intersect 返回两个表中都存在的记录。如果一条记录在一张或两张表中存在多次,则只返回一条记录,也就是说,结果表中不存在重复的记录。两张表必须具有相同的字段类型。Java:Table left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.intersect(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.intersect(right) 4.IntersectAll:Batch和 SQL INTERSECT ALL 子句类似。IntersectAll 返回两个表中都存在的记录。如果一条记录在两张表中出现多次,那么该记录返回的次数同该记录在两个表中都出现的次数一致,也就是说,结果表可能存在重复记录。两张表必须具有相同的字段类型。Java:Table left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.intersectAll(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.intersectAll(right) 5.Minus:Batch和 SQL EXCEPT 子句类似。Minus 返回左表中存在且右表中不存在的记录。左表中的重复记录只返回一次,换句话说,结果表中没有重复记录。两张表必须具有相同的字段类型。JavaTable left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.minus(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.minus(right) 6.MinusAll:Batch和 SQL EXCEPT ALL 子句类似。MinusAll 返回右表中不存在的记录。在左表中出现 n 次且在右表中出现 m 次的记录,在结果表中出现 (n - m) 次,例如,也就是说结果中删掉了在右表中存在重复记录的条数的记录。两张表必须具有相同的字段类型。Java:Table left = tableEnv.from("orders1"); Table right = tableEnv.from("orders2"); left.minusAll(right); Scala:val left = tableEnv.from("orders1") val right = tableEnv.from("orders2") left.minusAll(right) 7.In:Batch Streaming和 SQL IN 子句类似。如果表达式的值存在于给定表的子查询中,那么 In 子句返回 true。子查询表必须由一列组成。这个列必须与表达式具有相同的数据类型。Java:Table left = tableEnv.from("Orders1") Table right = tableEnv.from("Orders2"); Table result = left.select($("a"), $("b"), $("c")).where($("a").in(right)); Scala:val left = tableEnv.from("Orders1") val right = tableEnv.from("Orders2"); val result = left.select($"a", $"b", $"c").where($"a".in(right)) Python:left = t_env.from_path("Source1").select(col('a'), col('b'), col('c')) right = t_env.from_path("Source2").select(col('a')) result = left.select(left.a, left.b, left.c).where(left.a.in_(right)) 十二、OrderBy, Offset & Fetch1.Order By:Batch Streaming和 SQL ORDER BY 子句类似。返回跨所有并行分区的全局有序记录。对于无界表,该操作需要对时间属性进行排序或进行后续的 fetch 操作。Java:from pyflink.table.expressions import col left = t_env.from_path("Source1").select(col('a'), col('b'), col('c')) right = t_env.from_path("Source2").select(col('d'), col('e'), col('f')) left_outer_result = left.left_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) right_outer_result = left.right_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) full_outer_result = left.full_outer_join(right, left.a == right.d).select(left.a, left.b, right.e) Scala:val result = in.orderBy($"a".asc)Python:result = in.order_by(in.a.asc)2.Offset & Fetch:Batch Streaming和 SQL 的 OFFSET 和 FETCH 子句类似。Offset 操作根据偏移位置来限定(可能是已排序的)结果集。Fetch 操作将(可能已排序的)结果集限制为前 n 行。通常,这两个操作前面都有一个排序操作。对于无界表,offset 操作需要 fetch 操作。Java:// 从已排序的结果集中返回前5条记录 Table result1 = in.orderBy($("a").asc()).fetch(5); // 从已排序的结果集中返回跳过3条记录之后的所有记录 Table result2 = in.orderBy($("a").asc()).offset(3); // 从已排序的结果集中返回跳过10条记录之后的前5条记录 Table result3 = in.orderBy($("a").asc()).offset(10).fetch(5); Scala:// 从已排序的结果集中返回前5条记录 val result1: Table = in.orderBy($"a".asc).fetch(5) // 从已排序的结果集中返回跳过3条记录之后的所有记录 val result2: Table = in.orderBy($"a".asc).offset(3) // 从已排序的结果集中返回跳过10条记录之后的前5条记录 val result3: Table = in.orderBy($"a".asc).offset(10).fetch(5) Python:# 从已排序的结果集中返回前5条记录 result1 = table.order_by(table.a.asc).fetch(5) # 从已排序的结果集中返回跳过3条记录之后的所有记录 result2 = table.order_by(table.a.asc).offset(3) # 从已排序的结果集中返回跳过10条记录之后的前5条记录 result3 = table.order_by(table.a.asc).offset(10).fetch(5) 3.Insert:Batch Streaming和 SQL 查询中的 INSERT INTO 子句类似,该方法执行对已注册的输出表的插入操作。executeInsert() 方法将立即提交执行插入操作的 Flink job。输出表必须已注册在 TableEnvironment(详见表连接器)中。此外,已注册表的 schema 必须与查询中的 schema 相匹配。Java:Table orders = tableEnv.from("Orders"); orders.executeInsert("OutOrders");Scala:val orders = tableEnv.from("Orders") orders.executeInsert("OutOrders")Python:orders = t_env.from_path("Orders") orders.execute_insert("OutOrders")十三、Windows1.Group WindowsGroup window 聚合根据时间或行计数间隔将行分为有限组,并为每个分组进行一次聚合函数计算。对于批处理表,窗口是按时间间隔对记录进行分组的便捷方式。窗口是使用 window(GroupWindow w) 子句定义的,并且需要使用 as 子句来指定别名。为了按窗口对表进行分组,窗口别名必须像常规分组属性一样在 groupBy(…) 子句中引用。 以下示例展示了如何在表上定义窗口聚合Java:Table table = input .window([GroupWindow w].as("w")) // 定义窗口并指定别名为 w .groupBy($("w")) // 以窗口 w 对表进行分组 .select($("b").sum()); // 聚合Scala:val table = input .window([w: GroupWindow] as $"w") // 定义窗口并指定别名为 w .groupBy($"w") // 以窗口 w 对表进行分组 .select($"b".sum) // 聚合Python:# 定义窗口并指定别名为 w,以窗口 w 对表进行分组,然后再聚合 table = input.window([w: GroupWindow].alias("w")) \ .group_by(col('w')).select(input.b.sum)在流环境中,如果窗口聚合除了窗口之外还根据一个或多个属性进行分组,则它们只能并行计算,例如,groupBy(…) 子句引用了一个窗口别名和至少一个附加属性。仅引用窗口别名(例如在上面的示例中)的 groupBy(…) 子句只能由单个非并行任务进行计算。 以下示例展示了如何定义有附加分组属性的窗口聚合。Java:Table table = input .window([GroupWindow w].as("w")) // 定义窗口并指定别名为 w .groupBy($("w"), $("a")) // 以属性 a 和窗口 w 对表进行分组 .select($("a"), $("b").sum()); // 聚合Scala:val table = input .window([w: GroupWindow] as $"w") // 定义窗口并指定别名为 w .groupBy($"w", $"a") // 以属性 a 和窗口 w 对表进行分组 .select($"a", $"b".sum) // 聚合Python:# 定义窗口并指定别名为 w,以属性 a 和窗口 w 对表进行分组, # 然后再聚合 table = input.window([w: GroupWindow].alias("w")) \ .group_by(col('w'), input.a).select(input.b.sum)时间窗口的开始、结束或行时间戳等窗口属性可以作为窗口别名的属性添加到 select 子句中,如 w.start、w.end 和 w.rowtime。窗口开始和行时间戳是包含的上下窗口边界。相反,窗口结束时间戳是唯一的上窗口边界。例如,从下午 2 点开始的 30 分钟滚动窗口将 “14:00:00.000” 作为开始时间戳,“14:29:59.999” 作为行时间时间戳,“14:30:00.000” 作为结束时间戳。Java:able table = input .window([GroupWindow w].as("w")) // 定义窗口并指定别名为 w .groupBy($("w"), $("a")) // 以属性 a 和窗口 w 对表进行分组 .select($("a"), $("w").start(), $("w").end(), $("w").rowtime(), $("b").count()); // 聚合并添加窗口开始、结束和 rowtime 时间戳Scala:val table = input .window([w: GroupWindow] as $"w") // 定义窗口并指定别名为 w .groupBy($"w", $"a") // 以属性 a 和窗口 w 对表进行分组 .select($"a", $"w".start, $"w".end, $"w".rowtime, $"b".count) // 聚合并添加窗口开始、结束和 rowtime 时间戳Python:# 定义窗口并指定别名为 w,以属性 a 和窗口 w 对表进行分组, # 然后再聚合并添加窗口开始、结束和 rowtime 时间戳 table = input.window([w: GroupWindow].alias("w")) \ .group_by(col('w'), input.a) \ .select(input.a, col('w').start, col('w').end, col('w').rowtime, input.b.count)Window 参数定义了如何将行映射到窗口。 Window 不是用户可以实现的接口。相反,Table API 提供了一组具有特定语义的预定义 Window 类。下面列出了支持的窗口定义。2.Tumble (Tumbling Windows)滚动窗口将行分配给固定长度的非重叠连续窗口。例如,一个 5 分钟的滚动窗口以 5 分钟的间隔对行进行分组。滚动窗口可以定义在事件时间、处理时间或行数上。滚动窗口是通过 Tumble 类定义的,具体如下:MethodDescriptionover将窗口的长度定义为时间或行计数间隔。on要对数据进行分组(时间间隔)或排序(行计数)的时间属性。批处理查询支持任意 Long 或 Timestamp 类型的属性。流处理查询仅支持声明的事件时间或处理时间属性。alias指定窗口的别名。别名用于在 group_by() 子句中引用窗口,并可以在 select() 子句中选择如窗口开始、结束或行时间戳的窗口属性。Java:// Tumbling Event-time Window .window(Tumble.over(lit(10).minutes()).on($("rowtime")).as("w")); // Tumbling Processing-time Window (assuming a processing-time attribute "proctime") .window(Tumble.over(lit(10).minutes()).on($("proctime")).as("w")); // Tumbling Row-count Window (assuming a processing-time attribute "proctime") .window(Tumble.over(rowInterval(10)).on($("proctime")).as("w"));Scala:// Tumbling Event-time Window .window(Tumble over 10.minutes on $"rowtime" as $"w") // Tumbling Processing-time Window (assuming a processing-time attribute "proctime") .window(Tumble over 10.minutes on $"proctime" as $"w") // Tumbling Row-count Window (assuming a processing-time attribute "proctime") .window(Tumble over 10.rows on $"proctime" as $"w")Python:# Tumbling Event-time Window .window(Tumble.over(lit(10).minutes).on(col('rowtime')).alias("w")) # Tumbling Processing-time Window (assuming a processing-time attribute "proctime") .window(Tumble.over(lit(10).minutes).on(col('proctime')).alias("w")) # Tumbling Row-count Window (assuming a processing-time attribute "proctime") .window(Tumble.over(row_interval(10)).on(col('proctime')).alias("w"))3.Slide (Sliding Windows)滑动窗口具有固定大小并按指定的滑动间隔滑动。如果滑动间隔小于窗口大小,则滑动窗口重叠。因此,行可能分配给多个窗口。例如,15 分钟大小和 5 分钟滑动间隔的滑动窗口将每一行分配给 3 个不同的 15 分钟大小的窗口,以 5 分钟的间隔进行一次计算。滑动窗口可以定义在事件时间、处理时间或行数上。滑动窗口是通过 Slide 类定义的,具体如下:MethodDescriptionover 将窗口的长度定义为时间或行计数间隔。every将窗口的长度定义为时间或行计数间隔。滑动间隔的类型必须与窗口长度的类型相同。on要对数据进行分组(时间间隔)或排序(行计数)的时间属性。批处理查询支持任意 Long 或 Timestamp 类型的属性。流处理查询仅支持声明的事件时间或处理时间属性。as指定窗口的别名。别名用于在 groupBy() 子句中引用窗口,并可以在 select() 子句中选择如窗口开始、结束或行时间戳的窗口属性。Java:// Sliding Event-time Window .window(Slide.over(lit(10).minutes()) .every(lit(5).minutes()) .on($("rowtime")) .as("w")); // Sliding Processing-time window (assuming a processing-time attribute "proctime") .window(Slide.over(lit(10).minutes()) .every(lit(5).minutes()) .on($("proctime")) .as("w")); // Sliding Row-count window (assuming a processing-time attribute "proctime") .window(Slide.over(rowInterval(10)).every(rowInterval(5)).on($("proctime")).as("w"));Scala:// Sliding Event-time Window .window(Slide over 10.minutes every 5.minutes on $"rowtime" as $"w") // Sliding Processing-time window (assuming a processing-time attribute "proctime") .window(Slide over 10.minutes every 5.minutes on $"proctime" as $"w") // Sliding Row-count window (assuming a processing-time attribute "proctime") .window(Slide over 10.rows every 5.rows on $"proctime" as $"w")Python:# Sliding Event-time Window .window(Slide.over(lit(10).minutes).every(lit(5).minutes).on(col('rowtime')).alias("w")) # Sliding Processing-time window (assuming a processing-time attribute "proctime") .window(Slide.over(lit(10).minutes).every(lit(5).minutes).on(col('proctime')).alias("w")) # Sliding Row-count window (assuming a processing-time attribute "proctime") .window(Slide.over(row_interval(10)).every(row_interval(5)).on(col('proctime')).alias("w"))4.Session (Session Windows)会话窗口没有固定的大小,其边界是由不活动的间隔定义的,例如,如果在定义的间隔期内没有事件出现,则会话窗口将关闭。例如,定义30 分钟间隔的会话窗口,当观察到一行在 30 分钟内不活动(否则该行将被添加到现有窗口中)且30 分钟内没有添加新行,窗口会关闭。会话窗口支持事件时间和处理时间。MethodDescriptionwithGap将两个窗口之间的间隙定义为时间间隔。on要对数据进行分组(时间间隔)或排序(行计数)的时间属性。批处理查询支持任意 Long 或 Timestamp 类型的属性。流处理查询仅支持声明的事件时间或处理时间属性。as指定窗口的别名。别名用于在 groupBy() 子句中引用窗口,并可以在 select() 子句中选择如窗口开始、结束或行时间戳的窗口属性。Java:// Session Event-time Window .window(Session.withGap(lit(10).minutes()).on($("rowtime")).as("w")); // Session Processing-time Window (assuming a processing-time attribute "proctime") .window(Session.withGap(lit(10).minutes()).on($("proctime")).as("w"));Scala:// Session Event-time Window .window(Session withGap 10.minutes on $"rowtime" as $"w") // Session Processing-time Window (assuming a processing-time attribute "proctime") .window(Session withGap 10.minutes on $"proctime" as $"w")Python:# Session Event-time Window .window(Session.with_gap(lit(10).minutes).on(col('rowtime')).alias("w")) # Session Processing-time Window (assuming a processing-time attribute "proctime") .window(Session.with_gap(lit(10).minutes).on(col('proctime')).alias("w"))5.Over WindowsOver window 聚合聚合来自在标准的 SQL(OVER 子句),可以在 SELECT 查询子句中定义。与在“GROUP BY”子句中指定的 group window 不同, over window 不会折叠行。相反,over window 聚合为每个输入行在其相邻行的范围内计算聚合。Over windows 使用 window(w: OverWindow*) 子句(在 Python API 中使用 over_window(*OverWindow))定义,并通过 select() 方法中的别名引用。以下示例显示如何在表上定义 over window 聚合。Java:Table table = input .window([OverWindow w].as("w")) // define over window with alias w .select($("a"), $("b").sum().over($("w")), $("c").min().over($("w"))); // aggregate over the over window wScala:val table = input .window([w: OverWindow] as $"w") // define over window with alias w .select($"a", $"b".sum over $"w", $"c".min over $"w") // aggregate over the over window wPython:# define over window with alias w and aggregate over the over window w table = input.over_window([w: OverWindow].alias("w")) \ .select(input.a, input.b.sum.over(col('w')), input.c.min.over(col('w')))OverWindow 定义了计算聚合的行范围。OverWindow 不是用户可以实现的接口。相反,Table API 提供了Over 类来配置 over window 的属性。可以在事件时间或处理时间以及指定为时间间隔或行计数的范围内定义 over window 。可以通过 Over 类(和其他类)上的方法来定义 over window,具体如下:Partition By可选的在一个或多个属性上定义输入的分区。每个分区单独排序,聚合函数分别应用于每个分区。注意:在流环境中,如果窗口包含 partition by 子句,则只能并行计算 over window 聚合。如果没有 partitionBy(…),数据流将由单个非并行任务处理。Order By必须的定义每个分区内行的顺序,从而定义聚合函数应用于行的顺序。注意:对于流处理查询,必须声明事件时间或处理时间属性。目前,仅支持单个排序属性。Preceding可选的定义了包含在窗口中并位于当前行之前的行的间隔。间隔可以是时间或行计数间隔。有界 over window 用间隔的大小指定,例如,时间间隔为10分钟或行计数间隔为10行。无界 over window 通过常量来指定,例如,用UNBOUNDED_RANGE指定时间间隔或用 UNBOUNDED_ROW 指定行计数间隔。无界 over windows 从分区的第一行开始。如果省略前面的子句,则使用 UNBOUNDED_RANGE 和 CURRENT_RANGE 作为窗口前后的默认值。Following可选的定义包含在窗口中并在当前行之后的行的窗口间隔。间隔必须以与前一个间隔(时间或行计数)相同的单位指定。目前,不支持在当前行之后有行的 over window。相反,你可以指定两个常量之一:CURRENT_ROW 将窗口的上限设置为当前行。CURRENT_RANGE 将窗口的上限设置为当前行的排序键,例如,与当前行具有相同排序键的所有行都包含在窗口中。如果省略后面的子句,则时间间隔窗口的上限定义为 CURRENT_RANGE,行计数间隔窗口的上限定义为CURRENT_ROW。As必须的为 over window 指定别名。别名用于在之后的 select() 子句中引用该 over window。注意:目前,同一个 select() 调用中的所有聚合函数必须在同一个 over window 上计算。6.Unbounded Over WindowsJava// 无界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over.partitionBy($("a")).orderBy($("rowtime")).preceding(UNBOUNDED_RANGE).as("w")); // 无界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .window(Over.partitionBy($("a")).orderBy("proctime").preceding(UNBOUNDED_RANGE).as("w")); // 无界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over.partitionBy($("a")).orderBy($("rowtime")).preceding(UNBOUNDED_ROW).as("w")); // 无界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .window(Over.partitionBy($("a")).orderBy($("proctime")).preceding(UNBOUNDED_ROW).as("w"));Scala// 无界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over partitionBy $"a" orderBy $"rowtime" preceding UNBOUNDED_RANGE as "w") // 无界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .window(Over partitionBy $"a" orderBy $"proctime" preceding UNBOUNDED_RANGE as "w") // 无界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over partitionBy $"a" orderBy $"rowtime" preceding UNBOUNDED_ROW as "w") // 无界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .window(Over partitionBy $"a" orderBy $"proctime" preceding UNBOUNDED_ROW as "w")Python# 无界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .over_window(Over.partition_by(col('a')).order_by(col('rowtime')).preceding(UNBOUNDED_RANGE).alias("w")) # 无界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .over_window(Over.partition_by(col('a')).order_by(col('proctime')).preceding(UNBOUNDED_RANGE).alias("w")) # 无界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .over_window(Over.partition_by(col('a')).order_by(col('rowtime')).preceding(UNBOUNDED_ROW).alias("w")) # 无界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .over_window(Over.partition_by(col('a')).order_by(col('proctime')).preceding(UNBOUNDED_ROW).alias("w"))7.Bounded Over WindowsJava// 有界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over.partitionBy($("a")).orderBy($("rowtime")).preceding(lit(1).minutes()).as("w")); // 有界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .window(Over.partitionBy($("a")).orderBy($("proctime")).preceding(lit(1).minutes()).as("w")); // 有界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over.partitionBy($("a")).orderBy($("rowtime")).preceding(rowInterval(10)).as("w")); // 有界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .window(Over.partitionBy($("a")).orderBy($("proctime")).preceding(rowInterval(10)).as("w"));Scala:// 有界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over partitionBy $"a" orderBy $"rowtime" preceding 1.minutes as "w") // 有界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .window(Over partitionBy $"a" orderBy $"proctime" preceding 1.minutes as "w") // 有界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .window(Over partitionBy $"a" orderBy $"rowtime" preceding 10.rows as "w") // 有界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .window(Over partitionBy $"a" orderBy $"proctime" preceding 10.rows as "w")Python:# 有界的事件时间 over window(假定有一个叫“rowtime”的事件时间属性) .over_window(Over.partition_by(col('a')).order_by(col('rowtime')).preceding(lit(1).minutes).alias("w")) # 有界的处理时间 over window(假定有一个叫“proctime”的处理时间属性) .over_window(Over.partition_by(col('a')).order_by(col('proctime')).preceding(lit(1).minutes).alias("w")) # 有界的事件时间行数 over window(假定有一个叫“rowtime”的事件时间属性) .over_window(Over.partition_by(col('a')).order_by(col('rowtime')).preceding(row_interval(10)).alias("w")) # 有界的处理时间行数 over window(假定有一个叫“proctime”的处理时间属性) .over_window(Over.partition_by(col('a')).order_by(col('proctime')).preceding(row_interval(10)).alias("w")) 十四、Row-based Operations基于行生成多列输出的操作。1.Map:Batch Streaming使用用户定义的标量函数或内置标量函数执行 map 操作。如果输出类型是复合类型,则输出将被展平。Java:public class MyMapFunction extends ScalarFunction { public Row eval(String a) { return Row.of(a, "pre-" + a); } @Override public TypeInformation<?> getResultType(Class<?>[] signature) { return Types.ROW(Types.STRING(), Types.STRING()); } } ScalarFunction func = new MyMapFunction(); tableEnv.registerFunction("func", func); Table table = input .map(call("func", $("c")).as("a", "b")); Scala:class MyMapFunction extends ScalarFunction { def eval(a: String): Row = { Row.of(a, "pre-" + a) } override def getResultType(signature: Array[Class[_]]): TypeInformation[_] = Types.ROW(Types.STRING, Types.STRING) } val func = new MyMapFunction() val table = input .map(func($"c")).as("a", "b") Python:使用 python 的通用标量函数或向量化标量函数执行 map 操作。如果输出类型是复合类型,则输出将被展平。from pyflink.common import Row from pyflink.table import DataTypes from pyflink.table.udf import udf def map_function(a: Row) -> Row: return Row(a.a + 1, a.b * a.b) # 使用 python 通用标量函数进行 map 操作 func = udf(map_function, result_type=DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.BIGINT()), DataTypes.FIELD("b", DataTypes.BIGINT())])) table = input.map(func).alias('a', 'b') # 使用 python 向量化标量函数进行 map 操作 pandas_func = udf(lambda x: x * 2, result_type=DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.BIGINT()), DataTypes.FIELD("b", DataTypes.BIGINT()))]), func_type='pandas') table = input.map(pandas_func).alias('a', 'b') 2.FlatMap:Batch StreamingJava:使用表函数执行 flatMap 操作。public class MyFlatMapFunction extends TableFunction<Row> { public void eval(String str) { if (str.contains("#")) { String[] array = str.split("#"); for (int i = 0; i < array.length; ++i) { collect(Row.of(array[i], array[i].length())); } } } @Override public TypeInformation<Row> getResultType() { return Types.ROW(Types.STRING(), Types.INT()); } } TableFunction func = new MyFlatMapFunction(); tableEnv.registerFunction("func", func); Table table = input .flatMap(call("func", $("c")).as("a", "b")); Scala:class MyFlatMapFunction extends TableFunction[Row] { def eval(str: String): Unit = { if (str.contains("#")) { str.split("#").foreach({ s => val row = new Row(2) row.setField(0, s) row.setField(1, s.length) collect(row) }) } } override def getResultType: TypeInformation[Row] = { Types.ROW(Types.STRING, Types.INT) } } val func = new MyFlatMapFunction val table = input .flatMap(func($"c")).as("a", "b") Python:from pyflink.table.udf import udtf from pyflink.table import DataTypes from pyflink.common import Row @udtf(result_types=[DataTypes.INT(), DataTypes.STRING()]) def split(x: Row) -> Row: for s in x.b.split(","): yield x.a, s input.flat_map(split) 3.Aggregate:Batch StreamingJava:使用聚合函数来执行聚合操作。你必须使用 select 子句关闭 aggregate,并且 select 子句不支持聚合函数。如果输出类型是复合类型,则聚合的输出将被展平。public class MyMinMaxAcc { public int min = 0; public int max = 0; } public class MyMinMax extends AggregateFunction<Row, MyMinMaxAcc> { public void accumulate(MyMinMaxAcc acc, int value) { if (value < acc.min) { acc.min = value; } if (value > acc.max) { acc.max = value; } } @Override public MyMinMaxAcc createAccumulator() { return new MyMinMaxAcc(); } public void resetAccumulator(MyMinMaxAcc acc) { acc.min = 0; acc.max = 0; } @Override public Row getValue(MyMinMaxAcc acc) { return Row.of(acc.min, acc.max); } @Override public TypeInformation<Row> getResultType() { return new RowTypeInfo(Types.INT, Types.INT); } } AggregateFunction myAggFunc = new MyMinMax(); tableEnv.registerFunction("myAggFunc", myAggFunc); Table table = input .groupBy($("key")) .aggregate(call("myAggFunc", $("a")).as("x", "y")) .select($("key"), $("x"), $("y")); Scala:case class MyMinMaxAcc(var min: Int, var max: Int) class MyMinMax extends AggregateFunction[Row, MyMinMaxAcc] { def accumulate(acc: MyMinMaxAcc, value: Int): Unit = { if (value < acc.min) { acc.min = value } if (value > acc.max) { acc.max = value } } override def createAccumulator(): MyMinMaxAcc = MyMinMaxAcc(0, 0) def resetAccumulator(acc: MyMinMaxAcc): Unit = { acc.min = 0 acc.max = 0 } override def getValue(acc: MyMinMaxAcc): Row = { Row.of(Integer.valueOf(acc.min), Integer.valueOf(acc.max)) } override def getResultType: TypeInformation[Row] = { new RowTypeInfo(Types.INT, Types.INT) } } val myAggFunc = new MyMinMax val table = input .groupBy($"key") .aggregate(myAggFunc($"a") as ("x", "y")) .select($"key", $"x", $"y") Python:使用 python 的通用聚合函数或 向量化聚合函数来执行聚合操作。你必须使用 select 子句关闭 aggregate ,并且 select 子句不支持聚合函数。如果输出类型是复合类型,则聚合的输出将被展平。from pyflink.common import Row from pyflink.table import DataTypes from pyflink.table.udf import AggregateFunction, udaf class CountAndSumAggregateFunction(AggregateFunction): def get_value(self, accumulator): return Row(accumulator[0], accumulator[1]) def create_accumulator(self): return Row(0, 0) def accumulate(self, accumulator, row: Row): accumulator[0] += 1 accumulator[1] += row.b def retract(self, accumulator, row: Row): accumulator[0] -= 1 accumulator[1] -= row.b def merge(self, accumulator, accumulators): for other_acc in accumulators: accumulator[0] += other_acc[0] accumulator[1] += other_acc[1] def get_accumulator_type(self): return DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.BIGINT()), DataTypes.FIELD("b", DataTypes.BIGINT())]) def get_result_type(self): return DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.BIGINT()), DataTypes.FIELD("b", DataTypes.BIGINT())]) function = CountAndSumAggregateFunction() agg = udaf(function, result_type=function.get_result_type(), accumulator_type=function.get_accumulator_type(), name=str(function.__class__.__name__)) # 使用 python 通用聚合函数进行聚合 result = t.group_by(t.a) \ .aggregate(agg.alias("c", "d")) \ .select("a, c, d") # 使用 python 向量化聚合函数进行聚合 pandas_udaf = udaf(lambda pd: (pd.b.mean(), pd.b.max()), result_type=DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.FLOAT()), DataTypes.FIELD("b", DataTypes.INT())]), func_type="pandas") t.aggregate(pandas_udaf.alias("a", "b")) \ .select("a, b") 4.Group Window Aggregate:Batch Streaming在 group window 和可能的一个或多个分组键上对表进行分组和聚合。你必须使用 select 子句关闭 aggregate。并且 select 子句不支持“*“或聚合函数。Java:AggregateFunction myAggFunc = new MyMinMax(); tableEnv.registerFunction("myAggFunc", myAggFunc); Table table = input .window(Tumble.over(lit(5).minutes()) .on($("rowtime")) .as("w")) // 定义窗口 .groupBy($("key"), $("w")) // 以键和窗口分组 .aggregate(call("myAggFunc", $("a")).as("x", "y")) .select($("key"), $("x"), $("y"), $("w").start(), $("w").end()); // 访问窗口属性与聚合结果Scala:val myAggFunc = new MyMinMax val table = input .window(Tumble over 5.minutes on $"rowtime" as "w") // 定义窗口 .groupBy($"key", $"w") // 以键和窗口分组 .aggregate(myAggFunc($"a") as ("x", "y")) .select($"key", $"x", $"y", $"w".start, $"w".end) // 访问窗口属性与聚合结果Python:from pyflink.table import DataTypes from pyflink.table.udf import AggregateFunction, udaf pandas_udaf = udaf(lambda pd: (pd.b.mean(), pd.b.max()), result_type=DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.FLOAT()), DataTypes.FIELD("b", DataTypes.INT())]), func_type="pandas") tumble_window = Tumble.over(expr.lit(1).hours) \ .on(expr.col("rowtime")) \ .alias("w") t.select(t.b, t.rowtime) \ .window(tumble_window) \ .group_by("w") \ .aggregate(pandas_udaf.alias("d", "e")) \ .select("w.rowtime, d, e")5.FlatAggregate和 GroupBy Aggregation 类似。使用运行中的表之后的聚合算子对分组键上的行进行分组,以按组聚合行。和 AggregateFunction 的不同之处在于,TableAggregateFunction 的每个分组可能返回0或多条记录。你必须使用 select 子句关闭 flatAggregate。并且 select 子句不支持聚合函数。除了使用 emitValue 输出结果,你还可以使用 emitUpdateWithRetract 方法。和 emitValue 不同的是,emitUpdateWithRetract 用于下发已更新的值。此方法在retract 模式下增量输出数据,例如,一旦有更新,我们必须在发送新的更新记录之前收回旧记录。如果在表聚合函数中定义了这两个方法,则将优先使用 emitUpdateWithRetract 方法而不是 emitValue 方法,这是因为该方法可以增量输出值,因此被视为比 emitValue 方法更有效。Java:/** * Top2 Accumulator。 */ public class Top2Accum { public Integer first; public Integer second; } /** * 用户定义的聚合函数 top2。 */ public class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> { @Override public Top2Accum createAccumulator() { Top2Accum acc = new Top2Accum(); acc.first = Integer.MIN_VALUE; acc.second = Integer.MIN_VALUE; return acc; } public void accumulate(Top2Accum acc, Integer v) { if (v > acc.first) { acc.second = acc.first; acc.first = v; } else if (v > acc.second) { acc.second = v; } } public void merge(Top2Accum acc, java.lang.Iterable<Top2Accum> iterable) { for (Top2Accum otherAcc : iterable) { accumulate(acc, otherAcc.first); accumulate(acc, otherAcc.second); } } public void emitValue(Top2Accum acc, Collector<Tuple2<Integer, Integer>> out) { // 下发 value 与 rank if (acc.first != Integer.MIN_VALUE) { out.collect(Tuple2.of(acc.first, 1)); } if (acc.second != Integer.MIN_VALUE) { out.collect(Tuple2.of(acc.second, 2)); } } } tEnv.registerFunction("top2", new Top2()); Table orders = tableEnv.from("Orders"); Table result = orders .groupBy($("key")) .flatAggregate(call("top2", $("a")).as("v", "rank")) .select($("key"), $("v"), $("rank");Scala:import java.lang.{Integer => JInteger} import org.apache.flink.table.api.Types import org.apache.flink.table.functions.TableAggregateFunction /** * Top2 Accumulator。 */ class Top2Accum { var first: JInteger = _ var second: JInteger = _ } /** * 用户定义的聚合函数 top2。 */ class Top2 extends TableAggregateFunction[JTuple2[JInteger, JInteger], Top2Accum] { override def createAccumulator(): Top2Accum = { val acc = new Top2Accum acc.first = Int.MinValue acc.second = Int.MinValue acc } def accumulate(acc: Top2Accum, v: Int) { if (v > acc.first) { acc.second = acc.first acc.first = v } else if (v > acc.second) { acc.second = v } } def merge(acc: Top2Accum, its: JIterable[Top2Accum]): Unit = { val iter = its.iterator() while (iter.hasNext) { val top2 = iter.next() accumulate(acc, top2.first) accumulate(acc, top2.second) } } def emitValue(acc: Top2Accum, out: Collector[JTuple2[JInteger, JInteger]]): Unit = { // 下发 value 与 rank if (acc.first != Int.MinValue) { out.collect(JTuple2.of(acc.first, 1)) } if (acc.second != Int.MinValue) { out.collect(JTuple2.of(acc.second, 2)) } } } val top2 = new Top2 val orders: Table = tableEnv.from("Orders") val result = orders .groupBy($"key") .flatAggregate(top2($"a") as ($"v", $"rank")) .select($"key", $"v", $"rank")Python:使用 python 通用 Table Aggregate Function 执行 flat_aggregate 操作。和 GroupBy Aggregation 类似。使用运行中的表之后的聚合运算符对分组键上的行进行分组,以按组聚合行。和 AggregateFunction 的不同之处在于,TableAggregateFunction 的每个分组可能返回0或多条记录。你必须使用 select 子句关闭 flat_aggregate。并且 select 子句不支持聚合函数。from pyflink.common import Row from pyflink.table.udf import TableAggregateFunction, udtaf from pyflink.table import DataTypes class Top2(TableAggregateFunction): def emit_value(self, accumulator): yield Row(accumulator[0]) yield Row(accumulator[1]) def create_accumulator(self): return [None, None] def accumulate(self, accumulator, row: Row): if row.a is not None: if accumulator[0] is None or row.a > accumulator[0]: accumulator[1] = accumulator[0] accumulator[0] = row.a elif accumulator[1] is None or row.a > accumulator[1]: accumulator[1] = row.a def merge(self, accumulator, accumulators): for other_acc in accumulators: self.accumulate(accumulator, other_acc[0]) self.accumulate(accumulator, other_acc[1]) def get_accumulator_type(self): return DataTypes.ARRAY(DataTypes.BIGINT()) def get_result_type(self): return DataTypes.ROW( [DataTypes.FIELD("a", DataTypes.BIGINT())]) mytop = udtaf(Top2()) t = t_env.from_elements([(1, 'Hi', 'Hello'), (3, 'Hi', 'hi'), (5, 'Hi2', 'hi'), (7, 'Hi', 'Hello'), (2, 'Hi', 'Hello')], ['a', 'b', 'c']) result = t.select(t.a, t.c) \ .group_by(t.c) \ .flat_aggregate(mytop) \ .select(t.a) \ .flat_aggregate(mytop.alias("b"))
0
0
0
浏览量2011
打酱油的后端

Flink系列之:监控Checkpoint

一、概览Flink 的 Web 界面提供了选项卡/标签(tab)来监视作业的 checkpoint 信息。作业终止后,这些统计信息仍然可用。有四个不同的选项卡可显示有关 checkpoint 的信息:概览(Overview),历史记录(History),摘要信息(Summary)和配置信息(Configuration)。以下各节将依次介绍这些内容。二、概览(Overview)选项卡Checkpoint CountsTriggered:自作业开始以来触发的 checkpoint 总数。In Progress:当前正在进行的 checkpoint 数量。Completed:自作业开始以来成功完成的 checkpoint 总数。Failed:自作业开始以来失败的 checkpoint 总数。Restored:自作业开始以来进行的恢复操作的次数。这还表示自 提交以来已重新启动多少次。请注意,带有 savepoint 的初始提交也算作一次恢复,如果 JobManager 在此操作过程中丢失,则该统计将重新计数。Latest Completed Checkpoint:最新(最近)成功完成的 checkpoint。点击 More details 可以得到 subtask 级别的详细统计信息。Latest Failed Checkpoint:最新失败的 checkpoint。点击 More details 可以得到 subtask 级别的详细统计信息。Latest Savepoint:最新触发的 savepoint 及其外部路径。点击 More details 可以得到 subtask 级别的详细统计信息。Latest Restore:有两种类型的恢复操作。Restore from Checkpoint:从 checkpoint 恢复。Restore from Savepoint:从 savepoint 恢复。三、历史记录(History)选项卡Checkpoint 历史记录保存有关最近触发的 checkpoint 的统计信息,包括当前正在进行的 checkpoint。注意,对于失败的 checkpoint,指标会尽最大努力进行更新,但是可能不准确。ID:已触发 checkpoint 的 ID。每个 checkpoint 的 ID 都会递增,从 1 开始。Status:Checkpoint 的当前状态,可以是正在进行(In Progress)、已完成(Completed) 或失败(Failed))。如果触发的检查点是一个保存点,你将看到一个 符号。Acknowledged:已确认完成的子任务数量与总任务数量。Trigger Time:在 JobManager 上发起 checkpoint 的时间。Latest Acknowledgement:JobManager 接收到任何 subtask 的最新确认的时间(如果尚未收到确认,则不适用)。End to End Duration:从触发时间戳到最后一次确认的持续时间(如果还没有收到确认,则不适用)。完整 checkpoint 的端到端持续时间由确认 checkpoint 的最后一个 subtask 确定。这个时间通常大于单个 subtask 实际 checkpoint state 所需的时间。Checkpointed Data Size: 在此次checkpoint的sync以及async阶段中持久化的数据量。如果启用了增量 checkpoint或者changelog,则此值可能会与全量checkpoint数据量产生区别。Full Checkpoint Data Size: 所有已确认的 subtask 的 checkpoint 的全量数据大小。Processed (persisted) in-flight data:在 checkpoint 对齐期间(从接收第一个和最后一个 checkpoint barrier 之间的时间)所有已确认的 subtask 处理/持久化 的大约字节数。如果启用了 unaligned checkpoint,持久化的字节数可能会大于0。对于 subtask,有两个更详细的统计信息可用。Sync Duration:Checkpoint 同步部分的持续时间。这包括 operator 的快照状态,并阻塞 subtask 上的所有其他活动(处理记录、触发计时器等)。Async Duration:Checkpoint 的异步部分的持续时间。这包括将 checkpoint 写入设置的文件系统所需的时间。对于 unaligned checkpoint,这还包括 subtask 必须等待最后一个 checkpoint barrier 到达的时间(checkpoint alignment 持续时间)以及持久化数据所需的时间。Alignment Duration:处理第一个和最后一个 checkpoint barrier 之间的时间。对于 checkpoint alignment 机制的 checkpoint,在 checkpoint alignment 过程中,已经接收到 checkpoint barrier 的 channel 将阻塞并停止处理后续的数据。Start Delay:从 checkpoint barrier 创建开始到 subtask 收到第一个 checkpoint barrier 所用的时间。Unaligned Checkpoint:Checkpoint 完成的时候是否是一个 unaligned checkpoint。在 alignment 超时的时候 aligned checkpoint 可以自动切换成 unaligned checkpoint。四、历史记录数量配置你可以通过以下配置键配置历史记录所保存的最近检查点的数量。默认值为 10。# 保存最近 checkpoint 的个数 web.checkpoints.history: 15五、摘要信息(Summary)选项卡摘要计算了所有已完成 checkpoint 的端到端持续时间、增量/全量Checkpoint 数据大小和 checkpoint alignment 期间缓冲的字节数的简单 min/average/maximum 统计信息。请注意,这些统计信息不会在 JobManager 丢失后无法保存,如果 JobManager 故障转移,这些统计信息将重新计数。六、配置信息(Configuration)选项卡该配置选项卡列出了你指定的配置(streaming configuration):Checkpointing Mode:恰好一次(Exactly Once)或者至少一次(At least Once)。Interval:配置的 checkpoint 触发间隔。在此间隔内触发 checkpoint。Timeout:超时之后,JobManager 取消 checkpoint 并触发新的 checkpoint。Minimum Pause Between Checkpoints:Checkpoint 之间所需的最小暂停时间。Checkpoint 成功完成后,我们至少要等这段时间再触发下一个,这可能会延迟正常的间隔。Maximum Concurrent Checkpoints:可以同时进行的最大 checkpoint 个数。Persist Checkpoints Externally:启用或禁用持久化 checkpoint 到外部系统。如果启用,还会列出外部化 checkpoint 的清理配置(取消时删除或保留)。七、Checkpoint 详细信息当你点击某个 checkpoint 的 More details 链接时,你将获得其所有 operator 的 Minimum/Average/Maximum 摘要信息,以及每个 subtask 单独的详细量化信息。每个 Operator 的摘要信息所有 Subtask 的统计信息
0
0
0
浏览量1071
打酱油的后端

Flink系列之:动态发现新增分区

为了在不重新启动 Flink 作业的情况下处理主题扩展或主题创建等场景,可以将 Kafka 源配置为在提供的主题分区订阅模式下定期发现新分区。要启用分区发现,请为属性partition.discovery.interval.ms设置一个非负值。一、动态发现新增分区flink程序增加自动发现分区参数:flink.partition-discovery.interval-millis是一个配置属性,用于设置Flink作业中的分区发现间隔时间(以毫秒为单位)。在Flink作业中,数据源(例如Kafka或文件系统)的分区可能会发生变化。为了及时感知分区的变化情况,并根据变化进行相应的处理,Flink提供了分区发现机制。flink.partition-discovery.interval-millis配置属性用于设置Flink作业在进行分区发现时的间隔时间。Flink作业会定期检查数据源的分区情况,如果发现分区发生了变化(例如增加或减少了分区),Flink会相应地调整作业的并行度或重新分配任务来适应新的分区情况。通过调整flink.partition-discovery.interval-millis的值,可以控制Flink作业进行分区发现的频率。较小的间隔时间可以实时感知到分区变化,但可能会增加作业的开销;较大的间隔时间可以减少开销,但可能导致较长时间的延迟。需要注意的是,flink.partition-discovery.interval-millis的默认值是5分钟(300000毫秒),可以根据具体需求进行调整。二、Flink SQL动态发现新增分区参数:scan.topic-partition-discovery.intervalCREATE TABLE KafkaTable ( `event_time` TIMESTAMP(3) METADATA FROM 'timestamp', `partition` BIGINT METADATA VIRTUAL, `offset` BIGINT METADATA VIRTUAL, `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'format' = 'csv' );Connector Options:OptionRequiredDefaultTypeDescriptionscan.topic-partition-discovery.intervaloptional(none)Duration消费者定期发现动态创建的Kafka主题和分区的时间间隔。三、Flink API动态发现新增分区参数:partition.discovery.interval.msJavaKafkaSource.builder() .setProperty("partition.discovery.interval.ms", "10000"); // discover new partitions per 10 secondsPythonKafkaSource.builder() \ .set_property("partition.discovery.interval.ms", "10000") # discover new partitions per 10 seconds
0
0
0
浏览量859
打酱油的后端

Flink系列之:Checkpoints 与 Savepoints

一、概述从概念上讲,Flink 的 savepoints 与 checkpoints 的不同之处类似于传统数据库系统中的备份与恢复日志之间的差异。Checkpoints 的主要目的是为意外失败的作业提供恢复机制。 Checkpoint 的生命周期 由 Flink 管理, 即 Flink 创建,管理和删除 checkpoint - 无需用户交互。 由于 checkpoint 被经常触发,且被用于作业恢复,所以 Checkpoint 的实现有两个设计目标:i)轻量级创建和 ii)尽可能快地恢复。 可能会利用某些特定的属性来达到这个目标,例如, 作业的代码在执行尝试时不会改变。在用户终止作业后,会自动删除 Checkpoint(除非明确配置为保留的 Checkpoint)。Checkpoint 以状态后端特定的(原生的)数据格式存储(有些状态后端可能是增量的)尽管 savepoints 在内部使用与 checkpoints 相同的机制创建,但它们在概念上有所不同,并且生成和恢复的成本可能会更高一些。Savepoints的设计更侧重于可移植性和操作灵活性,尤其是在 job 变更方面。Savepoint 的用例是针对计划中的、手动的运维。例如,可能是更新你的 Flink 版本,更改你的作业图等等。Savepoint 仅由用户创建、拥有和删除。这意味着 Flink 在作业终止后和恢复后都不会删除 savepoint。Savepoint 以状态后端独立的(标准的)数据格式存储(注意:从 Flink 1.15 开始,savepoint 也可以以后端特定的原生格式存储,这种格式创建和恢复速度更快,但有一些限制)。二、功能和限制下表概述了各种类型的 savepoint 和 checkpoint 的功能和限制。✓ - Flink 完全支持这种类型的快照x - Flink 不支持这种类型的快照! - 虽然这些操作目前有效,但 Flink 并未正式保证对它们的支持,因此它们存在一定程度的风险更换状态后端 - 配置与创建快照时使用的不同的状态后端。State Processor API (写) - 通过 State Processor API 创建这种类型的新快照的能力。State Processor API (读) - 通过 State Processor API 从该类型的现有快照中读取状态的能力。自包含和可移动 - 快照目录包含从该快照恢复所需的所有内容,并且不依赖于其他快照,这意味着如果需要的话,它可以轻松移动到另一个地方。Schema 变更 - 如果使用支持 Schema 变更的序列化器(例如 POJO 和 Avro 类型),则可以更改状态数据类型。任意 job 升级 - 即使现有算子的 partitioning 类型(rescale, rebalance, map, 等)或运行中数据类型已经更改,也可以从该快照恢复。非任意 job 升级 - 如果作业图拓扑和运行中数据类型保持不变,则可以使用变更后的 operator 恢复快照。Flink 小版本升级 - 从更旧的 Flink 小版本创建的快照恢复(1.x → 1.y)。Flink bug/patch 版本升级 - 从更旧的 Flink 补丁版本创建的快照恢复(1.14.x → 1.14.y)。扩缩容 - 使用与快照制作时不同的并发度从该快照恢复。
0
0
0
浏览量1644
打酱油的后端

Flink系列Table API和SQL之:创建表环境和创建表

一、快速上手Table API和SQL创建表环境TableEnvironment tableEnv = ...;创建输入表,连接外部系统读取数据tableEnv.executeSql("CREATE TEMPORARY TABLE inputTable ... WITH ('connector' = ... )");注册一个表,连接到外部系统,用于输出tableEnv.executeSql("CREATE TEMPORARY TABLE outputTable ... WITH ('connector' = ... )");执行SQL对表进行查询转换,得到一个新的表Table table1 = tableEnv.sqlQuery("SELECT ... FROM inputTable ...");使用Table API对表进行查询转换,得到一个新的表Table table2 = tableEnv.from("inputTable").select(...);将得到的结果写入输出表TableResult tableResult = table1.executeInsert("outputTable");二、创建表环境对于Flink这样的流处理框架来说,数据流和表在结构上还是有所区别的。所以使用Table API和SQL需要一个特别的运行时环境,这就是所谓的表环境(TableEnvironment)。主要负责:注册Catalog和表执行SQL查询注册用户自定义函数(UDF)DataStream和表之间的转换这里的Catalog就是目录,与标准SQL中的概念是一致的,主要用来管理所有数据库(database)和表(table)的元数据(metadata)。通过Catalog可以方便地对数据库和表进行查询的管理,所以可以认为我们所定义的表都会挂靠在某个目录下,这样就可以快速检索。在表环境中可以由用户自定义Catalog,并在其中注册表和自定义函数(UDF)。默认的Catalog就叫作default_catalog。每个表和SQL的执行,都必须绑定在一个表环境(TableEnvironment)中。TableEnvironment是Table API中提供的基本接口类,可以通过调用静态的create()方法来创建一个表环境实例。方法需要传入一个环境的配置参数EnvironmentSettings,它可以指定当前表环境的执行模式和计划器(planner)。执行模式有批处理和流处理两种选择,默认是流处理模式。计划器默认使用blink planner。import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.table.api.EnvironmentSettings; import org.apache.flink.table.api.TableEnvironment; import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; public class CommonApiTest { public static void main(String[] args) { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment talbeEnv = StreamTableEnvironment.create(env); //1。定义环境配置来创建表执行环境 EnvironmentSettings settings = EnvironmentSettings.newInstance() .inStreamingMode() .useBlinkPlanner() .build(); TableEnvironment tableEnv = TableEnvironment.create(settings); //2.基于blink版本planner进行批处理 EnvironmentSettings settings2 = EnvironmentSettings.newInstance() .inBatchMode() .useBlinkPlanner() .build(); TableEnvironment tableEnv3 = TableEnvironment.create(settings2); } }三、创建表表(Table)是关系型数据库中数据存储的基本形式,也是SQL执行的基本对象。Flink中的表是由多个行数据构成的,每个行(Row)又可以有定义好的多个列(Column)字段。整体来看,表就是固定类型的数据组成的二维矩阵。为了方便的查询表,表环境中会维护一个目录(Catalog)和表的对应关系。所以表都是通过Catalog来进行注册创建的。表在环境中有一个唯一的ID,由三部分组成:目录(catalog)名,数据库(database)名,以及表名。在默认情况下,目录名为default_catalog,数据库名为default_database。所以如果我们直接创建一个叫做MyTable的表,它的ID就是:default_catalog.default_database.MyTable具体创建表的方式,有通过连接器(connector)和虚拟表(virtual tables)两种。1.连接器表(Connector Tables)最直观的创建表的方式,就是通过连接器(connector)连接到一个外部系统,然后定义出对应的表结构。例如我们可以连接到Kafka或者文件系统,将存储在这些外部系统的数据以表的形式定义出来,这样对表的读写就可以通过连接器转换成对外部系统的读写了。当我们在表环境中读取这张表,连接器就会从外部系统读取数据并进行转换。而当我们向这张表写入数据,连接器就会将数据输出(Sink)到外部系统中。在代码中,可以调用表环境的executeSql()方法,可以传入一个DDL作为参数执行SQL操作。我们传入一个CREATE语句进行表的创建,并通过WITH关键字指定连接到外部系统的连接器:tableEnv.executeSql("CREATE [TEMPORARY] TABLE MyTable ... WITH ('connector' = ... )");这里的TEMPORARY关键字可以省略。这里没有定义Catalog和Database,所以都是默认的,表的完整ID就是default_catalog.default_database.MyTable。如果希望使用自定义的目录名和库名,可以在环境中进行设置:tEnv.useCatalog("custom_catalog"); tEnv.useDatabase("custom_database");这样创建的表完整ID就变成了custom_catalog.custom_database.MyTable。之后在表环境中创建的所有表,ID也会都以custom_catalog.custom_database作为前缀。 //2。创建表 String creatDDL = "CREATE TABLE clickTable (" + "user STRING, " + "url STRING, " + "ts BIGINT " + ") WITH (" + " 'connector' = 'filesystem',"+ " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink" + "/input/clicks.txt'," + " 'format' = 'csv'" + ")"; tableEnv.executeSql(creatDDL); //创建一张用于输出的表 //2。创建表 String creatOutDDL = "CREATE TABLE outTable (" + "user STRING, " + "url STRING, " + "ts BIGINT " + ") WITH (" + " 'connector' = 'filesystem',"+ " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink" + "/input/output.txt'," + " 'format' = 'csv'" + ")"; tableEnv.executeSql(creatOutDDL);2.虚拟表(Virtual Tables)在环境中注册之后,我们就可以在SQL中直接使用这张表进行查询转换了。Table newTable = tableEnv.sqlQuery("SELECT ... FROM MyTable ...");这里调用了表环境的sqlQuery()方法,直接传入一条SQL语句作为参数执行查询,得到的结果是一个Table对象。Table是Table API中提供的核心接口类,就代表了一个Java中定义的表实例。得到的newTable是一个中间转换结果,如果之后又希望直接使用这个表执行SQL,又该怎么做呢?由于newTable是一个Table对象,并没有在表环境中注册。所以我们还需要将这个中间结果表注册到环境中,才能在SQL中使用:tableEnv.createTemporaryView("NewTable",newTable);我们发现,这里的注册其实是创建了一个虚拟表(Virtual Table)。这个概念与SQL语法中的视图(View)非常类似,所以调用的方法也叫作创建虚拟视图(createTemporaryView)。视图之所以是虚拟的,是因为我们并不会直接保存这个表的内容,并没有实体。只是在用到这张表的时候,会将它对应的查询语句嵌入到SQL中。
0
0
0
浏览量2011
打酱油的后端

Flink系列之:Apache Kafka SQL 连接器

一、Apache Kafka SQL 连接器Scan Source: Unbounded Sink: Streaming Append ModeKafka 连接器提供从 Kafka topic 中消费和写入数据的能力。二、依赖 <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka</artifactId> <version>3.0.2-1.18</version> </dependency>三、创建Kafka 表以下示例展示了如何创建 Kafka 表:CREATE TABLE KafkaTable ( `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING, `ts` TIMESTAMP(3) METADATA FROM 'timestamp' ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'format' = 'csv' )四、可用的元数据以下的连接器元数据可以在表定义中通过元数据列的形式获取。R/W 列定义了一个元数据是可读的(R)还是可写的(W)。 只读列必须声明为 VIRTUAL 以在 INSERT INTO 操作中排除它们。以下扩展的 CREATE TABLE 示例展示了使用这些元数据字段的语法:CREATE TABLE KafkaTable ( `event_time` TIMESTAMP(3) METADATA FROM 'timestamp', `partition` BIGINT METADATA VIRTUAL, `offset` BIGINT METADATA VIRTUAL, `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'format' = 'csv' );格式元信息连接器可以读出消息格式的元数据。格式元数据的配置键以 ‘value.’ 作为前缀。以下示例展示了如何获取 Kafka 和 Debezium 的元数据字段:CREATE TABLE KafkaTable ( `event_time` TIMESTAMP(3) METADATA FROM 'value.source.timestamp' VIRTUAL, -- from Debezium format `origin_table` STRING METADATA FROM 'value.source.table' VIRTUAL, -- from Debezium format `partition_id` BIGINT METADATA FROM 'partition' VIRTUAL, -- from Kafka connector `offset` BIGINT METADATA VIRTUAL, -- from Kafka connector `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING ) WITH ( 'connector' = 'kafka', 'topic' = 'user_behavior', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'testGroup', 'scan.startup.mode' = 'earliest-offset', 'value.format' = 'debezium-json' );五、连接器参数六、特性消息键(Key)与消息体(Value)的格式Kafka 消息的消息键和消息体部分都可以使用某种 格式 来序列化或反序列化成二进制数据。消息体格式由于 Kafka 消息中消息键是可选的,以下语句将使用消息体格式读取和写入消息,但不使用消息键格式。 ‘format’ 选项与 ‘value.format’ 意义相同。 所有的格式配置使用格式识别符作为前缀。CREATE TABLE KafkaTable ( `ts` TIMESTAMP(3) METADATA FROM 'timestamp', `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING ) WITH ( 'connector' = 'kafka', ... 'format' = 'json', 'json.ignore-parse-errors' = 'true' )消息体格式将配置为以下的数据类型:ROW<`user_id` BIGINT, `item_id` BIGINT, `behavior` STRING>消息键和消息体格式以下示例展示了如何配置和使用消息键和消息体格式。 格式配置使用 ‘key’ 或 ‘value’ 加上格式识别符作为前缀。CREATE TABLE KafkaTable ( `ts` TIMESTAMP(3) METADATA FROM 'timestamp', `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING ) WITH ( 'connector' = 'kafka', ... 'key.format' = 'json', 'key.json.ignore-parse-errors' = 'true', 'key.fields' = 'user_id;item_id', 'value.format' = 'json', 'value.json.fail-on-missing-field' = 'false', 'value.fields-include' = 'ALL' )消息键格式包含了在 ‘key.fields’ 中列出的字段(使用 ‘;’ 分隔)和字段顺序。 因此将配置为以下的数据类型:ROW<`user_id` BIGINT, `item_id` BIGINT>由于消息体格式配置为 ‘value.fields-include’ = ‘ALL’,所以消息键字段也会出现在消息体格式的数据类型中:ROW<`user_id` BIGINT, `item_id` BIGINT, `behavior` STRING>重名的格式字段如果消息键字段和消息体字段重名,连接器无法根据表结构信息将这些列区分开。 ‘key.fields-prefix’ 配置项可以在表结构中为消息键字段指定一个唯一名称,并在配置消息键格式的时候保留原名。以下示例展示了在消息键和消息体中同时包含 version 字段的情况:CREATE TABLE KafkaTable ( `k_version` INT, `k_user_id` BIGINT, `k_item_id` BIGINT, `version` INT, `behavior` STRING ) WITH ( 'connector' = 'kafka', ... 'key.format' = 'json', 'key.fields-prefix' = 'k_', 'key.fields' = 'k_version;k_user_id;k_item_id', 'value.format' = 'json', 'value.fields-include' = 'EXCEPT_KEY' )消息体格式必须配置为 ‘EXCEPT_KEY’ 模式。格式将被配置为以下的数据类型:消息键格式: ROW<`version` INT, `user_id` BIGINT, `item_id` BIGINT> 消息体格式: ROW<`version` INT, `behavior` STRING> 七、Topic 和 Partition 的探测topic 和 topic-pattern 配置项决定了 source 消费的 topic 或 topic 的匹配规则。topic 配置项可接受使用分号间隔的 topic 列表,例如 topic-1;topic-2。 topic-pattern 配置项使用正则表达式来探测匹配的 topic。例如 topic-pattern 设置为 test-topic-[0-9],则在作业启动时,所有匹配该正则表达式的 topic(以 test-topic- 开头,以一位数字结尾)都将被 consumer 订阅。为允许 consumer 在作业启动之后探测到动态创建的 topic,请将 scan.topic-partition-discovery.interval 配置为一个非负值。这将使 consumer 能够探测匹配名称规则的 topic 中新的 partition。注意 topic 列表和 topic 匹配规则只适用于 source。对于 sink 端,Flink 目前只支持单一 topic。八、起始消费位点scan.startup.mode 配置项决定了 Kafka consumer 的启动模式。有效值为:group-offsets:从 Zookeeper/Kafka 中某个指定的消费组已提交的偏移量开始。earliest-offset:从可能的最早偏移量开始。latest-offset:从最末尾偏移量开始。timestamp:从用户为每个 partition 指定的时间戳开始。specific-offsets:从用户为每个 partition 指定的偏移量开始。默认值 group-offsets 表示从 Zookeeper/Kafka 中最近一次已提交的偏移量开始消费。如果使用了 timestamp,必须使用另外一个配置项 scan.startup.timestamp-millis 来指定一个从格林尼治标准时间 1970 年 1 月 1 日 00:00:00.000 开始计算的毫秒单位时间戳作为起始时间。如果使用了 specific-offsets,必须使用另外一个配置项 scan.startup.specific-offsets 来为每个 partition 指定起始偏移量, 例如,选项值 partition:0,offset:42;partition:1,offset:300 表示 partition 0 从偏移量 42 开始,partition 1 从偏移量 300 开始。九、有界结束位置配置选项 scan.bounded.mode 指定 Kafka 消费者的有界模式。有效的枚举是:group-offsets:以特定消费者组的 ZooKeeper / Kafka 代理中提交的偏移量为界。这是在给定分区的消费开始时进行评估的。latest-offset:以最新偏移量为界。这是在给定分区的消费开始时进行评估的。timestamp:以用户提供的时间戳为界。specific-offsets:以用户为每个分区提供的特定偏移量为界。如果未设置配置选项值 scan.bounded.mode ,则默认为无界表。如果指定了时间戳,则需要另一个配置选项 scan.bounded.timestamp-millis 来指定自 1970 年 1 月 1 日 00:00:00.000 GMT 以来的特定有界时间戳(以毫秒为单位)。如果指定了 Specific-offsets,则需要另一个配置选项 scan.bounded.specific-offsets 来为每个分区指定特定的有界偏移量,例如选项值partition:0,offset:42;partition:1,offset:300表示分区0的偏移量42和分区1的偏移量300。如果未提供分区的偏移量,则不会从该分区消耗数据。十、CDC 变更日志(Changelog) SourceFlink 原生支持使用 Kafka 作为 CDC 变更日志(changelog) source。如果 Kafka topic 中的消息是通过变更数据捕获(CDC)工具从其他数据库捕获的变更事件,则你可以使用 CDC 格式将消息解析为 Flink SQL 系统中的插入(INSERT)、更新(UPDATE)、删除(DELETE)消息。在许多情况下,变更日志(changelog) source 都是非常有用的功能,例如将数据库中的增量数据同步到其他系统,审核日志,数据库的物化视图,时态表关联数据库表的更改历史等。Flink 提供了几种 CDC 格式:debeziumcanalmaxwell十一、Sink 分区配置项 sink.partitioner 指定了从 Flink 分区到 Kafka 分区的映射关系。 默认情况下,Flink 使用 Kafka 默认分区器 来对消息分区。默认分区器对没有消息键的消息使用 粘性分区策略(sticky partition strategy) 进行分区,对含有消息键的消息使用 murmur2 哈希算法计算分区。为了控制数据行到分区的路由,也可以提供一个自定义的 sink 分区器。‘fixed’ 分区器会将同一个 Flink 分区中的消息写入同一个 Kafka 分区,从而减少网络连接的开销。十二、一致性保证默认情况下,如果查询在 启用 checkpoint 模式下执行时,Kafka sink 按照至少一次(at-lease-once)语义保证将数据写入到 Kafka topic 中。当 Flink checkpoint 启用时,kafka 连接器可以提供精确一次(exactly-once)的语义保证。除了启用 Flink checkpoint,还可以通过传入对应的 sink.semantic 选项来选择三种不同的运行模式:none:Flink 不保证任何语义。已经写出的记录可能会丢失或重复。at-least-once (默认设置):保证没有记录会丢失(但可能会重复)。exactly-once:使用 Kafka 事务提供精确一次(exactly-once)语义。当使用事务向 Kafka 写入数据时,请将所有从 Kafka 中消费记录的应用中的 isolation.level 配置项设置成实际所需的值(read_committed 或 read_uncommitted,后者为默认值)。十三、Source 按分区 WatermarkFlink 对于 Kafka 支持发送按分区的 watermark。Watermark 在 Kafka consumer 中生成。按分区 watermark 的合并方式和在流 shuffle 时合并 Watermark 的方式一致。 Source 输出的 watermark 由读取的分区中最小的 watermark 决定。如果 topic 中的某些分区闲置,watermark 生成器将不会向前推进。 你可以在表配置中设置 ‘table.exec.source.idle-timeout’ 选项来避免上述问题。十四、安全要启用加密和认证相关的安全配置,只需将安全配置加上 “properties.” 前缀配置在 Kafka 表上即可。下面的代码片段展示了如何配置 Kafka 表以使用 PLAIN 作为 SASL 机制并提供 JAAS 配置:CREATE TABLE KafkaTable ( `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING, `ts` TIMESTAMP(3) METADATA FROM 'timestamp' ) WITH ( 'connector' = 'kafka', ... 'properties.security.protocol' = 'SASL_PLAINTEXT', 'properties.sasl.mechanism' = 'PLAIN', 'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.plain.PlainLoginModule required username=\"username\" password=\"password\";' )另一个更复杂的例子,使用 SASL_SSL 作为安全协议并使用 SCRAM-SHA-256 作为 SASL 机制:CREATE TABLE KafkaTable ( `user_id` BIGINT, `item_id` BIGINT, `behavior` STRING, `ts` TIMESTAMP(3) METADATA FROM 'timestamp' ) WITH ( 'connector' = 'kafka', ... 'properties.security.protocol' = 'SASL_SSL', /* SSL 配置 */ /* 配置服务端提供的 truststore (CA 证书) 的路径 */ 'properties.ssl.truststore.location' = '/path/to/kafka.client.truststore.jks', 'properties.ssl.truststore.password' = 'test1234', /* 如果要求客户端认证,则需要配置 keystore (私钥) 的路径 */ 'properties.ssl.keystore.location' = '/path/to/kafka.client.keystore.jks', 'properties.ssl.keystore.password' = 'test1234', /* SASL 配置 */ /* 将 SASL 机制配置为 as SCRAM-SHA-256 */ 'properties.sasl.mechanism' = 'SCRAM-SHA-256', /* 配置 JAAS */ 'properties.sasl.jaas.config' = 'org.apache.kafka.common.security.scram.ScramLoginModule required username=\"username\" password=\"password\";' )如果在作业 JAR 中 Kafka 客户端依赖的类路径被重置了(relocate class),登录模块(login module)的类路径可能会不同,因此请根据登录模块在 JAR 中实际的类路径来改写以上配置。例如在 SQL client JAR 中,Kafka client 依赖被重置在了 org.apache.flink.kafka.shaded.org.apache.kafka 路径下, 因此 plain 登录模块的类路径应写为 org.apache.flink.kafka.shaded.org.apache.kafka.common.security.plain.PlainLoginModule。十五、数据类型映射Kafka 将消息键值以二进制进行存储,因此 Kafka 并不存在 schema 或数据类型。Kafka 消息使用格式配置进行序列化和反序列化,例如 csv,json,avro。 因此,数据类型映射取决于使用的格式。
0
0
0
浏览量1483
打酱油的后端

Flink系列之:窗口聚合

一、窗口表值函数(TVF)聚合适用于流批窗口聚合在 GROUP BY 子句中定义,包含应用窗口 TVF 的关系的“window_start”和“window_end”列。就像使用常规 GROUP BY 子句的查询一样,使用按窗口聚合进行分组的查询将计算每个组的单个结果行。SELECT ... FROM <windowed_table> -- relation applied windowing TVF GROUP BY window_start, window_end, ...与连续表上的其他聚合不同,窗口聚合不会发出中间结果,而只会发出最终结果,即窗口末尾的总聚合。此外,窗口聚合会在不再需要时清除所有中间状态。二、窗口表值函数TVFFlink 支持 TUMBLE、HOP 和 CUMULATE 类型的窗口聚合。在流模式下,窗口表值函数的时间属性字段必须位于事件或处理时间属性上。在批处理模式下,窗口表值函数的时间属性字段必须是 TIMESTAMP 或 TIMESTAMP_LTZ 类型的属性。以下是 TUMBLE、HOP 和 CUMULATE 窗口聚合的一些示例。-- 表必须具有时间属性,例如该表中的“bidtime” Flink SQL> desc Bid; +-------------+------------------------+------+-----+--------+---------------------------------+ | name | type | null | key | extras | watermark | +-------------+------------------------+------+-----+--------+---------------------------------+ | bidtime | TIMESTAMP(3) *ROWTIME* | true | | | `bidtime` - INTERVAL '1' SECOND | | price | DECIMAL(10, 2) | true | | | | | item | STRING | true | | | | | supplier_id | STRING | true | | | | +-------------+------------------------+------+-----+--------+---------------------------------+ Flink SQL> SELECT * FROM Bid; +------------------+-------+------+-------------+ | bidtime | price | item | supplier_id | +------------------+-------+------+-------------+ | 2020-04-15 08:05 | 4.00 | C | supplier1 | | 2020-04-15 08:07 | 2.00 | A | supplier1 | | 2020-04-15 08:09 | 5.00 | D | supplier2 | | 2020-04-15 08:11 | 3.00 | B | supplier2 | | 2020-04-15 08:13 | 1.00 | E | supplier1 | | 2020-04-15 08:17 | 6.00 | F | supplier2 | +------------------+-------+------+-------------+ -- 翻转窗口聚合 Flink SQL> SELECT window_start, window_end, SUM(price) FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | +------------------+------------------+-------+ -- 跳跃窗口聚合 Flink SQL> SELECT window_start, window_end, SUM(price) FROM TABLE( HOP(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '5' MINUTES, INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:05 | 2020-04-15 08:15 | 15.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | | 2020-04-15 08:15 | 2020-04-15 08:25 | 6.00 | +------------------+------------------+-------+ -- 累积窗口聚合 Flink SQL> SELECT window_start, window_end, SUM(price) FROM TABLE( CUMULATE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '2' MINUTES, INTERVAL '10' MINUTES)) GROUP BY window_start, window_end; +------------------+------------------+-------+ | window_start | window_end | price | +------------------+------------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:06 | 4.00 | | 2020-04-15 08:00 | 2020-04-15 08:08 | 6.00 | | 2020-04-15 08:00 | 2020-04-15 08:10 | 11.00 | | 2020-04-15 08:10 | 2020-04-15 08:12 | 3.00 | | 2020-04-15 08:10 | 2020-04-15 08:14 | 4.00 | | 2020-04-15 08:10 | 2020-04-15 08:16 | 4.00 | | 2020-04-15 08:10 | 2020-04-15 08:18 | 10.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | 10.00 | +------------------+------------------+-------+注意:为了更好地理解窗口的行为,我们简化了时间戳值的显示,不显示尾随零,例如如果类型为 TIMESTAMP(3),2020-04-15 08:05 在 Flink SQL Client 中应显示为 2020-04-15 08:05:00.000。三、分组集窗口聚合还支持 GROUPING SETS 语法。分组集允许比标准 GROUP BY 描述的分组操作更复杂的分组操作。行按每个指定的分组集单独分组,并为每个组计算聚合,就像简单的 GROUP BY 子句一样。使用 GROUPING SETS 的窗口聚合要求 window_start 和 window_end 列必须位于 GROUP BY 子句中,但不能位于 GROUPING SETS 子句中。Flink SQL> SELECT window_start, window_end, supplier_id, SUM(price) as price FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end, GROUPING SETS ((supplier_id), ()); +------------------+------------------+-------------+-------+ | window_start | window_end | supplier_id | price | +------------------+------------------+-------------+-------+ | 2020-04-15 08:00 | 2020-04-15 08:10 | (NULL) | 11.00 | | 2020-04-15 08:00 | 2020-04-15 08:10 | supplier2 | 5.00 | | 2020-04-15 08:00 | 2020-04-15 08:10 | supplier1 | 6.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | (NULL) | 10.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | supplier2 | 9.00 | | 2020-04-15 08:10 | 2020-04-15 08:20 | supplier1 | 1.00 | +------------------+------------------+-------------+-------+GROUPING SETS 的每个子列表可以指定零个或多个列或表达式,并以与直接在 GROUP BY 子句中使用相同的方式进行解释。空分组集意味着所有行都聚合到一个组,即使不存在输入行也会输出该组。对于未出现这些列的分组集,对分组列或表达式的引用将替换为结果行中的空值。四、ROLLUPROLLUP 是用于指定常见类型的分组集的简写符号。它表示给定的表达式列表和列表的所有前缀,包括空列表。使用 ROLLUP 进行窗口聚合要求 window_start 和 window_end 列必须位于 GROUP BY 子句中,但不能位于 ROLLUP 子句中。例如,以下查询与上面的查询等效。SELECT window_start, window_end, supplier_id, SUM(price) as price FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end, ROLLUP (supplier_id);五、CUBECUBE 是用于指定常见类型的分组集的简写符号。它表示给定的列表及其所有可能的子集 - 幂集。使用 CUBE 的窗口聚合要求 window_start 和 window_end 列必须位于 GROUP BY 子句中,但不能位于 CUBE 子句中。例如,以下两个查询是等效的。SELECT window_start, window_end, item, supplier_id, SUM(price) as price FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end, CUBE (supplier_id, item); SELECT window_start, window_end, item, supplier_id, SUM(price) as price FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end, GROUPING SETS ( (supplier_id, item), (supplier_id ), ( item), ( ) )六、选择组窗口开始和结束时间戳分组窗口的开始和结束时间戳可以通过 window_start 和 window_end 来选定.七、多级窗口聚合window_start 和 window_end 列是普通的时间戳字段,并不是时间属性。因此它们不能在后续的操作中当做时间属性进行基于时间的操作。 为了传递时间属性,需要在 GROUP BY 子句中添加 window_time 列。window_time 是 Windowing TVFs 产生的三列之一,它是窗口的时间属性。 window_time 添加到 GROUP BY 子句后就能被选定了。下面的查询可以把它用于后续基于时间的操作,比如:多级窗口聚合 和 Window TopN。下面展示了一个多级窗口聚合:第一个窗口聚合后把时间属性传递给第二个窗口聚合。-- 每个supplier_id翻滚5分钟 CREATE VIEW window1 AS -- 注意:内部Window TVF 的窗口开始和窗口结束字段在select 子句中是可选的。但是,如果它们出现在子句中,则需要为它们起别名,以防止名称与外部窗口 TVF 的窗口开始和窗口结束发生冲突。 SELECT window_start as window_5mintumble_start, window_end as window_5mintumble_end, window_time as rowtime, SUM(price) as partial_price FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '5' MINUTES)) GROUP BY supplier_id, window_start, window_end, window_time; -- 在第一个窗口翻滚 10 分钟 SELECT window_start, window_end, SUM(partial_price) as total_price FROM TABLE( TUMBLE(TABLE window1, DESCRIPTOR(rowtime), INTERVAL '10' MINUTES)) GROUP BY window_start, window_end;八、分组窗口聚合警告:分组窗口聚合已经过时。推荐使用更加强大和有效的窗口表值函数聚合。“窗口表值函数聚合"相对于"分组窗口聚合"有如下优点:包含 性能调优 中提到的所有性能优化。支持标准的 GROUPING SETS 语法。可以在窗口聚合结果上使用 窗口 TopN。等等。分组窗口聚合定义在 SQL 的 GROUP BY 子句中。和普通的 GROUP BY 子句一样,包含分组窗口函数的 GROUP BY 子句的查询会对各组分别计算,各产生一个结果行。批处理表和流表上的SQL支持以下分组窗口函数:分组窗口函数Group Window FunctionDescriptionTUMBLE(time_attr, interval)定义一个滚动时间窗口。它把数据分配到连续且不重叠的固定时间区间(interval),例如:一个5分钟的滚动窗口以5分钟为间隔对数据进行分组。滚动窗口可以被定义在事件时间(流 + 批)或者处理时间(流)上。HOP(time_attr, interval, interval)定义一个滑动时间窗口,它有窗口大小(第二个 interval 参数)和滑动间隔(第一个 interval 参数)两个参数。如果滑动间隔小于窗口大小,窗口会产生重叠。所以,数据可以被指定到多个窗口。例如:一个15分钟大小和5分钟滑动间隔的滑动窗口将每一行分配给3个15分钟大小的不同窗口,这些窗口以5分钟的间隔计算。滑动窗口可以被定义在事件时间(流 + 批)或者处理时间(流)上。SESSION(time_attr, interval)定义一个会话时间窗口。会话时间窗口没有固定的时间区间,但其边界是通过不活动的时间 interval 定义的,即:一个会话窗口会在指定的时长内没有事件出现时关闭。例如:一个30分钟间隔的会话窗口收到一条数据时,如果之前已经30分钟不活动了(否则,这条数据会被分配到已经存在的窗口中),它会开启一个新窗口,如果30分钟之内没有新数据到来,就会关闭。会话窗口可以被定义在事件时间(流 + 批) 或者处理时间(流)上。九、时间属性在流处理模式,分组窗口函数的 time_attr 属性必须是一个有效的处理或事件时间。在批处理模式,分组窗口函数的 time_attr 参数必须是一个 TIMESTAMP 类型的属性。十、选取分组窗口开始和结束时间戳分组窗口的开始和结束时间戳以及时间属性也可以通过下列辅助函数的方式获取到:辅助函数描述TUMBLE_START(time_attr, interval)、HOP_START(time_attr, interval, interval)、SESSION_START(time_attr, interval)返回相应的滚动,滑动或会话窗口的下限的时间戳(inclusive),即窗口开始时间。TUMBLE_END(time_attr, interval)、HOP_END(time_attr, interval, interval)、SESSION_END(time_attr, interval)返回相应滚动窗口,跳跃窗口或会话窗口的上限的时间戳(exclusive),即窗口结束时间。注意: 上限时间戳(exlusive)不能作为 rowtime attribute 用于后续基于时间的操作,例如:interval joins 和 group window 或 over window aggregations。TUMBLE_ROWTIME(time_attr, interval)、HOP_ROWTIME(time_attr, interval, interval)、SESSION_ROWTIME(time_attr, interval)返回相应滚动窗口,跳跃窗口或会话窗口的上限的时间戳(inclusive),即窗口事件时间,或窗口处理时间。返回的值是 rowtime attribute,可以用于后续基于时间的操作,比如:interval joins 和 group window 或 over window aggregations。TUMBLE_PROCTIME(time_attr, interval)、HOP_PROCTIME(time_attr, interval, interval)、SESSION_PROCTIME(time_attr, interval)返回的值是 proctime attribute,可以用于后续基于时间的操作,比如: interval joins 和 group window 或 over window aggregations。注意: 辅助函数的参数必须和 GROUP BY 子句中的分组窗口函数一致。下面的例子展示了在流式表上如何使用分组窗口 SQL 查询:CREATE TABLE Orders ( user BIGINT, product STRING, amount INT, order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '1' MINUTE ) WITH (...); SELECT user, TUMBLE_START(order_time, INTERVAL '1' DAY) AS wStart, SUM(amount) FROM Orders GROUP BY TUMBLE(order_time, INTERVAL '1' DAY), user
0
0
0
浏览量1948
打酱油的后端

Flink系列之:自定义函数

一、自定义函数自定义函数(UDF)是一种扩展开发机制,可以用来在查询语句里调用难以用其他方式表达的频繁使用或自定义的逻辑。自定义函数可以用 JVM 语言(例如 Java 或 Scala)或 Python 实现,实现者可以在 UDF 中使用任意第三方库,本文聚焦于使用 JVM 语言开发自定义函数。二、概述当前 Flink 有如下几种函数:标量函数 将标量值转换成一个新标量值;表值函数 将标量值转换成新的行数据;聚合函数 将多行数据里的标量值转换成一个新标量值;表值聚合函数 将多行数据里的标量值转换成新的行数据;异步表值函数 是异步查询外部数据系统的特殊函数。注意:标量和表值函数已经使用了新的基于数据类型的类型系统,聚合函数仍然使用基于 TypeInformation 的旧类型系统。以下示例展示了如何创建一个基本的标量函数,以及如何在 Table API 和 SQL 里调用这个函数。函数用于 SQL 查询前要先经过注册;而在用于 Table API 时,函数可以先注册后调用,也可以 内联 后直接使用。Java版本:import org.apache.flink.table.api.*; import org.apache.flink.table.functions.ScalarFunction; import static org.apache.flink.table.api.Expressions.*; // 定义函数逻辑 public static class SubstringFunction extends ScalarFunction { public String eval(String s, Integer begin, Integer end) { return s.substring(begin, end); } } TableEnvironment env = TableEnvironment.create(...); // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(SubstringFunction.class, $("myField"), 5, 12)); // 注册函数 env.createTemporarySystemFunction("SubstringFunction", SubstringFunction.class); // 在 Table API 里调用注册好的函数 env.from("MyTable").select(call("SubstringFunction", $("myField"), 5, 12)); // 在 SQL 里调用注册好的函数 env.sqlQuery("SELECT SubstringFunction(myField, 5, 12) FROM MyTable");Scala版本:import org.apache.flink.table.api._ import org.apache.flink.table.functions.ScalarFunction // define function logic class SubstringFunction extends ScalarFunction { def eval(s: String, begin: Integer, end: Integer): String = { s.substring(begin, end) } } val env = TableEnvironment.create(...) // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(classOf[SubstringFunction], $"myField", 5, 12)) // 注册函数 env.createTemporarySystemFunction("SubstringFunction", classOf[SubstringFunction]) // 在 Table API 里调用注册好的函数 env.from("MyTable").select(call("SubstringFunction", $"myField", 5, 12)) // 在 SQL 里调用注册好的函数 env.sqlQuery("SELECT SubstringFunction(myField, 5, 12) FROM MyTable")对于交互式会话,还可以在使用或注册函数之前对其进行参数化,这样可以把函数 实例 而不是函数 类 用作临时函数。为确保函数实例可应用于集群环境,参数必须是可序列化的。Java版本:import org.apache.flink.table.api.*; import org.apache.flink.table.functions.ScalarFunction; import static org.apache.flink.table.api.Expressions.*; // 定义可参数化的函数逻辑 public static class SubstringFunction extends ScalarFunction { private boolean endInclusive; public SubstringFunction(boolean endInclusive) { this.endInclusive = endInclusive; } public String eval(String s, Integer begin, Integer end) { return s.substring(begin, endInclusive ? end + 1 : end); } } TableEnvironment env = TableEnvironment.create(...); // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(new SubstringFunction(true), $("myField"), 5, 12)); // 注册函数 env.createTemporarySystemFunction("SubstringFunction", new SubstringFunction(true));Scala版本:import org.apache.flink.table.api._ import org.apache.flink.table.functions.ScalarFunction // 定义可参数化的函数逻辑 class SubstringFunction(val endInclusive) extends ScalarFunction { def eval(s: String, begin: Integer, end: Integer): String = { s.substring(endInclusive ? end + 1 : end) } } val env = TableEnvironment.create(...) // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(new SubstringFunction(true), $"myField", 5, 12)) // 注册函数 env.createTemporarySystemFunction("SubstringFunction", new SubstringFunction(true))你可以在 Table API 中使用 * 表达式作为函数的一个参数,它将被扩展为该表所有的列作为函数对应位置的参数。Java版本:import org.apache.flink.table.api.*; import org.apache.flink.table.functions.ScalarFunction; import static org.apache.flink.table.api.Expressions.*; public static class MyConcatFunction extends ScalarFunction { public String eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object... fields) { return Arrays.stream(fields) .map(Object::toString) .collect(Collectors.joining(",")); } } TableEnvironment env = TableEnvironment.create(...); // 使用 $("*") 作为函数的参数,如果 MyTable 有 3 列 (a, b, c), // 它们都将会被传给 MyConcatFunction。 env.from("MyTable").select(call(MyConcatFunction.class, $("*"))); // 它等价于显式地将所有列传给 MyConcatFunction。 env.from("MyTable").select(call(MyConcatFunction.class, $("a"), $("b"), $("c")));Scala版本:import org.apache.flink.table.api._ import org.apache.flink.table.functions.ScalarFunction import scala.annotation.varargs class MyConcatFunction extends ScalarFunction { @varargs def eval(@DataTypeHint(inputGroup = InputGroup.ANY) row: AnyRef*): String = { row.map(f => f.toString).mkString(",") } } val env = TableEnvironment.create(...) // 使用 $"*" 作为函数的参数,如果 MyTable 有 3 个列 (a, b, c), // 它们都将会被传给 MyConcatFunction。 env.from("MyTable").select(call(classOf[MyConcatFunction], $"*")); // 它等价于显式地将所有列传给 MyConcatFunction。 env.from("MyTable").select(call(classOf[MyConcatFunction], $"a", $"b", $"c"));三、开发指南注意在聚合函数使用新的类型系统前,本节仅适用于标量和表值函数。所有的自定义函数都遵循一些基本的实现原则。四、函数类实现类必须继承自合适的基类之一(例如 org.apache.flink.table.functions.ScalarFunction )。该类必须声明为 public ,而不是 abstract ,并且可以被全局访问。不允许使用非静态内部类或匿名类。为了将自定义函数存储在持久化的 catalog 中,该类必须具有默认构造器,且在运行时可实例化。Anonymous functions in Table API can only be persisted if the function is not stateful (i.e. containing only transient and static fields).五、求值方法基类提供了一组可以被重写的方法,例如 open()、 close() 或 isDeterministic() 。但是,除了上述方法之外,作用于每条传入记录的主要逻辑还必须通过专门的 求值方法 来实现。根据函数的种类,后台生成的运算符会在运行时调用诸如 eval()、accumulate() 或 retract() 之类的求值方法。这些方法必须声明为 public ,并带有一组定义明确的参数。常规的 JVM 方法调用语义是适用的。因此可以:实现重载的方法,例如 eval(Integer) 和 eval(LocalDateTime);使用变长参数,例如 eval(Integer…);使用对象继承,例如 eval(Object) 可接受 LocalDateTime 和 Integer 作为参数;也可组合使用,例如 eval(Object…) 可接受所有类型的参数。以下代码片段展示了一个重载函数的示例:import org.apache.flink.table.functions.ScalarFunction; // 有多个重载求值方法的函数 public static class SumFunction extends ScalarFunction { public Integer eval(Integer a, Integer b) { return a + b; } public Integer eval(String a, String b) { return Integer.valueOf(a) + Integer.valueOf(b); } public Integer eval(Double... d) { double result = 0; for (double value : d) result += value; return (int) result; } }Scala代码:import org.apache.flink.table.functions.ScalarFunction import scala.annotation.varargs // 有多个重载求值方法的函数 class SumFunction extends ScalarFunction { def eval(a: Integer, b: Integer): Integer = { a + b } def eval(a: String, b: String): Integer = { Integer.valueOf(a) + Integer.valueOf(b) } @varargs // generate var-args like Java def eval(d: Double*): Integer = { d.sum.toInt } }六、类型推导Table(类似于 SQL 标准)是一种强类型的 API。因此,函数的参数和返回类型都必须映射到数据类型。从逻辑角度看,Planner 需要知道数据类型、精度和小数位数;从 JVM 角度来看,Planner 在调用自定义函数时需要知道如何将内部数据结构表示为 JVM 对象。术语 类型推导 概括了意在验证输入值、派生出参数/返回值数据类型的逻辑。Flink 自定义函数实现了自动的类型推导提取,通过反射从函数的类及其求值方法中派生数据类型。如果这种隐式的反射提取方法不成功,则可以通过使用 @DataTypeHint 和 @FunctionHint 注解相关参数、类或方法来支持提取过程,下面展示了有关如何注解函数的例子。如果需要更高级的类型推导逻辑,实现者可以在每个自定义函数中显式重写 getTypeInference() 方法。但是,建议使用注解方式,因为它可使自定义类型推导逻辑保持在受影响位置附近,而在其他位置则保持默认状态。七、自动类型推导自动类型推导会检查函数的类和求值方法,派生出函数参数和结果的数据类型, @DataTypeHint 和 @FunctionHint 注解支持自动类型推导。@DataTypeHint在许多情况下,需要支持以 内联 方式自动提取出函数参数、返回值的类型。以下例子展示了如何使用 @DataTypeHint。Java代码:import org.apache.flink.table.annotation.DataTypeHint; import org.apache.flink.table.annotation.InputGroup; import org.apache.flink.table.functions.ScalarFunction; import org.apache.flink.types.Row; // 有多个重载求值方法的函数 public static class OverloadedFunction extends ScalarFunction { // no hint required public Long eval(long a, long b) { return a + b; } // 定义 decimal 的精度和小数位 public @DataTypeHint("DECIMAL(12, 3)") BigDecimal eval(double a, double b) { return BigDecimal.valueOf(a + b); } // 定义嵌套数据类型 @DataTypeHint("ROW<s STRING, t TIMESTAMP_LTZ(3)>") public Row eval(int i) { return Row.of(String.valueOf(i), Instant.ofEpochSecond(i)); } // 允许任意类型的符入,并输出序列化定制后的值 @DataTypeHint(value = "RAW", bridgedTo = ByteBuffer.class) public ByteBuffer eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) { return MyUtils.serializeToByteBuffer(o); } }Scala代码:import org.apache.flink.table.annotation.DataTypeHint import org.apache.flink.table.annotation.InputGroup import org.apache.flink.table.functions.ScalarFunction import org.apache.flink.types.Row import scala.annotation.varargs // function with overloaded evaluation methods class OverloadedFunction extends ScalarFunction { // no hint required def eval(a: Long, b: Long): Long = { a + b } // 定义 decimal 的精度和小数位 @DataTypeHint("DECIMAL(12, 3)") def eval(double a, double b): BigDecimal = { java.lang.BigDecimal.valueOf(a + b) } // 定义嵌套数据类型 @DataTypeHint("ROW<s STRING, t TIMESTAMP_LTZ(3)>") def eval(Int i): Row = { Row.of(java.lang.String.valueOf(i), java.time.Instant.ofEpochSecond(i)) } // 允许任意类型的符入,并输出定制序列化后的值 @DataTypeHint(value = "RAW", bridgedTo = classOf[java.nio.ByteBuffer]) def eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o): java.nio.ByteBuffer = { MyUtils.serializeToByteBuffer(o) } }@FunctionHint有时我们希望一种求值方法可以同时处理多种数据类型,有时又要求对重载的多个求值方法仅声明一次通用的结果类型。@FunctionHint 注解可以提供从入参数据类型到结果数据类型的映射,它可以在整个函数类或求值方法上注解输入、累加器和结果的数据类型。可以在类顶部声明一个或多个注解,也可以为类的所有求值方法分别声明一个或多个注解。所有的 hint 参数都是可选的,如果未定义参数,则使用默认的基于反射的类型提取。在函数类顶部定义的 hint 参数被所有求值方法继承。以下例子展示了如何使用 @FunctionHint。Java代码:import org.apache.flink.table.annotation.DataTypeHint; import org.apache.flink.table.annotation.FunctionHint; import org.apache.flink.table.functions.TableFunction; import org.apache.flink.types.Row; // 为函数类的所有求值方法指定同一个输出类型 @FunctionHint(output = @DataTypeHint("ROW<s STRING, i INT>")) public static class OverloadedFunction extends TableFunction<Row> { public void eval(int a, int b) { collect(Row.of("Sum", a + b)); } // overloading of arguments is still possible public void eval() { collect(Row.of("Empty args", -1)); } } // 解耦类型推导与求值方法,类型推导完全取决于 FunctionHint @FunctionHint( input = {@DataTypeHint("INT"), @DataTypeHint("INT")}, output = @DataTypeHint("INT") ) @FunctionHint( input = {@DataTypeHint("BIGINT"), @DataTypeHint("BIGINT")}, output = @DataTypeHint("BIGINT") ) @FunctionHint( input = {}, output = @DataTypeHint("BOOLEAN") ) public static class OverloadedFunction extends TableFunction<Object> { // an implementer just needs to make sure that a method exists // that can be called by the JVM public void eval(Object... o) { if (o.length == 0) { collect(false); } collect(o[0]); } }Scala代码:import org.apache.flink.table.annotation.DataTypeHint import org.apache.flink.table.annotation.FunctionHint import org.apache.flink.table.functions.TableFunction import org.apache.flink.types.Row // 为函数类的所有求值方法指定同一个输出类型 @FunctionHint(output = new DataTypeHint("ROW<s STRING, i INT>")) class OverloadedFunction extends TableFunction[Row] { def eval(a: Int, b: Int): Unit = { collect(Row.of("Sum", Int.box(a + b))) } // overloading of arguments is still possible def eval(): Unit = { collect(Row.of("Empty args", Int.box(-1))) } } // 解耦类型推导与求值方法,类型推导完全取决于 @FunctionHint @FunctionHint( input = Array(new DataTypeHint("INT"), new DataTypeHint("INT")), output = new DataTypeHint("INT") ) @FunctionHint( input = Array(new DataTypeHint("BIGINT"), new DataTypeHint("BIGINT")), output = new DataTypeHint("BIGINT") ) @FunctionHint( input = Array(), output = new DataTypeHint("BOOLEAN") ) class OverloadedFunction extends TableFunction[AnyRef] { // an implementer just needs to make sure that a method exists // that can be called by the JVM @varargs def eval(o: AnyRef*) = { if (o.length == 0) { collect(Boolean.box(false)) } collect(o(0)) } }八、定制类型推导在大多数情况下,@DataTypeHint 和 @FunctionHint 足以构建自定义函数,然而通过重写 getTypeInference() 定制自动类型推导逻辑,实现者可以创建任意像系统内置函数那样有用的函数。以下用 Java 实现的例子展示了定制类型推导的潜力,它根据字符串参数来确定函数的结果类型。该函数带有两个字符串参数:第一个参数表示要分析的字符串,第二个参数表示目标类型。Java代码:import org.apache.flink.table.api.DataTypes; import org.apache.flink.table.catalog.DataTypeFactory; import org.apache.flink.table.functions.ScalarFunction; import org.apache.flink.table.types.inference.TypeInference; import org.apache.flink.types.Row; public static class LiteralFunction extends ScalarFunction { public Object eval(String s, String type) { switch (type) { case "INT": return Integer.valueOf(s); case "DOUBLE": return Double.valueOf(s); case "STRING": default: return s; } } // 禁用自动的反射式类型推导,使用如下逻辑进行类型推导 @Override public TypeInference getTypeInference(DataTypeFactory typeFactory) { return TypeInference.newBuilder() // 指定输入参数的类型,必要时参数会被隐式转换 .typedArguments(DataTypes.STRING(), DataTypes.STRING()) // specify a strategy for the result data type of the function .outputTypeStrategy(callContext -> { if (!callContext.isArgumentLiteral(1) || callContext.isArgumentNull(1)) { throw callContext.newValidationError("Literal expected for second argument."); } // 基于字符串值返回数据类型 final String literal = callContext.getArgumentValue(1, String.class).orElse("STRING"); switch (literal) { case "INT": return Optional.of(DataTypes.INT().notNull()); case "DOUBLE": return Optional.of(DataTypes.DOUBLE().notNull()); case "STRING": default: return Optional.of(DataTypes.STRING()); } }) .build(); } }九、确定性每个用户自定义函数类都可以通过重写 isDeterministic() 方法来声明它是否产生确定性的结果。如果该函数不是纯粹函数式的(如random(), date(), 或now()),该方法必须返回 false。默认情况下,isDeterministic() 返回 true。此外,重写 isDeterministic() 方法也可能影响运行时行为。运行时实现可能会在两个不同的阶段被调用:在生成执行计划期间:如果一个函数是通过常量表达式调用的或者常量表达式可以从给定的语句中推导出来,那么一个函数就会被预计算以减少常量表达式,并且可能不再在集群上执行。 除非 isDeterministic() 被重写为 false 用来在这种情况下禁用常量表达式简化。比如说,以下对 ABS 的调用在生成执行计划期间被执行:SELECT ABS(-1) FROM t 和 SELECT ABS(field) FROM t WHERE field = -1,而 SELECT ABS(field) FROM t 则不执行。在运行时(即在集群执行):如果一个函数被调用时带有非常量表达式或 isDeterministic() 返回 false。十、内置函数的确定性系统(内置)函数的确定性是不可改变的。存在两种不具有确定性的函数:动态函数和非确定性函数,根据 Apache Calcite SqlOperator 的定义:/** * Returns whether a call to this operator is guaranteed to always return * the same result given the same operands; true is assumed by default. */ public boolean isDeterministic() { return true; } /** * Returns whether it is unsafe to cache query plans referencing this * operator; false is assumed by default. */ public boolean isDynamicFunction() { return false; }isDeterministic 表示函数的确定性,声明返回 false 时将在运行时对每个记录进行计算。 isDynamicFunction 声明返回 true 时意味着该函数只能在查询开始时被计算,对于批处理模式,它只在生成执行计划期间被执行, 而对于流模式,它等效于一个非确定性的函数,这是因为查询在逻辑上是连续执行的(流模式对动态表的连续查询抽象),所以动态函数在每次查询执行时也会被重新计算(当前实现下等效于每条记录计算)。以下内置函数总是非确定性的(批和流模式下,都在运行时对每条记录进行计算)UUIDRANDRAND_INTEGERCURRENT_DATABASEUNIX_TIMESTAMPCURRENT_ROW_TIMESTAMP以下内置时间函数是动态的,批处理模式下,将在生成执行计划期间被执行(查询开始),对于流模式,将在运行时对每条记录进行计算CURRENT_DATECURRENT_TIMECURRENT_TIMESTAMPNOWLOCALTIMELOCALTIMESTAMP注意:isDynamicFunction 仅适用于内置函数十一、运行时集成有时候自定义函数需要获取一些全局信息,或者在真正被调用之前做一些配置(setup)/清理(clean-up)的工作。自定义函数也提供了 open() 和 close() 方法,你可以重写这两个方法做到类似于 DataStream API 中 RichFunction 的功能。open() 方法在求值方法被调用之前先调用。close() 方法在求值方法调用完之后被调用。open() 方法提供了一个 FunctionContext,它包含了一些自定义函数被执行时的上下文信息,比如 metric group、分布式文件缓存,或者是全局的作业参数等。下面的信息可以通过调用 FunctionContext 的对应的方法来获得:方法描述getMetricGroup()执行该函数的 subtask 的 Metric Group。getCachedFile(name)分布式文件缓存的本地临时文件副本。getJobParameter(name, defaultValue)跟对应的 key 关联的全局参数值。下面的例子展示了如何在一个标量函数中通过 FunctionContext 来获取一个全局的任务参数:Java代码:import org.apache.flink.table.api.*; import org.apache.flink.table.functions.FunctionContext; import org.apache.flink.table.functions.ScalarFunction; public static class HashCodeFunction extends ScalarFunction { private int factor = 0; @Override public void open(FunctionContext context) throws Exception { // 获取参数 "hashcode_factor" // 如果不存在,则使用默认值 "12" factor = Integer.parseInt(context.getJobParameter("hashcode_factor", "12")); } public int eval(String s) { return s.hashCode() * factor; } } TableEnvironment env = TableEnvironment.create(...); // 设置任务参数 env.getConfig().addJobParameter("hashcode_factor", "31"); // 注册函数 env.createTemporarySystemFunction("hashCode", HashCodeFunction.class); // 调用函数 env.sqlQuery("SELECT myField, hashCode(myField) FROM MyTable");Scala代码:import org.apache.flink.table.api._ import org.apache.flink.table.functions.FunctionContext import org.apache.flink.table.functions.ScalarFunction class HashCodeFunction extends ScalarFunction { private var factor: Int = 0 override def open(context: FunctionContext): Unit = { // 获取参数 "hashcode_factor" // 如果不存在,则使用默认值 "12" factor = context.getJobParameter("hashcode_factor", "12").toInt } def eval(s: String): Int = { s.hashCode * factor } } val env = TableEnvironment.create(...) // 设置任务参数 env.getConfig.addJobParameter("hashcode_factor", "31") // 注册函数 env.createTemporarySystemFunction("hashCode", classOf[HashCodeFunction]) // 调用函数 env.sqlQuery("SELECT myField, hashCode(myField) FROM MyTable")十二、标量函数自定义标量函数可以把 0 到多个标量值映射成 1 个标量值,数据类型里列出的任何数据类型都可作为求值方法的参数和返回值类型。想要实现自定义标量函数,你需要扩展 org.apache.flink.table.functions 里面的 ScalarFunction 并且实现一个或者多个求值方法。标量函数的行为取决于你写的求值方法。求值方法必须是 public 的,而且名字必须是 eval。下面的例子展示了如何实现一个求哈希值的函数并在查询里调用它Java代码:import org.apache.flink.table.annotation.InputGroup; import org.apache.flink.table.api.*; import org.apache.flink.table.functions.ScalarFunction; import static org.apache.flink.table.api.Expressions.*; public static class HashFunction extends ScalarFunction { // 接受任意类型输入,返回 INT 型输出 public int eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) { return o.hashCode(); } } TableEnvironment env = TableEnvironment.create(...); // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(HashFunction.class, $("myField"))); // 注册函数 env.createTemporarySystemFunction("HashFunction", HashFunction.class); // 在 Table API 里调用注册好的函数 env.from("MyTable").select(call("HashFunction", $("myField"))); // 在 SQL 里调用注册好的函数 env.sqlQuery("SELECT HashFunction(myField) FROM MyTable");Scala代码:import org.apache.flink.table.annotation.InputGroup import org.apache.flink.table.api._ import org.apache.flink.table.functions.ScalarFunction class HashFunction extends ScalarFunction { // 接受任意类型输入,返回 INT 型输出 def eval(@DataTypeHint(inputGroup = InputGroup.ANY) o: AnyRef): Int { return o.hashCode(); } } val env = TableEnvironment.create(...) // 在 Table API 里不经注册直接“内联”调用函数 env.from("MyTable").select(call(classOf[HashFunction], $"myField")) // 注册函数 env.createTemporarySystemFunction("HashFunction", classOf[HashFunction]) // 在 Table API 里调用注册好的函数 env.from("MyTable").select(call("HashFunction", $"myField")) // 在 SQL 里调用注册好的函数 env.sqlQuery("SELECT HashFunction(myField) FROM MyTable")十三、表值函数跟自定义标量函数一样,自定义表值函数的输入参数也可以是 0 到多个标量。但是跟标量函数只能返回一个值不同的是,它可以返回任意多行。返回的每一行可以包含 1 到多列,如果输出行只包含 1 列,会省略结构化信息并生成标量值,这个标量值在运行阶段会隐式地包装进行里。要定义一个表值函数,你需要扩展 org.apache.flink.table.functions 下的 TableFunction,可以通过实现多个名为 eval 的方法对求值方法进行重载。像其他函数一样,输入和输出类型也可以通过反射自动提取出来。表值函数返回的表的类型取决于 TableFunction 类的泛型参数 T,不同于标量函数,表值函数的求值方法本身不包含返回类型,而是通过 collect(T) 方法来发送要输出的行。在 Table API 中,表值函数是通过 .joinLateral(…) 或者 .leftOuterJoinLateral(…) 来使用的。joinLateral 算子会把外表(算子左侧的表)的每一行跟跟表值函数返回的所有行(位于算子右侧)进行 (cross)join。leftOuterJoinLateral 算子也是把外表(算子左侧的表)的每一行跟表值函数返回的所有行(位于算子右侧)进行(cross)join,并且如果表值函数返回 0 行也会保留外表的这一行。在 SQL 里面用 JOIN 或者 以 ON TRUE 为条件的 LEFT JOIN 来配合 LATERAL TABLE() 的使用。下面的例子展示了如何实现一个分隔函数并在查询里调用它:Java代码:import org.apache.flink.table.annotation.DataTypeHint; import org.apache.flink.table.annotation.FunctionHint; import org.apache.flink.table.api.*; import org.apache.flink.table.functions.TableFunction; import org.apache.flink.types.Row; import static org.apache.flink.table.api.Expressions.*; @FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>")) public static class SplitFunction extends TableFunction<Row> { public void eval(String str) { for (String s : str.split(" ")) { // use collect(...) to emit a row collect(Row.of(s, s.length())); } } } TableEnvironment env = TableEnvironment.create(...); // 在 Table API 里不经注册直接“内联”调用函数 env .from("MyTable") .joinLateral(call(SplitFunction.class, $("myField"))) .select($("myField"), $("word"), $("length")); env .from("MyTable") .leftOuterJoinLateral(call(SplitFunction.class, $("myField"))) .select($("myField"), $("word"), $("length")); // 在 Table API 里重命名函数字段 env .from("MyTable") .leftOuterJoinLateral(call(SplitFunction.class, $("myField")).as("newWord", "newLength")) .select($("myField"), $("newWord"), $("newLength")); // 注册函数 env.createTemporarySystemFunction("SplitFunction", SplitFunction.class); // 在 Table API 里调用注册好的函数 env .from("MyTable") .joinLateral(call("SplitFunction", $("myField"))) .select($("myField"), $("word"), $("length")); env .from("MyTable") .leftOuterJoinLateral(call("SplitFunction", $("myField"))) .select($("myField"), $("word"), $("length")); // 在 SQL 里调用注册好的函数 env.sqlQuery( "SELECT myField, word, length " + "FROM MyTable, LATERAL TABLE(SplitFunction(myField))"); env.sqlQuery( "SELECT myField, word, length " + "FROM MyTable " + "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE"); // 在 SQL 里重命名函数字段 env.sqlQuery( "SELECT myField, newWord, newLength " + "FROM MyTable " + "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON TRUE");Scala代码:import org.apache.flink.table.annotation.DataTypeHint import org.apache.flink.table.annotation.FunctionHint import org.apache.flink.table.api._ import org.apache.flink.table.functions.TableFunction import org.apache.flink.types.Row @FunctionHint(output = new DataTypeHint("ROW<word STRING, length INT>")) class SplitFunction extends TableFunction[Row] { def eval(str: String): Unit = { // use collect(...) to emit a row str.split(" ").foreach(s => collect(Row.of(s, Int.box(s.length)))) } } val env = TableEnvironment.create(...) // 在 Table API 里不经注册直接“内联”调用函数 env .from("MyTable") .joinLateral(call(classOf[SplitFunction], $"myField") .select($"myField", $"word", $"length") env .from("MyTable") .leftOuterJoinLateral(call(classOf[SplitFunction], $"myField")) .select($"myField", $"word", $"length") // 在 Table API 里重命名函数字段 env .from("MyTable") .leftOuterJoinLateral(call(classOf[SplitFunction], $"myField").as("newWord", "newLength")) .select($"myField", $"newWord", $"newLength") // 注册函数 env.createTemporarySystemFunction("SplitFunction", classOf[SplitFunction]) // 在 Table API 里调用注册好的函数 env .from("MyTable") .joinLateral(call("SplitFunction", $"myField")) .select($"myField", $"word", $"length") env .from("MyTable") .leftOuterJoinLateral(call("SplitFunction", $"myField")) .select($"myField", $"word", $"length") // 在 SQL 里调用注册好的函数 env.sqlQuery( "SELECT myField, word, length " + "FROM MyTable, LATERAL TABLE(SplitFunction(myField))"); env.sqlQuery( "SELECT myField, word, length " + "FROM MyTable " + "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE") // 在 SQL 里重命名函数字段 env.sqlQuery( "SELECT myField, newWord, newLength " + "FROM MyTable " + "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON TRUE")如果你打算使用 Scala,不要把表值函数声明为 Scala object,Scala object 是单例对象,将导致并发问题。十四、聚合函数自定义聚合函数(UDAGG)是把一个表(一行或者多行,每行可以有一列或者多列)聚合成一个标量值。上面的图片展示了一个聚合的例子。假设你有一个关于饮料的表。表里面有三个字段,分别是 id、name、price,表里有 5 行数据。假设你需要找到所有饮料里最贵的饮料的价格,即执行一个 max() 聚合。你需要遍历所有 5 行数据,而结果就只有一个数值。自定义聚合函数是通过扩展 AggregateFunction 来实现的。AggregateFunction 的工作过程如下。首先,它需要一个 accumulator,它是一个数据结构,存储了聚合的中间结果。通过调用 AggregateFunction 的 createAccumulator() 方法创建一个空的 accumulator。接下来,对于每一行数据,会调用 accumulate() 方法来更新 accumulator。当所有的数据都处理完了之后,通过调用 getValue 方法来计算和返回最终的结果。下面几个方法是每个 AggregateFunction 必须要实现的:createAccumulator()accumulate()getValue()Flink 的类型推导在遇到复杂类型的时候可能会推导出错误的结果,比如那些非基本类型和普通的 POJO 类型的复杂类型。所以跟 ScalarFunction 和 TableFunction 一样,AggregateFunction 也提供了 AggregateFunction#getResultType() 和 AggregateFunction#getAccumulatorType() 来分别指定返回值类型和 accumulator 的类型,两个函数的返回值类型也都是 TypeInformation。除了上面的方法,还有几个方法可以选择实现。这些方法有些可以让查询更加高效,而有些是在某些特定场景下必须要实现的。例如,如果聚合函数用在会话窗口(当两个会话窗口合并的时候需要 merge 他们的 accumulator)的话,merge() 方法就是必须要实现的。AggregateFunction 的以下方法在某些场景下是必须实现的:retract() 在 bounded OVER 窗口中是必须实现的。merge() 在许多批式聚合和会话以及滚动窗口聚合中是必须实现的。除此之外,这个方法对于优化也很多帮助。例如,两阶段聚合优化就需要所有的 AggregateFunction 都实现 merge 方法。resetAccumulator() 在许多批式聚合中是必须实现的。AggregateFunction 的所有方法都必须是 public 的,不能是 static 的,而且名字必须跟上面写的一样。createAccumulator、getValue、getResultType 以及 getAccumulatorType 这几个函数是在抽象类 AggregateFunction 中定义的,而其他函数都是约定的方法。如果要定义一个聚合函数,你需要扩展 org.apache.flink.table.functions.AggregateFunction,并且实现一个(或者多个)accumulate 方法。accumulate 方法可以重载,每个方法的参数类型不同,并且支持变长参数。AggregateFunction 的所有方法的详细文档如下。Java代码:/** * Base class for user-defined aggregates and table aggregates. * * @param <T> the type of the aggregation result. * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. */ public abstract class UserDefinedAggregateFunction<T, ACC> extends UserDefinedFunction { /** * Creates and init the Accumulator for this (table)aggregate function. * * @return the accumulator with the initial value */ public ACC createAccumulator(); // MANDATORY /** * Returns the TypeInformation of the (table)aggregate function's result. * * @return The TypeInformation of the (table)aggregate function's result or null if the result * type should be automatically inferred. */ public TypeInformation<T> getResultType = null; // PRE-DEFINED /** * Returns the TypeInformation of the (table)aggregate function's accumulator. * * @return The TypeInformation of the (table)aggregate function's accumulator or null if the * accumulator type should be automatically inferred. */ public TypeInformation<ACC> getAccumulatorType = null; // PRE-DEFINED } /** * Base class for aggregation functions. * * @param <T> the type of the aggregation result * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. * AggregateFunction represents its state using accumulator, thereby the state of the * AggregateFunction must be put into the accumulator. */ public abstract class AggregateFunction<T, ACC> extends UserDefinedAggregateFunction<T, ACC> { /** Processes the input values and update the provided accumulator instance. The method * accumulate can be overloaded with different custom types and arguments. An AggregateFunction * requires at least one accumulate() method. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ public void accumulate(ACC accumulator, [user defined inputs]); // MANDATORY /** * Retracts the input values from the accumulator instance. The current design assumes the * inputs are the values that have been previously accumulated. The method retract can be * overloaded with different custom types and arguments. This function must be implemented for * datastream bounded over aggregate. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ public void retract(ACC accumulator, [user defined inputs]); // OPTIONAL /** * Merges a group of accumulator instances into one accumulator instance. This function must be * implemented for datastream session window grouping aggregate and bounded grouping aggregate. * * @param accumulator the accumulator which will keep the merged aggregate results. It should * be noted that the accumulator may contain the previous aggregated * results. Therefore user should not replace or clean this instance in the * custom merge method. * @param its an {@link java.lang.Iterable} pointed to a group of accumulators that will be * merged. */ public void merge(ACC accumulator, java.lang.Iterable<ACC> its); // OPTIONAL /** * Called every time when an aggregation result should be materialized. * The returned value could be either an early and incomplete result * (periodically emitted as data arrive) or the final result of the * aggregation. * * @param accumulator the accumulator which contains the current * aggregated results * @return the aggregation result */ public T getValue(ACC accumulator); // MANDATORY /** * Resets the accumulator for this [[AggregateFunction]]. This function must be implemented for * bounded grouping aggregate. * * @param accumulator the accumulator which needs to be reset */ public void resetAccumulator(ACC accumulator); // OPTIONAL /** * Returns true if this AggregateFunction can only be applied in an OVER window. * * @return true if the AggregateFunction requires an OVER window, false otherwise. */ public Boolean requiresOver = false; // PRE-DEFINED }Scala代码:/** * Base class for user-defined aggregates and table aggregates. * * @tparam T the type of the aggregation result. * @tparam ACC the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. */ abstract class UserDefinedAggregateFunction[T, ACC] extends UserDefinedFunction { /** * Creates and init the Accumulator for this (table)aggregate function. * * @return the accumulator with the initial value */ def createAccumulator(): ACC // MANDATORY /** * Returns the TypeInformation of the (table)aggregate function's result. * * @return The TypeInformation of the (table)aggregate function's result or null if the result * type should be automatically inferred. */ def getResultType: TypeInformation[T] = null // PRE-DEFINED /** * Returns the TypeInformation of the (table)aggregate function's accumulator. * * @return The TypeInformation of the (table)aggregate function's accumulator or null if the * accumulator type should be automatically inferred. */ def getAccumulatorType: TypeInformation[ACC] = null // PRE-DEFINED } /** * Base class for aggregation functions. * * @tparam T the type of the aggregation result * @tparam ACC the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. * AggregateFunction represents its state using accumulator, thereby the state of the * AggregateFunction must be put into the accumulator. */ abstract class AggregateFunction[T, ACC] extends UserDefinedAggregateFunction[T, ACC] { /** * Processes the input values and update the provided accumulator instance. The method * accumulate can be overloaded with different custom types and arguments. An AggregateFunction * requires at least one accumulate() method. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ def accumulate(accumulator: ACC, [user defined inputs]): Unit // MANDATORY /** * Retracts the input values from the accumulator instance. The current design assumes the * inputs are the values that have been previously accumulated. The method retract can be * overloaded with different custom types and arguments. This function must be implemented for * datastream bounded over aggregate. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ def retract(accumulator: ACC, [user defined inputs]): Unit // OPTIONAL /** * Merges a group of accumulator instances into one accumulator instance. This function must be * implemented for datastream session window grouping aggregate and bounded grouping aggregate. * * @param accumulator the accumulator which will keep the merged aggregate results. It should * be noted that the accumulator may contain the previous aggregated * results. Therefore user should not replace or clean this instance in the * custom merge method. * @param its an [[java.lang.Iterable]] pointed to a group of accumulators that will be * merged. */ def merge(accumulator: ACC, its: java.lang.Iterable[ACC]): Unit // OPTIONAL /** * Called every time when an aggregation result should be materialized. * The returned value could be either an early and incomplete result * (periodically emitted as data arrive) or the final result of the * aggregation. * * @param accumulator the accumulator which contains the current * aggregated results * @return the aggregation result */ def getValue(accumulator: ACC): T // MANDATORY /** * Resets the accumulator for this [[AggregateFunction]]. This function must be implemented for * bounded grouping aggregate. * * @param accumulator the accumulator which needs to be reset */ def resetAccumulator(accumulator: ACC): Unit // OPTIONAL /** * Returns true if this AggregateFunction can only be applied in an OVER window. * * @return true if the AggregateFunction requires an OVER window, false otherwise. */ def requiresOver: Boolean = false // PRE-DEFINED }下面的例子展示了如何:定义一个聚合函数来计算某一列的加权平均,在 TableEnvironment 中注册函数,在查询中使用函数。为了计算加权平均值,accumulator 需要存储加权总和以及数据的条数。在我们的例子里,我们定义了一个类 WeightedAvgAccum 来作为 accumulator。Flink 的 checkpoint 机制会自动保存 accumulator,在失败时进行恢复,以此来保证精确一次的语义。我们的 WeightedAvg(聚合函数)的 accumulate 方法有三个输入参数。第一个是 WeightedAvgAccum accumulator,另外两个是用户自定义的输入:输入的值 ivalue 和 输入的权重 iweight。尽管 retract()、merge()、resetAccumulator() 这几个方法在大多数聚合类型中都不是必须实现的,我们也在样例中提供了他们的实现。请注意我们在 Scala 样例中也是用的是 Java 的基础类型,并且定义了 getResultType() 和 getAccumulatorType(),因为 Flink 的类型推导对于 Scala 的类型推导做的不是很好。Java代码:/** * Accumulator for WeightedAvg. */ public static class WeightedAvgAccum { public long sum = 0; public int count = 0; } /** * Weighted Average user-defined aggregate function. */ public static class WeightedAvg extends AggregateFunction<Long, WeightedAvgAccum> { @Override public WeightedAvgAccum createAccumulator() { return new WeightedAvgAccum(); } @Override public Long getValue(WeightedAvgAccum acc) { if (acc.count == 0) { return null; } else { return acc.sum / acc.count; } } public void accumulate(WeightedAvgAccum acc, long iValue, int iWeight) { acc.sum += iValue * iWeight; acc.count += iWeight; } public void retract(WeightedAvgAccum acc, long iValue, int iWeight) { acc.sum -= iValue * iWeight; acc.count -= iWeight; } public void merge(WeightedAvgAccum acc, Iterable<WeightedAvgAccum> it) { Iterator<WeightedAvgAccum> iter = it.iterator(); while (iter.hasNext()) { WeightedAvgAccum a = iter.next(); acc.count += a.count; acc.sum += a.sum; } } public void resetAccumulator(WeightedAvgAccum acc) { acc.count = 0; acc.sum = 0L; } } // 注册函数 StreamTableEnvironment tEnv = ... tEnv.registerFunction("wAvg", new WeightedAvg()); // 使用函数 tEnv.sqlQuery("SELECT user, wAvg(points, level) AS avgPoints FROM userScores GROUP BY user");Scala代码:import java.lang.{Long => JLong, Integer => JInteger} import org.apache.flink.api.java.tuple.{Tuple1 => JTuple1} import org.apache.flink.api.java.typeutils.TupleTypeInfo import org.apache.flink.table.api.Types import org.apache.flink.table.functions.AggregateFunction /** * Accumulator for WeightedAvg. */ class WeightedAvgAccum extends JTuple1[JLong, JInteger] { sum = 0L count = 0 } /** * Weighted Average user-defined aggregate function. */ class WeightedAvg extends AggregateFunction[JLong, CountAccumulator] { override def createAccumulator(): WeightedAvgAccum = { new WeightedAvgAccum } override def getValue(acc: WeightedAvgAccum): JLong = { if (acc.count == 0) { null } else { acc.sum / acc.count } } def accumulate(acc: WeightedAvgAccum, iValue: JLong, iWeight: JInteger): Unit = { acc.sum += iValue * iWeight acc.count += iWeight } def retract(acc: WeightedAvgAccum, iValue: JLong, iWeight: JInteger): Unit = { acc.sum -= iValue * iWeight acc.count -= iWeight } def merge(acc: WeightedAvgAccum, it: java.lang.Iterable[WeightedAvgAccum]): Unit = { val iter = it.iterator() while (iter.hasNext) { val a = iter.next() acc.count += a.count acc.sum += a.sum } } def resetAccumulator(acc: WeightedAvgAccum): Unit = { acc.count = 0 acc.sum = 0L } override def getAccumulatorType: TypeInformation[WeightedAvgAccum] = { new TupleTypeInfo(classOf[WeightedAvgAccum], Types.LONG, Types.INT) } override def getResultType: TypeInformation[JLong] = Types.LONG } // 注册函数 val tEnv: StreamTableEnvironment = ??? tEnv.registerFunction("wAvg", new WeightedAvg()) // 使用函数 tEnv.sqlQuery("SELECT user, wAvg(points, level) AS avgPoints FROM userScores GROUP BY user")python代码:''' Java code: /** * Accumulator for WeightedAvg. */ public static class WeightedAvgAccum { public long sum = 0; public int count = 0; } // The java class must have a public no-argument constructor and can be founded in current java classloader. // Java 类必须有一个 public 的无参构造函数,并且可以在当前类加载器中加载到。 /** * Weighted Average user-defined aggregate function. */ public static class WeightedAvg extends AggregateFunction<Long, WeightedAvgAccum> { @Override public WeightedAvgAccum createAccumulator() { return new WeightedAvgAccum(); } @Override public Long getValue(WeightedAvgAccum acc) { if (acc.count == 0) { return null; } else { return acc.sum / acc.count; } } public void accumulate(WeightedAvgAccum acc, long iValue, int iWeight) { acc.sum += iValue * iWeight; acc.count += iWeight; } public void retract(WeightedAvgAccum acc, long iValue, int iWeight) { acc.sum -= iValue * iWeight; acc.count -= iWeight; } public void merge(WeightedAvgAccum acc, Iterable<WeightedAvgAccum> it) { Iterator<WeightedAvgAccum> iter = it.iterator(); while (iter.hasNext()) { WeightedAvgAccum a = iter.next(); acc.count += a.count; acc.sum += a.sum; } } public void resetAccumulator(WeightedAvgAccum acc) { acc.count = 0; acc.sum = 0L; } } ''' # 注册函数 t_env = ... # type: StreamTableEnvironment t_env.register_java_function("wAvg", "my.java.function.WeightedAvg") # 使用函数 t_env.sql_query("SELECT user, wAvg(points, level) AS avgPoints FROM userScores GROUP BY user")十五、表值聚合函数自定义表值聚合函数(UDTAGG)可以把一个表(一行或者多行,每行有一列或者多列)聚合成另一张表,结果中可以有多行多列上图展示了一个表值聚合函数的例子。假设你有一个饮料的表,这个表有 3 列,分别是 id、name 和 price,一共有 5 行。假设你需要找到价格最高的两个饮料,类似于 top2() 表值聚合函数。你需要遍历所有 5 行数据,结果是有 2 行数据的一个表。用户自定义表值聚合函数是通过扩展 TableAggregateFunction 类来实现的。一个 TableAggregateFunction 的工作过程如下。首先,它需要一个 accumulator,这个 accumulator 负责存储聚合的中间结果。 通过调用 TableAggregateFunction 的 createAccumulator 方法来构造一个空的 accumulator。接下来,对于每一行数据,会调用 accumulate 方法来更新 accumulator。当所有数据都处理完之后,调用 emitValue 方法来计算和返回最终的结果。下面几个 TableAggregateFunction 的方法是必须要实现的:createAccumulator()accumulate()Flink 的类型推导在遇到复杂类型的时候可能会推导出错误的结果,比如那些非基本类型和普通的 POJO 类型的复杂类型。所以类似于 ScalarFunction 和 TableFunction,TableAggregateFunction 也提供了 TableAggregateFunction#getResultType() 和 TableAggregateFunction#getAccumulatorType() 方法来指定返回值类型和 accumulator 的类型,这两个方法都需要返回 TypeInformation。除了上面的方法,还有几个其他的方法可以选择性的实现。有些方法可以让查询更加高效,而有些方法对于某些特定场景是必须要实现的。比如,在会话窗口(当两个会话窗口合并时会合并两个 accumulator)中使用聚合函数时,必须要实现merge() 方法。下面几个 TableAggregateFunction 的方法在某些特定场景下是必须要实现的:retract() 在 bounded OVER 窗口中的聚合函数必须要实现。merge() 在许多批式聚合和以及流式会话和滑动窗口聚合中是必须要实现的。resetAccumulator() 在许多批式聚合中是必须要实现的。emitValue() 在批式聚合以及窗口聚合中是必须要实现的。下面的 TableAggregateFunction 的方法可以提升流式任务的效率:emitUpdateWithRetract() 在 retract 模式下,该方法负责发送被更新的值。emitValue 方法会发送所有 accumulator 给出的结果。拿 TopN 来说,emitValue 每次都会发送所有的最大的 n 个值。这在流式任务中可能会有一些性能问题。为了提升性能,用户可以实现 emitUpdateWithRetract 方法。这个方法在 retract 模式下会增量的输出结果,比如有数据更新了,我们必须要撤回老的数据,然后再发送新的数据。如果定义了 emitUpdateWithRetract 方法,那它会优先于 emitValue 方法被使用,因为一般认为 emitUpdateWithRetract 会更加高效,因为它的输出是增量的。TableAggregateFunction 的所有方法都必须是 public 的、非 static 的,而且名字必须跟上面提到的一样。createAccumulator、getResultType 和 getAccumulatorType 这三个方法是在抽象父类 TableAggregateFunction 中定义的,而其他的方法都是约定的方法。要实现一个表值聚合函数,你必须扩展 org.apache.flink.table.functions.TableAggregateFunction,并且实现一个(或者多个)accumulate 方法。accumulate 方法可以有多个重载的方法,也可以支持变长参数。TableAggregateFunction 的所有方法的详细文档如下:Java代码:/** * Base class for user-defined aggregates and table aggregates. * * @param <T> the type of the aggregation result. * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. */ public abstract class UserDefinedAggregateFunction<T, ACC> extends UserDefinedFunction { /** * Creates and init the Accumulator for this (table)aggregate function. * * @return the accumulator with the initial value */ public ACC createAccumulator(); // MANDATORY /** * Returns the TypeInformation of the (table)aggregate function's result. * * @return The TypeInformation of the (table)aggregate function's result or null if the result * type should be automatically inferred. */ public TypeInformation<T> getResultType = null; // PRE-DEFINED /** * Returns the TypeInformation of the (table)aggregate function's accumulator. * * @return The TypeInformation of the (table)aggregate function's accumulator or null if the * accumulator type should be automatically inferred. */ public TypeInformation<ACC> getAccumulatorType = null; // PRE-DEFINED } /** * Base class for table aggregation functions. * * @param <T> the type of the aggregation result * @param <ACC> the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute a table aggregation result. * TableAggregateFunction represents its state using accumulator, thereby the state of * the TableAggregateFunction must be put into the accumulator. */ public abstract class TableAggregateFunction<T, ACC> extends UserDefinedAggregateFunction<T, ACC> { /** Processes the input values and update the provided accumulator instance. The method * accumulate can be overloaded with different custom types and arguments. A TableAggregateFunction * requires at least one accumulate() method. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ public void accumulate(ACC accumulator, [user defined inputs]); // MANDATORY /** * Retracts the input values from the accumulator instance. The current design assumes the * inputs are the values that have been previously accumulated. The method retract can be * overloaded with different custom types and arguments. This function must be implemented for * datastream bounded over aggregate. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ public void retract(ACC accumulator, [user defined inputs]); // OPTIONAL /** * Merges a group of accumulator instances into one accumulator instance. This function must be * implemented for datastream session window grouping aggregate and bounded grouping aggregate. * * @param accumulator the accumulator which will keep the merged aggregate results. It should * be noted that the accumulator may contain the previous aggregated * results. Therefore user should not replace or clean this instance in the * custom merge method. * @param its an {@link java.lang.Iterable} pointed to a group of accumulators that will be * merged. */ public void merge(ACC accumulator, java.lang.Iterable<ACC> its); // OPTIONAL /** * Called every time when an aggregation result should be materialized. The returned value * could be either an early and incomplete result (periodically emitted as data arrive) or * the final result of the aggregation. * * @param accumulator the accumulator which contains the current * aggregated results * @param out the collector used to output data */ public void emitValue(ACC accumulator, Collector<T> out); // OPTIONAL /** * Called every time when an aggregation result should be materialized. The returned value * could be either an early and incomplete result (periodically emitted as data arrive) or * the final result of the aggregation. * * Different from emitValue, emitUpdateWithRetract is used to emit values that have been updated. * This method outputs data incrementally in retract mode, i.e., once there is an update, we * have to retract old records before sending new updated ones. The emitUpdateWithRetract * method will be used in preference to the emitValue method if both methods are defined in the * table aggregate function, because the method is treated to be more efficient than emitValue * as it can outputvalues incrementally. * * @param accumulator the accumulator which contains the current * aggregated results * @param out the retractable collector used to output data. Use collect method * to output(add) records and use retract method to retract(delete) * records. */ public void emitUpdateWithRetract(ACC accumulator, RetractableCollector<T> out); // OPTIONAL /** * Collects a record and forwards it. The collector can output retract messages with the retract * method. Note: only use it in {@code emitRetractValueIncrementally}. */ public interface RetractableCollector<T> extends Collector<T> { /** * Retract a record. * * @param record The record to retract. */ void retract(T record); } }Scala代码:/** * Base class for user-defined aggregates and table aggregates. * * @tparam T the type of the aggregation result. * @tparam ACC the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. */ abstract class UserDefinedAggregateFunction[T, ACC] extends UserDefinedFunction { /** * Creates and init the Accumulator for this (table)aggregate function. * * @return the accumulator with the initial value */ def createAccumulator(): ACC // MANDATORY /** * Returns the TypeInformation of the (table)aggregate function's result. * * @return The TypeInformation of the (table)aggregate function's result or null if the result * type should be automatically inferred. */ def getResultType: TypeInformation[T] = null // PRE-DEFINED /** * Returns the TypeInformation of the (table)aggregate function's accumulator. * * @return The TypeInformation of the (table)aggregate function's accumulator or null if the * accumulator type should be automatically inferred. */ def getAccumulatorType: TypeInformation[ACC] = null // PRE-DEFINED } /** * Base class for table aggregation functions. * * @tparam T the type of the aggregation result * @tparam ACC the type of the aggregation accumulator. The accumulator is used to keep the * aggregated values which are needed to compute an aggregation result. * TableAggregateFunction represents its state using accumulator, thereby the state of * the TableAggregateFunction must be put into the accumulator. */ abstract class TableAggregateFunction[T, ACC] extends UserDefinedAggregateFunction[T, ACC] { /** * Processes the input values and update the provided accumulator instance. The method * accumulate can be overloaded with different custom types and arguments. A TableAggregateFunction * requires at least one accumulate() method. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ def accumulate(accumulator: ACC, [user defined inputs]): Unit // MANDATORY /** * Retracts the input values from the accumulator instance. The current design assumes the * inputs are the values that have been previously accumulated. The method retract can be * overloaded with different custom types and arguments. This function must be implemented for * datastream bounded over aggregate. * * @param accumulator the accumulator which contains the current aggregated results * @param [user defined inputs] the input value (usually obtained from a new arrived data). */ def retract(accumulator: ACC, [user defined inputs]): Unit // OPTIONAL /** * Merges a group of accumulator instances into one accumulator instance. This function must be * implemented for datastream session window grouping aggregate and bounded grouping aggregate. * * @param accumulator the accumulator which will keep the merged aggregate results. It should * be noted that the accumulator may contain the previous aggregated * results. Therefore user should not replace or clean this instance in the * custom merge method. * @param its an [[java.lang.Iterable]] pointed to a group of accumulators that will be * merged. */ def merge(accumulator: ACC, its: java.lang.Iterable[ACC]): Unit // OPTIONAL /** * Called every time when an aggregation result should be materialized. The returned value * could be either an early and incomplete result (periodically emitted as data arrive) or * the final result of the aggregation. * * @param accumulator the accumulator which contains the current * aggregated results * @param out the collector used to output data */ def emitValue(accumulator: ACC, out: Collector[T]): Unit // OPTIONAL /** * Called every time when an aggregation result should be materialized. The returned value * could be either an early and incomplete result (periodically emitted as data arrive) or * the final result of the aggregation. * * Different from emitValue, emitUpdateWithRetract is used to emit values that have been updated. * This method outputs data incrementally in retract mode, i.e., once there is an update, we * have to retract old records before sending new updated ones. The emitUpdateWithRetract * method will be used in preference to the emitValue method if both methods are defined in the * table aggregate function, because the method is treated to be more efficient than emitValue * as it can outputvalues incrementally. * * @param accumulator the accumulator which contains the current * aggregated results * @param out the retractable collector used to output data. Use collect method * to output(add) records and use retract method to retract(delete) * records. */ def emitUpdateWithRetract(accumulator: ACC, out: RetractableCollector[T]): Unit // OPTIONAL /** * Collects a record and forwards it. The collector can output retract messages with the retract * method. Note: only use it in `emitRetractValueIncrementally`. */ trait RetractableCollector[T] extends Collector[T] { /** * Retract a record. * * @param record The record to retract. */ def retract(record: T): Unit } }下面的例子展示了如何定义一个 TableAggregateFunction 来计算给定列的最大的 2 个值,在 TableEnvironment 中注册函数,在 Table API 查询中使用函数(当前只在 Table API 中支持 TableAggregateFunction)。为了计算最大的 2 个值,accumulator 需要保存当前看到的最大的 2 个值。在我们的例子中,我们定义了类 Top2Accum 来作为 accumulator。Flink 的 checkpoint 机制会自动保存 accumulator,并且在失败时进行恢复,来保证精确一次的语义。我们的 Top2 表值聚合函数(TableAggregateFunction)的 accumulate() 方法有两个输入,第一个是 Top2Accum accumulator,另一个是用户定义的输入:输入的值 v。尽管 merge() 方法在大多数聚合类型中不是必须的,我们也在样例中提供了它的实现。请注意,我们在 Scala 样例中也使用的是 Java 的基础类型,并且定义了 getResultType() 和 getAccumulatorType() 方法,因为 Flink 的类型推导对于 Scala 的类型推导支持的不是很好。Java代码:/** * Accumulator for Top2. */ public class Top2Accum { public Integer first; public Integer second; } /** * The top2 user-defined table aggregate function. */ public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> { @Override public Top2Accum createAccumulator() { Top2Accum acc = new Top2Accum(); acc.first = Integer.MIN_VALUE; acc.second = Integer.MIN_VALUE; return acc; } public void accumulate(Top2Accum acc, Integer v) { if (v > acc.first) { acc.second = acc.first; acc.first = v; } else if (v > acc.second) { acc.second = v; } } public void merge(Top2Accum acc, java.lang.Iterable<Top2Accum> iterable) { for (Top2Accum otherAcc : iterable) { accumulate(acc, otherAcc.first); accumulate(acc, otherAcc.second); } } public void emitValue(Top2Accum acc, Collector<Tuple2<Integer, Integer>> out) { // emit the value and rank if (acc.first != Integer.MIN_VALUE) { out.collect(Tuple2.of(acc.first, 1)); } if (acc.second != Integer.MIN_VALUE) { out.collect(Tuple2.of(acc.second, 2)); } } } // 注册函数 StreamTableEnvironment tEnv = ... tEnv.registerFunction("top2", new Top2()); // 初始化表 Table tab = ...; // 使用函数 tab.groupBy("key") .flatAggregate("top2(a) as (v, rank)") .select("key, v, rank");Scala代码:import java.lang.{Integer => JInteger} import org.apache.flink.table.api.Types import org.apache.flink.table.functions.TableAggregateFunction /** * Accumulator for top2. */ class Top2Accum { var first: JInteger = _ var second: JInteger = _ } /** * The top2 user-defined table aggregate function. */ class Top2 extends TableAggregateFunction[JTuple2[JInteger, JInteger], Top2Accum] { override def createAccumulator(): Top2Accum = { val acc = new Top2Accum acc.first = Int.MinValue acc.second = Int.MinValue acc } def accumulate(acc: Top2Accum, v: Int) { if (v > acc.first) { acc.second = acc.first acc.first = v } else if (v > acc.second) { acc.second = v } } def merge(acc: Top2Accum, its: JIterable[Top2Accum]): Unit = { val iter = its.iterator() while (iter.hasNext) { val top2 = iter.next() accumulate(acc, top2.first) accumulate(acc, top2.second) } } def emitValue(acc: Top2Accum, out: Collector[JTuple2[JInteger, JInteger]]): Unit = { // emit the value and rank if (acc.first != Int.MinValue) { out.collect(JTuple2.of(acc.first, 1)) } if (acc.second != Int.MinValue) { out.collect(JTuple2.of(acc.second, 2)) } } } // 初始化表 val tab = ... // 使用函数 tab .groupBy('key) .flatAggregate(top2('a) as ('v, 'rank)) .select('key, 'v, 'rank)下面的例子展示了如何使用 emitUpdateWithRetract 方法来只发送更新的数据。为了只发送更新的结果,accumulator 保存了上一次的最大的2个值,也保存了当前最大的2个值。注意:如果 TopN 中的 n 非常大,这种既保存上次的结果,也保存当前的结果的方式不太高效。一种解决这种问题的方式是把输入数据直接存储到 accumulator 中,然后在调用 emitUpdateWithRetract 方法时再进行计算。Java代码:/** * Accumulator for Top2. */ public class Top2Accum { public Integer first; public Integer second; public Integer oldFirst; public Integer oldSecond; } /** * The top2 user-defined table aggregate function. */ public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, Top2Accum> { @Override public Top2Accum createAccumulator() { Top2Accum acc = new Top2Accum(); acc.first = Integer.MIN_VALUE; acc.second = Integer.MIN_VALUE; acc.oldFirst = Integer.MIN_VALUE; acc.oldSecond = Integer.MIN_VALUE; return acc; } public void accumulate(Top2Accum acc, Integer v) { if (v > acc.first) { acc.second = acc.first; acc.first = v; } else if (v > acc.second) { acc.second = v; } } public void emitUpdateWithRetract(Top2Accum acc, RetractableCollector<Tuple2<Integer, Integer>> out) { if (!acc.first.equals(acc.oldFirst)) { // if there is an update, retract old value then emit new value. if (acc.oldFirst != Integer.MIN_VALUE) { out.retract(Tuple2.of(acc.oldFirst, 1)); } out.collect(Tuple2.of(acc.first, 1)); acc.oldFirst = acc.first; } if (!acc.second.equals(acc.oldSecond)) { // if there is an update, retract old value then emit new value. if (acc.oldSecond != Integer.MIN_VALUE) { out.retract(Tuple2.of(acc.oldSecond, 2)); } out.collect(Tuple2.of(acc.second, 2)); acc.oldSecond = acc.second; } } } // 注册函数 StreamTableEnvironment tEnv = ... tEnv.registerFunction("top2", new Top2()); // 初始化表 Table tab = ...; // 使用函数 tab.groupBy("key") .flatAggregate("top2(a) as (v, rank)") .select("key, v, rank");Scala代码:import java.lang.{Integer => JInteger} import org.apache.flink.table.api.Types import org.apache.flink.table.functions.TableAggregateFunction /** * Accumulator for top2. */ class Top2Accum { var first: JInteger = _ var second: JInteger = _ var oldFirst: JInteger = _ var oldSecond: JInteger = _ } /** * The top2 user-defined table aggregate function. */ class Top2 extends TableAggregateFunction[JTuple2[JInteger, JInteger], Top2Accum] { override def createAccumulator(): Top2Accum = { val acc = new Top2Accum acc.first = Int.MinValue acc.second = Int.MinValue acc.oldFirst = Int.MinValue acc.oldSecond = Int.MinValue acc } def accumulate(acc: Top2Accum, v: Int) { if (v > acc.first) { acc.second = acc.first acc.first = v } else if (v > acc.second) { acc.second = v } } def emitUpdateWithRetract( acc: Top2Accum, out: RetractableCollector[JTuple2[JInteger, JInteger]]) : Unit = { if (acc.first != acc.oldFirst) { // if there is an update, retract old value then emit new value. if (acc.oldFirst != Int.MinValue) { out.retract(JTuple2.of(acc.oldFirst, 1)) } out.collect(JTuple2.of(acc.first, 1)) acc.oldFirst = acc.first } if (acc.second != acc.oldSecond) { // if there is an update, retract old value then emit new value. if (acc.oldSecond != Int.MinValue) { out.retract(JTuple2.of(acc.oldSecond, 2)) } out.collect(JTuple2.of(acc.second, 2)) acc.oldSecond = acc.second } } } // 初始化表 val tab = ... // 使用函数 tab .groupBy('key) .flatAggregate(top2('a) as ('v, 'rank)) .select('key, 'v, 'rank)
0
0
0
浏览量1204
打酱油的后端

Flink系列之:窗口去重

一、窗口去重适用于Streaming窗口去重是一种特殊的去重,它根据指定的多个列来删除重复的行,保留每个窗口和分区键的第一个或最后一个数据。对于流式查询,与普通去重不同,窗口去重只在窗口的最后返回结果数据,不会产生中间结果。它会清除不需要的中间状态。 因此,窗口去重查询在用户不需要更新结果时,性能较好。通常,窗口去重直接用于 窗口表值函数 上。另外,它可以用于基于 窗口表值函数 的操作。比如 窗口聚合,窗口TopN 和 窗口关联。窗口Top-N的语法和普通的Top-N相同。 除此之外,窗口去重需要 PARTITION BY 子句包含表的 window_start 和 window_end 列。 否则优化器无法翻译。Flink 使用 ROW_NUMBER() 移除重复数据,就像 窗口 Top-N 一样。理论上,窗口是一种特殊的窗口 Top-N:N是1并且是根据处理时间或事件时间排序的。下面展示了窗口去重的语法:SELECT [column_list] FROM ( SELECT [column_list], ROW_NUMBER() OVER (PARTITION BY window_start, window_end [, col_key1...] ORDER BY time_attr [asc|desc]) AS rownum FROM table_name) -- relation applied windowing TVF WHERE (rownum = 1 | rownum <=1 | rownum < 2) [AND conditions]参数说明:ROW_NUMBER():为每一行分配一个唯一且连续的序号,从1开始。PARTITION BY window_start, window_end [, col_key1…]: 指定分区字段,需要包含window_start, window_end以及其他分区键。ORDER BY time_attr [asc|desc]: 指定排序列,必须是 时间属性。目前 Flink 支持 处理时间属性 和 事件时间属性。 Order by ASC 表示保留第一行,Order by DESC 表示保留最后一行。WHERE (rownum = 1 | rownum <=1 | rownum < 2): 优化器通过 rownum = 1 | rownum <=1 | rownum < 2 来识别查询能否被翻译成窗口去重。二、示例下面的示例展示了在10分钟的滚动窗口上保持最后一条记录。-- tables must have time attribute, e.g. `bidtime` in this table Flink SQL> DESC Bid; +-------------+------------------------+------+-----+--------+---------------------------------+ | name | type | null | key | extras | watermark | +-------------+------------------------+------+-----+--------+---------------------------------+ | bidtime | TIMESTAMP(3) *ROWTIME* | true | | | `bidtime` - INTERVAL '1' SECOND | | price | DECIMAL(10, 2) | true | | | | | item | STRING | true | | | | +-------------+------------------------+------+-----+--------+---------------------------------+ Flink SQL> SELECT * FROM Bid; +------------------+-------+------+ | bidtime | price | item | +------------------+-------+------+ | 2020-04-15 08:05 | 4.00 | C | | 2020-04-15 08:07 | 2.00 | A | | 2020-04-15 08:09 | 5.00 | D | | 2020-04-15 08:11 | 3.00 | B | | 2020-04-15 08:13 | 1.00 | E | | 2020-04-15 08:17 | 6.00 | F | +------------------+-------+------+ Flink SQL> SELECT * FROM ( SELECT bidtime, price, item, supplier_id, window_start, window_end, ROW_NUMBER() OVER (PARTITION BY window_start, window_end ORDER BY bidtime DESC) AS rownum FROM TABLE( TUMBLE(TABLE Bid, DESCRIPTOR(bidtime), INTERVAL '10' MINUTES)) ) WHERE rownum <= 1; +------------------+-------+------+-------------+------------------+------------------+--------+ | bidtime | price | item | supplier_id | window_start | window_end | rownum | +------------------+-------+------+-------------+------------------+------------------+--------+ | 2020-04-15 08:09 | 5.00 | D | supplier4 | 2020-04-15 08:00 | 2020-04-15 08:10 | 1 | | 2020-04-15 08:17 | 6.00 | F | supplier5 | 2020-04-15 08:10 | 2020-04-15 08:20 | 1 | +------------------+-------+------+-------------+------------------+------------------+--------+注意: 为了更好地理解窗口行为,这里把 timestamp 值后面的0去掉了。例如:在 Flink SQL Client 中,如果类型是 TIMESTAMP(3) ,2020-04-15 08:05 应该显示成 2020-04-15 08:05:00.000。三、限制在窗口表值函数后直接进行窗口去重的限制目前,Flink 只支持在滚动窗口、滑动窗口和累积窗口的窗口表值函数后进行窗口去重。会话窗口的去重将在未来版本中支持。根据时间属性排序的限制:目前,窗口去重只支持根据事件时间属性进行排序。根据处理时间排序将在未来版本中支持。
0
0
0
浏览量1186
打酱油的后端

Flink系列Table API和SQL之:快速上手Table API和SQL

一、引入flink相关jar包pom.xml引入flink相关jar包 <properties> <flink.version>1.13.0</flink.version> <java.version>1.8</java.version> <scala.binary.version>2.12</scala.binary.version> <slf4j.version>1.7.30</slf4j.version> </properties><!-- 引入 Flink 相关依赖--> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-java</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-clients_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <!-- 引入日志管理相关依赖--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-to-slf4j</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-api-java-bridge_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-planner-blink_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-table-common</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-csv</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-cep_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency>二、ClickSource类 import org.apache.flink.streaming.api.functions.source.SourceFunction; import java.util.Calendar; import java.util.Random; public class ClickSource implements SourceFunction<Event> { // 声明一个标志位 private Boolean running = true; @Override public void run(SourceContext<Event> ctx) throws Exception { // 随机生成数据 Random random = new Random(); // 定义字段选取的数据集 String[] users = {"Mary", "Alice", "Bob", "Cary"}; String[] urls = {"./home", "./cart", "./fav"}; // 循环生成数据 while (running){ String user = users[random.nextInt(users.length)]; String url = urls[random.nextInt(urls.length)]; Long timestamp = Calendar.getInstance().getTimeInMillis(); ctx.collect(new Event(user,url,timestamp)); Thread.sleep(1000L); } } @Override public void cancel() { running = false; } }三、Event类import java.sql.Timestamp; public class Event { public String user; public String url; public Long timestamp; public Event(){ } public Event(String user,String url,Long timestamp){ this.user = user; this.url = url; this.timestamp = timestamp; } public String toString(){ return "Event{" + "user='" + user + '\'' + ", url='" + url + '\'' + ", timestamp=" + new Timestamp(timestamp) + '}'; } }四、快速上手Table API和SQLimport org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; public class SimpleTableExample { public static void main(String[] args) throws Exception{ StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); SingleOutputStreamOperator<Event> eventStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event element, long recordTimestamp) { return element.timestamp; } }) ); //创建表执行环境 StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); //将dataStream转换成table Table eventTable = tableEnv.fromDataStream(eventStream); //直接写sql进行转换 Table resultTable = tableEnv.sqlQuery("select user,url from " + eventTable); //基于table直接转换 Table resultTable2 = eventTable.select($("user"), $("url")) .where($("user").isEqual("Alice")); //转换成流打印输出 tableEnv.toDataStream(resultTable2).print("result2"); tableEnv.toDataStream(resultTable).print("result"); env.execute(); } }五、运行输出如下所示result2> +I[Alice, ./cart] result> +I[Alice, ./cart] result> +I[Mary, ./cart] result> +I[Mary, ./fav] result> +I[Mary, ./cart] result2> +I[Alice, ./fav] result> +I[Alice, ./fav] result> +I[Cary, ./fav] result> +I[Bob, ./cart] result> +I[Mary, ./fav] result> +I[Bob, ./home] result> +I[Bob, ./home] result> +I[Mary, ./home] result> +I[Mary, ./home]
0
0
0
浏览量173
打酱油的后端

Flink系列Table API和SQL之:滚动窗口、滑动窗口、累计窗口、分组聚合

一、窗口(Window)有了时间属性,接下来就可以定义窗口进行计算了。窗口可以将无界流切割成大小有限的桶(bucket)来做计算,通过截取有限数据集来处理无限的流数据。在DataStream API中提供了对不同类型的窗口进行定义和处理的接口,而在Table API和SQL中,类似的功能也都可以实现。二、分组窗口(Group Window)在Flink 1.12之前的版本中,Table API和SQL提供了一组分组窗口(Group Window)函数,常用的时间窗口如滚动窗口、滑动窗口、会话窗口都有对应的实现。具体在SQL中就是调用TUMBLE()、HOP()、SESSION(),传入时间属性字段、窗口大小等参数就可以了。以滚动窗口为例:TUMBLE(ts,INTERVAL '1' HOUR)这里的ts是定义好的时间属性字段,窗口大小用时间间隔 INTERVAL来定义。在进行窗口计算时,分组窗口是将窗口本身当作一个字段对数据进行分组的,可以对组内的数据进行聚合。基本使用方式如下:Table result = tableEnv.sqlQuery( "SELECT " + "user, " + "TUMBLE_END(ts,INTERVAL '1' HOUR) as endT, " + "COUNT(url) AS cnt " + "FROM EventTable " + "GROUP BY "+ //使用窗口和用户名进行分组 "user, " + "TUMBLE(ts,INTERVAL '1' HOUR)" //定义1小时滚动窗口 );这里定义了1小时的滚动窗口,将窗口和用户user一起作为分组的字段。用聚合函数COUNT()对分组数据的个数进行了聚合统计,并将结果字段重命名为cnt。用TUPMBLE_END()函数获取滚动窗口的结束时间,重命名为endT提取出来。分组窗口的功能比较有限,只支持窗口聚合,所以目前已经处于弃用deprecated的状态。三、窗口表值函数(Windowing TVFs)Flink开始使用窗口表值函数(Windowing table-valued functions,Windowing TVFs)来定义窗口。窗口表值函数是Flink定义的多表函数(PTF),可以将表进行扩展后返回。表函数(table function)可以看作是返回一个表的函数。目前Flink提供了以下几个窗口TVF:滚动窗口(Tumbling Windows)滑动窗口(Hop Windows,跳跃窗口)累积窗口(Cumulate Windows)会话窗口(Session Windows)窗口表值函数可以完全替代传统的分组窗口函数,窗口TVF更符合SQL标准,性能得到了优化,拥有更强大的功能。可以支持基于窗口的复杂计算,例如窗口Top-N、窗口联结(window join)等。在窗口TVF的返回值中,除去原始表中的所有列,还增加了用来描述窗口的额外3个列。窗口起始点(window_start)、窗口结束点(window_end)、窗口时间(window_time)。起始点和结束点比较好理解,窗口时间指的是窗口中的时间属性,它的值等于window_end - 1ms,所以相当于是窗口能够包含数据的最大时间戳。在SQL中的声明方式,与以前的分组窗口是类似的,直接调用TUMBLE()、HOP()、CUMULATE()就可以实现滚动、滑动和累积窗口,不过传入的参数会有所不同。分别对这几种窗口TVF进行介绍1.滚动窗口(TUMBLE)滚动窗口中SQL中的概念与DataStream API中的定义完全一样,是长度固定、时间对齐、无重叠的窗口,一般用于周期性的统计计算。在SQL中通过调用TUMBLE()函数就可以声明一个滚动窗口,只有一个核心参数就是窗口大小(size)。在SQL中不考虑计数窗口,所以滚动窗口就是滚动时间窗口,参数重还需要将当前的时间属性字段传入,另外,窗口TVF本质上是表函数,可以对表进行扩展,所以还应该把当前查询的表昨晚参数整体传入。具体声明如下:TUMBLE(TABLE EventTable,DESCRIPTOR(ts),INTERVAL '1' HOUR)这里基于时间字段ts,对表EventTable中的数据开了大小1小时的滚动窗口。窗口会将表中的每一行数据,按照它们ts的值分配到一个指定的窗口中。2.滑动窗口(HOP)滑动窗口的使用与滚动窗口类似,可以通过设置滑动步长来控制统计输出的频率。在SQL中通过调用HOP()来声明滑动窗口,除了也要传入表名、时间属性外,还需要传入窗口大小(size)和滑动步长(slide)两个参数。HOP(TABLE EventTable,DESCRIPTOR(ts),INTERVAL '5' MINUTES,INTERVAL '1' HOURS);基于时间属性ts,在表EventTable上创建了大小为1小时的滑动窗口,每5分钟滑动一次。需要注意的是,紧跟在时间属性字段后面的第三个参数是步长slide,第四个参数才是窗口大小size。3.累计窗口(CUMULATE)滚动窗口和滑动窗口,可以用来计算大多数周期性的统计指标。不过在实际应用中还会遇到这样一类需求:统计周期可能较长,希望中间每隔一段时间就输出一次当前的统计值。与滑动窗口不同的是,在一个统计周期内,会多次输出统计值,应该是不断叠加累积的。例如:按天来统计网站的PV(Page View,页面浏览量),如果用1天的滚动窗口,那需要到每天24点才会计算一次,输出频率太低。如果用滑动窗口,计算频率可以更高,但统计的就变成了过去24小时的PV。所以真正希望的是,还是按照自然日统计每天的PV,不过需要每隔1小时就输出一次当天到目前为止的PV值。这种特殊的窗口就叫作累积窗口(Cumulate Window)。累积窗口是窗口TVF中新增的窗口功能,它会在一定的统计周期内进行累积计算。累积窗口中有两个核心的参数:最大窗口长度(max window size)和累积步长(step)。最大窗口长度其实就是我们所说的统计周期,最终目的就是统计这段时间内的数据。开始时,创建的第一个窗口大小就是步长step。之后的每个窗口都会在之前的基础上再扩展step的长度,直到达到最大窗口长度。在SQL中可以用CUMULATE()函数来定义,具体如下:CUMULATE(TABLE EventTable,DESCRIPTOR(ts),INTERVAL '1' HOURS,INTERVAL '1' DAYS)基于时间属性ts,在表EventTable上定义了一个统计周期为1天、累计步长伟1小时的累积窗口。注意第三个参数为步长step,第四个参数则是最大窗口长度。上面所有的语句只是定义了窗口,类似于DataStream API中的窗口分配器。在SQL中窗口的完整调用,还需要配合聚合操作和其他操作。四、分组聚合在SQL中,一个很常见的功能就是对某一列的多条数据做一个合并统计,得到一个或多个结果值:比如求和、最大最小值、平均值等,这种操作叫做聚合查询。Flink中的SQL是流处理与标准SQL结合的产物,所以聚合查询也可以分成两种:流处理中特有的聚合(主要指窗口聚合),以及SQL原生的聚合查询方式。分组聚合:SQL中一般所说的聚合,主要是通过内置的一些聚合函数来实现的,比如SUM()、MAX()、MIN()、AVG()以及COUNT()。特点是对多条输入数据进行计算,得到一个唯一的值,属于多对一的转换。比如可以通过下面的代码计算输入数据的个数。Table eventCountTable = tableEnv.sqlQuery("select COUNT(*) from EventTable");更多的情况下,可以通过GROUP BY子句来指定分组的键(key),从而对数据按照某个字段做一个分组统计,例如按照用户名进行分组,统计每个用户点击url的次数:SELECT user,COUNT(url) as cnt FROM EventTable GROUP BY user这种聚合方式,就叫做分组聚合(group aggregation)。从概念上讲,SQL中的分组聚合可以对应DataStream API中keyBy之后的聚合转换,都是按照某个key对数据进行了划分,各自维护状态来进行聚合统计。在流处理中,分组聚合同样是一个持续查询,而且是一个更新查询,得到的是一个动态表。每当流中有一个新的数据到来时,都会导致结果表的更新操作。因此,想要将结果表转换成流或输出到外部系统,必须采用撤回流或者更新插入流(upsert stream)的编码方式。如果在代码中直接转换成DataStream打印输出,需要调用toChangelogStream()。另外在持续查询的过程中,由于用于分组的key可能会不断增加,因此计算结果所需要维护的状态也会持续增长。为了防止状态无限增长耗尽资源,Flink Table API和SQL可以在表环境中配置状态的生存时间(TTL):TableEnvironment tableEnv = ... //获取表环境的配置 TableConfig tableConfig = tableEnv.getConfig(); //配置状态保持时间 tableConfig.setIdleStateRetention(Duration.ofMinutes(60));或者也可以直接设置配置项table.exec.state.ttlTableEnvironment tableEnv = ... Configuration configuration = tableEnv.getConfig().getConfiguration(); configuration.setString("table.exec.state.ttl","60 min");这两种方式是等效的,需要注意,配置TTL有可能会导致统计结果不准确,其实是以五、分组聚合实现代码import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; /** * Copyright (c) 2020-2030 尚硅谷 All Rights Reserved * <p> * Project: FlinkTutorial * <p> * Created by wushengran */ public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 聚合查询转换 // 1. 分组聚合 Table aggTable = tableEnv.sqlQuery("SELECT user_name, COUNT(1) FROM clickTable GROUP BY user_name"); // 结果表转换成流打印输出 tableEnv.toChangelogStream(aggTable).print("agg: "); env.execute(); } }( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* ) agg: > +I[Mary, 1] agg: > +I[Bob, 1] agg: > +I[Alice, 1] agg: > -U[Bob, 1] agg: > +U[Bob, 2] agg: > -U[Alice, 1] agg: > +U[Alice, 2]六、分组窗口聚合代码实现public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 2. 分组窗口聚合 Table groupWindowResultTable = tableEnv.sqlQuery("SELECT " + "user_name, " + "COUNT(1) AS cnt, " + "TUMBLE_END(et, INTERVAL '10' SECOND) as endT " + "FROM clickTable " + "GROUP BY " + // 使用窗口和用户名进行分组 " user_name, " + " TUMBLE(et, INTERVAL '10' SECOND)" // 定义1小时滚动窗口 ); // 结果表转换成流打印输出 tableEnv.toDataStream(groupWindowResultTable).print("group window: "); env.execute(); } } ( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* ) group window: > +I[Mary, 1, 1970-01-01T08:00:10] group window: > +I[Bob, 2, 1970-01-01T08:00:10] group window: > +I[Alice, 2, 1970-01-01T08:00:10] group window: > +I[Bob, 1, 1970-01-01T08:00:20] group window: > +I[Alice, 1, 1970-01-01T08:00:30]七、窗口聚合:滚动窗口import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; /** * Copyright (c) 2020-2030 尚硅谷 All Rights Reserved * <p> * Project: FlinkTutorial * <p> * Created by wushengran */ public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 3. 窗口聚合 // 3.1 滚动窗口 Table tumbleWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " + " window_end AS endT " + "FROM TABLE( " + " TUMBLE( TABLE clickTable, DESCRIPTOR(et), INTERVAL '10' SECOND)" + ") " + "GROUP BY user_name, window_start, window_end " ); // 结果表转换成流打印输出 tableEnv.toDataStream(tumbleWindowResultTable).print("tumble window: "); env.execute(); } }( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* ) tumble window: > +I[Mary, 1, 1970-01-01T08:00:10] tumble window: > +I[Bob, 2, 1970-01-01T08:00:10] tumble window: > +I[Alice, 2, 1970-01-01T08:00:10] tumble window: > +I[Bob, 1, 1970-01-01T08:00:20] tumble window: > +I[Alice, 1, 1970-01-01T08:00:30]八、窗口聚合:滑动窗口import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; /** * Copyright (c) 2020-2030 尚硅谷 All Rights Reserved * <p> * Project: FlinkTutorial * <p> * Created by wushengran */ public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 3.2 滑动窗口 Table hopWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " + " window_end AS endT " + "FROM TABLE( " + " HOP( TABLE clickTable, DESCRIPTOR(et), INTERVAL '5' SECOND, INTERVAL '10' SECOND)" + ") " + "GROUP BY user_name, window_start, window_end " ); // 结果表转换成流打印输出 tableEnv.toDataStream(hopWindowResultTable).print("hop window: "); env.execute(); } }( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* ) hop window: > +I[Mary, 1, 1970-01-01T08:00:05] hop window: > +I[Bob, 2, 1970-01-01T08:00:05] hop window: > +I[Alice, 2, 1970-01-01T08:00:05] hop window: > +I[Bob, 2, 1970-01-01T08:00:10] hop window: > +I[Alice, 2, 1970-01-01T08:00:10] hop window: > +I[Mary, 1, 1970-01-01T08:00:10] hop window: > +I[Bob, 1, 1970-01-01T08:00:15] hop window: > +I[Bob, 1, 1970-01-01T08:00:20] hop window: > +I[Alice, 1, 1970-01-01T08:00:25] hop window: > +I[Alice, 1, 1970-01-01T08:00:30]九、窗口聚合:累积窗口import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator; 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 java.time.Duration; import static org.apache.flink.table.api.Expressions.$; /** * Copyright (c) 2020-2030 尚硅谷 All Rights Reserved * <p> * Project: FlinkTutorial * <p> * Created by wushengran */ public class TimeAndWindowTest { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // 1. 在创建表的DDL中直接定义时间属性 String createDDL = "CREATE TABLE clickTable (" + " user_name STRING, " + " url STRING, " + " ts BIGINT, " + " et AS TO_TIMESTAMP( FROM_UNIXTIME(ts / 1000) ), " + " WATERMARK FOR et AS et - INTERVAL '1' SECOND " + ") WITH (" + " 'connector' = 'filesystem', " + " 'path' = '/Users/fei.yang4/project/learn/src/main/java/com/bigdata/plus/flink/input/clicks.csv', " + " 'format' = 'csv' " + ")"; tableEnv.executeSql(createDDL); // 2. 在流转换成Table时定义时间属性 SingleOutputStreamOperator<Event> clickStream = env.addSource(new ClickSource()) .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<Event>() { @Override public long extractTimestamp(Event event, long l) { return event.timestamp; } })); Table clickTable = tableEnv.fromDataStream(clickStream, $("user"), $("url"), $("timestamp").as("ts"), $("et").rowtime()); clickTable.printSchema(); // 聚合查询转换 // 3.3 累积窗口 Table cumulateWindowResultTable = tableEnv.sqlQuery("SELECT user_name, COUNT(url) AS cnt, " + " window_end AS endT " + "FROM TABLE( " + " CUMULATE( TABLE clickTable, DESCRIPTOR(et), INTERVAL '5' SECOND, INTERVAL '10' SECOND)" + ") " + "GROUP BY user_name, window_start, window_end " ); // 结果表转换成流打印输出 tableEnv.toDataStream(cumulateWindowResultTable).print("cumulate window: "); env.execute(); } }( `user` STRING, `url` STRING, `ts` BIGINT, `et` TIMESTAMP(3) *ROWTIME* ) cumulate window: > +I[Mary, 1, 1970-01-01T08:00:05] cumulate window: > +I[Bob, 2, 1970-01-01T08:00:05] cumulate window: > +I[Alice, 2, 1970-01-01T08:00:05] cumulate window: > +I[Bob, 2, 1970-01-01T08:00:10] cumulate window: > +I[Alice, 2, 1970-01-01T08:00:10] cumulate window: > +I[Mary, 1, 1970-01-01T08:00:10] cumulate window: > +I[Bob, 1, 1970-01-01T08:00:15] cumulate window: > +I[Bob, 1, 1970-01-01T08:00:20] cumulate window: > +I[Alice, 1, 1970-01-01T08:00:25] cumulate window: > +I[Alice, 1, 1970-01-01T08:00:30]
0
0
0
浏览量429
打酱油的后端

深入了解 Flink

学习Flink系列Table API和SQL,涵盖时间属性、窗口操作、开窗Over聚合、Top N查询、Table API和SQL入门、联结查询等。探索Debezium和Flink MySQL CDC实现数据库表数据同步,深入了解MySQL CDC连接器
0
0
0
浏览量2420
打酱油的后端

动力节点RabbitMQ学习笔记,超详细的入门使用教程

RabbitMQ是流行的开源消息队列系统,用erlang语言开发,RabbitMQ是AMQP(高级消息队列协议)的标准实现。采用该技术,我们可以实现异步处理、流量削峰、系统解耦。RabbitMQ 整体是一个与其他中间件相同的模型,主要是负责接收、存储和转发消息。可以把消息传递的过程想想成快递:将一个快递送到快递公司,快递公司由快递员送到收件人的人中,RabbitMQ就类似于一个快递公司,也就是RabbitMQ的交换机模型。什么是消息队列(MQ)消息是在不同应用间传递的数据。这里的消息可以非常简单,比如只包含字符串,也可以非常复杂,包含多个嵌套的对象。消息队列(Message Queue)简单来说就是一种应用程序间的通讯方式,消息发送后立即返回,然后由消息系统保证消息的可靠性传输,消息生产者只需要把消息发到MQ中就可以了,不需要关心消息的消费,同样,消息消费者只管从MQ中拉取消息而不管是谁生产的消息,通过这样的一个“互相不知道对象存在”模式,将消息的生产者和消息的消费者解耦了。什么场景下考虑使用消息队列?从上面可以知道,消息队列是一种应用间的异步协作机制,那么我们什么时候需要用到MQ呢?以常见的订单系统为例,当用户点击「下单」后的业务逻辑可能包括:扣减库存、生成相应订单数据、发短信通知等。在项目和业务发展初期上面这些逻辑可能放在一起执行,随着业务的发展订单量的增加,需要提升系统服务的性能,此时就可以将一些不需要立即生效的操作拆分出来异步执行,比如发送短信通知等。这种场景下就可以使用MQ,在下单主流程(比如扣减库存、生成订单数据等)完成之后发送一条消息到MQ让主流程快速走完,然后由另外一个线程拉取MQ的消息,执行相应的业务逻辑。这里的例子主要是用消息队列来解耦。RabbitMQ的特点RabbitMQ是一个由Erlang语言开发的AMQP的开源实现。AMQP(Advanced Message Queue:高级消息队列协议)它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。RabbitMQ最初起源于消息系统,用于在分布式系统中存储转发消息,具体有如下一些特点:可靠性:RabbitMQ使用一些机制来保证可靠性,比如持久化、传输确认机制(ack)和发布确认等。灵活的路由策略:在消息进入队列之前,通过Exchange来路由消息,对于典型的路由功能,RabbitMQ已经提供了一些内置的Exchange来实现。针对复杂的路由功能,可以将多个Exchange绑在一起,也通过插件机制实现自己的Exchange。消息集群:多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。高可用:队列可以在集群中的集群上进行镜像,使得在部分节点出问题的情况下队列仍然可用。多种协议:RabbitMQ支持多种消息队列协议,比如STOMP、MQTT等。多语言客户端:RabbitMQ几乎支持多有常用的语言,比如:Java、.NET等管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。想深入学习RabbitMQ,动力节点的视频教程将讲授RabbitMQ的环境搭建、消息的发送与接收、消息确认、与SpringBoot集成等,可以快速掌握RabbitMQ技术,以适应项目开发的需要。RabbitMQ实战视频教程https://www.bilibili.com/video/BV1Ap4y1D7tURabbitMQ教程配套资料下载http://www.bjpowernode.com/?csdn课程学习目录001.RabbitMQ: 什么是消息队列•002.RabbitMQ: 为什么使用消息队列•003.RabbitMQ: RabbitMQ的特点•004.RabbitMQ: 安装RabbitMQ•005.RabbitMQ: 启动和关闭服务•006.RabbitMQ: 插件添加•007.RabbitMQ: 用户管理•008.RabbitMQ: 权限分配•009.RabbitMQ: vhost•010.RabbitMQ: AMQP协议机制•011.RabbitMQ: 消息路由交换机类型•012.RabbitMQ: Direct类型交换机•013.RabbitMQ: Fanout类型交换机•014.RabbitMQ: Topic类型交换机•015.RabbitMQ: 消息发送•016.RabbitMQ: 消息发送测试•017.RabbitMQ: 消息接收•018.RabbitMQ: 消息接收测试•019.RabbitMQ: 交换机-direct-消息接收•020.RabbitMQ: 交换机-direct-消息发送•021.RabbitMQ: 交换机-fanout-消息接收•022.RabbitMQ: 交换机-fanout-消息发送•023.RabbitMQ: 交换机-topic-消息接收和发送•024.RabbitMQ: 交换机-fanout和topic使用场景对比•025.RabbitMQ: 事务性消息发送•026.RabbitMQ: 事务性消息接收•027.RabbitMQ: 发送者确认模式-普通确认•028.RabbitMQ: 发送者确认模式-批量确认•029.RabbitMQ: 发送者确认模式-异步确认•030.RabbitMQ: 消费者确认模式-手动确认消息•031.RabbitMQ: 消费者确认模式-事务对接收的影响以及防重复处理•032.RabbitMQ: SpringBoot集成RabbitMQ-direct发送消息•033.RabbitMQ: SpringBoot集成RabbitMQ-direct接收消息•034.RabbitMQ: SpringBoot集成RabbitMQ-异步监听接收消息•035.RabbitMQ: SpringBoot集成RabbitMQ-fanout接收消息•036.RabbitMQ: SpringBoot集成RabbitMQ-fanout发送消息•037.RabbitMQ: SpringBoot集成RabbitMQ-topic发送和接收消息•038.RabbitMQ: 集群-集群模式介绍•039.RabbitMQ: 集群-环境搭建•040.RabbitMQ: 集群-配置集群•041.RabbitMQ: 集群-SpringBoot链接集群•042.RabbitMQ: 集群-镜像集群配置
0
0
0
浏览量2159
打酱油的后端

动力节点rabbitmq-1-6章What is RabbitMQ?

1. What is RabbitMQ?1.1简介RabbitMQ是一个广泛使用的消息服务器,采用Erlang语言编写,是一种开源的实现 AMQP(高级消息队列协议)的消息中间件;RabbitMQ最初起源于金融系统,它的性能及稳定性都非常出色;AMQP协议(http://www.amqp.org),即 Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计;我们学的协议有哪些:(http、ftp)1.2 相关网址官网:https://www.rabbitmq.comGithub:https://github.com/rabbitmq1.3 消息中间件(MQ=Message Queue)简单来说,消息中间件就是指保存数据的一个容器(服务器),可以用于两个系统之间的数据传递。  消息中间件一般有三个主要角色:生产者、消费者、消息代理(消息队列、消息服务器);rabbitmq-java-client rabbitmq-server rabbitmq-java-client  生产者发送消息到消息服务器,然后消费者从消息代理(消息队列)中获取数据并进行处理;1.4 常用的消息中间件目前比较主流的几个消息中间件:RabbitMQkafka(大数据领域)RocketMQ(阿里巴巴开源)献给Apache组织pulsar(最近一两年流行起来的)2. MQ(Message Queue)的应用场景2.1 异步处理下订单:下订单–》加积分–》发红包–》发手机短信下订单—向MQ 发消息–》积分系统,红包系统,手机短信系统接收消息  同步是阻塞的(会造成等待),异步是非阻塞的(不会等待);  大流量高并发请求、批量数据传递,就可以采用异步处理,提升系统吞吐量;2.2系统解耦多个系统之间,不需要直接交互,通过消息进行业务流转;2.3 流量削峰高负载请求/任务的缓冲处理;2.4日志处理主要是用kafka这个服务器来做;  日志处理是指将消息队列用于在日志处理中,比如Kafka解决大量日志传输的问题;  loger.info(…)  ELK 日志处理解决方案:  loger.error(…) -->logstash收集消息–> 发送消息的kafka --> elastic search (es) -->Kibana ELK日志处理平台3. RabbitMQ运行环境搭建RabbitMQ是使用Erlang语言开发的,所以要先下载安装Erlang3.1 Erlang及RabbitMQ安装版本的选择下载时一定要注意版本兼容性  版本兼容说明地址:https://www.rabbitmq.com/which-erlang.html  我们选择的版本3.2下载ErlangErlang官网 https://www.erlang.org/ Linux下载: wget https://github.com/erlang/otp/releases/download/OTP-25.1.1/otp_src_25.1.1.tar.gz说明:wget 是linux命令,可以用来下载软件3.3 安装Erlang3.3.1 安装erlang前先安装Linux依赖库yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel说明:yum -y install 安装linux的一些依赖库的命令 ,-y表示自动确认;3.3.2 解压erlang压缩包文件tar -zxvf otp_src_25.1.1.tar.gz3.3.3 配置切换到解压的目录下,运行相应命令cd otp_src_25.1.1 ./configure3.3.4 编译make3.3.5安装make install安装好了erlang后可以将解压的文件夹删除:rm -rf otp_src_25.1.13.3.6 验证erlang是否安装成功在命令行输入: erl 如果进入了编程命令行则表示安装成功,然后按ctrl + z 退出编程命令行;3.4 下载RabbitMQ从RabbitMQ官网https://www.rabbitmq.com找到下载链接  Linux:下载3.10.11wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.10.11/rabbitmq-server-generic-unix-3.10.11.tar.xz generic 是通用的意思,这个版本也就是通用的unix版本3.5 安装RabbitMQ解压RabbitMQ的压缩包,即安装完成,无需再编译tar -xvf rabbitmq-server-generic-unix-3.10.11.tar.xz -C /usr/local/说明 -C 选项是指定解压目录,如果不指定会解压到当前目录  此时rabbitmq就安装好了;4. 启动及停止RabbitMQ4.1启动RabbitMQ切换到安装目录的sbin目录下:#启动 ./rabbitmq-server -detached说明:  -detached 将表示在后台启动运行rabbitmq;不加该参数表示前台启动;  rabbitmq的运行日志存放在安装目录的var目录下;  现在的目录是:/usr/local/rabbitmq_server-3.10.11/var/log/rabbitmq4.2 查看RabbitMQ的状态切换到sbin目录下执行:./rabbitmqctl -n rabbit status说明:-n rabbit 是指定节点名称为rabbit,目前只有一个节点,节点名默认为rabbit  此处-n rabbit 也可以省略4.3 停止RabbitMQ切换到sbin目录下执行:./rabbitmqctl shutdown4.4 配置path环境变量vi /etc/profileRABBIT_HOME=/usr/local/rabbitmq_server-3.10.11 PATH=PATH : PATH:PATH:RABBIT_HOME/sbin export RABBIT_HOME PATH刷新环境变量,命令如下source /etc/profile5. RabbitMQ管理命令./rabbitmqctl 是一个管理命令,可以管理rabbitmq的很多操作。./rabbitmqctl help可以查看一下有哪些操作  查看具体子命令 可以使用 ./rabbitmqctl help 子命令名称5.1 用户管理用户管理包括增加用户,删除用户,查看用户列表,修改用户密码。  这些操作都是通过rabbitmqctl管理命令来实现完成。  查看帮助:rabbitmqctl add_user --help相应的命令(1) 查看当前用户列表rabbitmqctl list_users(2) 新增一个用户| 语法:rabbitmqctl add_user Username Passwordrabbitmqctl list_users5.2 设置用户角色| rabbitmqctl set_user_tags User Tag示例: rabbitmqctl add_user admin 123456说明:此处设置用户的角色为管理员角色5.3 设置用户权限rabbitmqctl set_permissions -p / admin "." "." ".*"说明:此操作是设置admin用户拥有操作虚拟主机/下的所有权限  查看用户权限./rabbitmqctl list_permissions6. web管理后台Rabbitmq有一个web管理后台,这个管理后台是以插件的方式提供的,启动后台web管理功能,切换到sbin目录下执行:6.1 启用管理后台# 查看rabbitmq 的插件列表 ./rabbitmq-plugins list #启用 ./rabbitmq-plugins enable rabbitmq_management #禁用 ./rabbitmq-plugins disable rabbitmq_management6.2防火墙操作systemctl status firewalld --检查防火墙状态 systemctl stop firewalld --关闭防火墙,Linux重启之后会失效 systemctl disable firewalld --防火墙置为不可用,Linux重启后,防火墙服务不自动启动,依然是不可用6.3 访问http://192.168.131.131:15672用户名/密码为我们上面创建的admin/123456注意上面改成你的虚拟主机的ip地址备注:如果使用默认用户guest、密码guest登录,会提示User can only log in via localhost  说明guest用户只能从localhost本机登录,所以不要使用该用户。6.4 通过web页面新建虚拟主机  建完后如下
0
0
0
浏览量2067
打酱油的后端

RabbitMQ笔记一7-11章RabbitMQ工作模型【动力节点】

7. RabbitMQ工作模型  broker 相当于mysql服务器,virtual host相当于数据库(可以有多个数据库)  queue相当于表,消息相当于记录。  消息队列有三个核心要素: 消息生产者、消息队列、消息消费者;  生产者(Producer):发送消息的应用;(java程序,也可能是别的语言写的程序)  消费者(Consumer):接收消息的应用;(java程序,也可能是别的语言写的程序)  代理(Broker):就是消息服务器,RabbitMQ Server就是Message Broker;  连接(Connection):连接RabbitMQ服务器的TCP长连接;  信道(Channel):连接中的一个虚拟通道,消息队列发送或者接收消息时,都是通过信道进行的;  虚拟主机(Virtual host):一个虚拟分组,在代码中就是一个字符串,当多个不同的用户使用同一个RabbitMQ服务时,可以划分出多个Virtual host,每个用户在自己的Virtual host创建exchange/queue等;(分类比较清晰、相互隔离)  交换机(Exchange):交换机负责从生产者接收消息,并根据交换机类型分发到对应的消息队列中,起到一个路由的作用;  路由键(Routing Key):交换机根据路由键来决定消息分发到哪个队列,路由键是消息的目的地址;  绑定(Binding):绑定是队列和交换机的一个关联连接(关联关系);  队列(Queue):存储消息的缓存;  消息(Message):由生产者通过RabbitMQ发送给消费者的信息;(消息可以任何数据,字符串、user对象,json串等等)8. RabbitMQ交换机类型Exchange(X) 可翻译成交换机/交换器/路由器8.1 RabbitMQ交换器 (Exchange)类型  1、Fanout Exchange(扇形)  2、Direct Exchange(直连)  3、Topic Exchange(主题)8.2 Fanout Exchange8.2.1 介绍Fanout 扇形的,散开的; 扇形交换机  投递到所有绑定的队列,不需要路由键,不需要进行路由键的匹配,相当于广播、群发;8.2.2 示例8.3 Direct Exchange8.3.1 介绍根据路由键精确匹配(一模一样)进行路由消息队列;8.3.2 示例8.4 Topic Exchange8.4.1 介绍通配符匹配,相当于模糊匹配;  #匹配多个单词,用来表示任意数量(零个或多个)单词匹配一个单词(必须有一个,而且只有一个),用.隔开的为一个单词: beijing.# == beijing.queue.abc, beijing.queue.xyz.xxx beijing. == beijing.queue, beijing.xyz  发送时指定的路由键:lazy.orange.rabbit8.4.2 示例** **8.5 Headers Exchange (用的比较少)8.5.1 介绍基于消息内容中的headers属性进行匹配;8.5.2 示例绑定参考代码:Map<String, Object> headerValues = new HashMap<>(); headerValues.put("type", "m"); headerValues.put("status", 1); return BindingBuilder.bind(queueA).to(headersExchange).whereAll(headerValues).match(); 发送参考代码MessageProperties messageProperties = new MessageProperties(); messageProperties.setHeader("type", "m"); messageProperties.setHeader("status", 1); Message message = new Message(msg.getBytes(), messageProperties); // void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException; amqpTemplate.convertAndSend(RabbitConfig.EXCHANGE, null, message);学习它的目的是:发消息时可以指定消息属性(MessageProperties)9. RabbitMQ过期消息过期消息也叫TTL消息,TTL:Time To Live  消息的过期时间有两种设置方式:(过期消息)9.1 设置单条消息的过期时间参考代码MessageProperties messageProperties = new MessageProperties();messageProperties.setExpiration("15000"); // 设置过期时间,单位:毫秒 Message message = new Message(json.getBytes(), messageProperties); //发送消息 Template.convertAndSend(RabbitConfig.DIRECT_EXCHANGE, RabbitConfig.DIRECT_ROUTINGKEY, message); System.out.println("发送完毕:" + new Date());单条消息的过期时间决定了在没有任何消费者消费时,消息可以存活多久。9.2 通过队列属性设置消息过期时间@Bean public Queue directQueue() { Map<String, Object> arguments = new HashMap<>(); arguments.put("x-message-ttl", 10000); return new Queue(DIRECT_QUEUE, true, false, false, arguments); }队列的过期时间决定了在没有任何消费者的情况下,队列中的消息可以存活多久。  注意事项:  如果消息和对列都设置过期时间,则消息的TTL以两者之间较小的那个数值为准。10.   RabbitMQ死信队列也有叫 死信交换机、死信邮箱等说法;  DLX: Dead-Letter-Exchange 死信交换器,死信邮箱  如下情况下一个消息会进入DLX(Dead Letter Exchange)死信交换机。10.1 消息过期参考代码MessageProperties messageProperties=new MessageProperties(); //设置此条消息的过期时间为10秒 messageProperties.setExpiration("10000");10.2 队列过期参考代码Map<String, Object> arguments =new HashMap<>(); //指定死信交换机,通过x-dead-letter-exchange 来设置 arguments.put("x-dead-letter-exchange",EXCHANGE_DLX); //设置死信路由key,value 为死信交换机和死信队列绑定的key,要一模一样,因为死信交换机是直连交换机 arguments.put("x-dead-letter-routing-key",BINDING_DLX_KEY); //队列的过期时间 arguments.put("x-message-ttl",10000); return new Queue(QUEUE_NORMAL,true,false,false,arguments);TTL: Time to Live的简称,过期时间10.3 队列达到最大长度(先入队的消息会被发送到DLX)Map<String, Object> arguments = new HashMap<String, Object>(); //设置队列的最大长度 ,对头的消息会被挤出变成死信 arguments.put("x-max-length", 5);10.4 消费者拒绝消息不进行重新投递从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列。application.yml 启动手动确认spring: rabbitmq: listener: simple: acknowledge-mode: manual参考代码/** * 监听正常的那个队列的名字,不是监听那个死信队列 * 我们从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列 * * channel 消息信道(是连接下的一个消息信道,一个连接下有多个消息信息,发消息/接消息都是通过信道完成的) */ @RabbitListener(queues = {RabbitConfig.QUEUE}) public void process(Message message, Channel channel) { System.out.println("接收到的消息:" + message); //对消息不确认, ack单词是 确认 的意思 // void basicNack(long deliveryTag, boolean multiple, boolean requeue) // deliveryTag:消息的一个数字标签 // multiple:翻译成中文是多个的意思,如果是true表示对小于deliveryTag标签下的消息都进行Nack不确认,false表示只对当前deliveryTag标签的消息Nack // requeue:如果是true表示消息被Nack后,重新发送到队列,如果是false,消息被Nack后,不会重新发送到队列 try { System.out.println("deliveryTag = " + message.getMessageProperties().getDeliveryTag()); //要开启rabbitm消息消费的手动确认模式,然后才这么写代码; channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); } catch (IOException e) { e.printStackTrace(); } }10.5 消费者拒绝消息开启手动确认模式,并拒绝消息,不重新投递,则进入死信队列  参考代码:/** * 监听正常的那个队列的名字,不是监听那个死信队列 * 我们从正常的队列接收消息,但是对消息不进行确认,并且不对消息进行重新投递,此时消息就进入死信队列 * * channel 消息信道(是连接下的一个消息信道,一个连接下有多个消息信息,发消息/接消息都是通过信道完成的) */ @RabbitListener(queues = {RabbitConfig.QUEUE}) public void process(Message message, Channel channel) { System.out.println("接收到的消息:" + message); //对消息不确认, ack单词是 确认 的意思 // void basicNack(long deliveryTag, boolean multiple, boolean requeue) // deliveryTag:消息的一个数字标签 // multiple:翻译成中文是多个的意思,如果是true表示对小于deliveryTag标签下的消息都进行Nack不确认,false表示只对当前deliveryTag标签的消息Nack // requeue:如果是true表示消息被Nack后,重新发送到队列,如果是false,消息被Nack后,不会重新发送到队列 try { System.out.println("deliveryTag = " + message.getMessageProperties().getDeliveryTag()); //要开启rabbitm消息消费的手动确认模式,然后才这么写代码; channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { e.printStackTrace(); } }11.   RabbitMQ延迟队列场景:有一个订单,15分钟内如果不支付,就把该订单设置为交易关闭,那么就不能支付了,这类实现延迟任务的场景就可以采用延迟队列来实现,当然除了延迟队列来实现,也可以有一些其他办法实现;11.1 定时任务方式每隔3秒扫描一次数据库,查询过期的订单然后进行处理;优点:  简单,容易实现;缺点:  1、存在延迟(延迟时间不准确),如果你每隔1分钟扫一次,那么就有可能延迟1分钟;  2、性能较差,每次扫描数据库,如果订单量很大  1.  被动取消  当用户查询订单的时候,判断订单是否超时,超时了就取消(交易关闭);优点:  对服务器而言,压力小;缺点:  1、用户不查询订单,将永远处于待支付状态,会对数据统计等功能造成影响;  2、用户打开订单页面,有可能比较慢,因为要处理大量订单,用户体验少稍差;11.2 JDK延迟队列(单体应用,不能分布式下)DelayedQueue  无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素    优点:  实现简单,任务延迟低;缺点:  服务重启、宕机,数据丢失;  只适合单机版,不适合集群;  订单量大,可能内存不足而发生异常; oom11.3 采用消息中间件(rabbitmq)1、RabbitMQ本身不支持延迟队列,可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定,到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。  代码:正常延迟//问题? 如果先发送的消息,消息延迟时间长,会影响后面的 延迟时间段的消息的消费;//解决:不同延迟时间的消息要发到不同的队列上,同一个队列的消息,它的延迟时间应该一样  代码 延迟问题11.4 使用rabbitmq-delayed-message-exchange 延迟插件11.4.1 下载选择对应的版本下载 rabbitmq-delayed-message-exchange 插件,下载地址:http://www.rabbitmq.com/community-plugins.html  2、插件拷贝到 RabbitMQ 服务器plugins目录下11.4.2 解压unzip rabbitmq_delayed_message_exchange-3.10.2.ez如果unzip 没有安装,先安装一下yum install unzip -y11.4.3 启用插件./rabbitmq-plugins enable rabbitmq_delayed_message_exchange 开启插件;11.4.4 查询安装情况./rabbitmq-plugins list查询安装的所有插件;  重启rabbitmq使其生效;(此处也可以不重启)  消息发送后不会直接投递到队列,  而是存储到 Mnesia(嵌入式数据库),检查 x-delay 时间(消息头部);  延迟插件在 RabbitMQ 3.5.7 及以上的版本才支持,依赖 Erlang/OPT 18.0 及以上运行环境;  Mnesia 是一个小型数据库,不适合于大量延迟消息的实现  解决了消息过期时间不一致出现的问题。  参考代码:@Component @Slf4j public class RabbitConfig { public static final String _EXCHANGE = "exchange:plugin"; public static final String _QUEUE = "queue.plugin"; public static final String _KEY = "plugin"; @Bean public CustomExchange customExchange() { Map<String, Object> arguments = new HashMap<>(); arguments.put("x-delayed-type", "direct"); // CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments) return new CustomExchange(EXCHANGE, "x-delayed-message", true, false, arguments); } @Bean public Queue queue() { return QueueBuilder.durable(QUEUE).build(); } @Bean public Binding binding(CustomExchange customExchange, Queue queue) { return BindingBuilder.bind(queue).to(customExchange).with(KEY).noargs(); } }发消息参考代码MessageProperties messageProperties=new MessageProperties(); messageProperties.setHeader("x-delay",16000);String msg = "hello world"; Message message=new Message(msg.getBytes(),messageProperties); rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE, "plugin", message); log.info("发送完毕,发送时间为:{}",new Date());
0
0
0
浏览量2055

履历