【flink】 各种join类型对比

文章详细介绍了Flink中不同类型的JOIN操作,包括动态表、版本表和时态表的概念,以及RegularJoins、IntervalJoins和TemporalJoins的实现和特性。RegularJoins可能因状态膨胀而影响性能,IntervalJoins通过时间区间控制状态,TemporalJoins则支持根据事件时间关联历史版本。文章还提到了LookupJoin,用于查询外部数据丰富流的属性。

表定义

  • 动态表(dynamic table):动态表是流的另一种表达方式,动态表作为一个逻辑的抽象概念,使我们更容易理解flink中将streaming发展到table这个层次的设计,本质都是对无边界、持续变更数据的表示形式,所以动态表与流之间可以相互转换。

  • 版本表(dynamic table):动态表之上的定义,版本是一个拥有主键和时间属性的动态表(建表语句必需包含PRIMARY KEY和WATERMARK),所以版本表可以追踪每个key在某时间点/时间内的变化情况。版本表可以直接从changelog格式的source创建,或者基于append-only的源创建版本视图。

  • 时态表(temporal table):时态表是随着时间变化而变化的,也就是动态表,时态表包含一个或多个版本表的快照;当它能追踪所有记录的历史变更(来自数据库的changelog)时,就是个版本表;如果它只能表示所有记录经过物化后的最新快照(直接一个数据库表),那就是个普通表。

Regular Joins

Regular Joins是flink这么多join类型中最普通的,任意一侧的数据流有变更或者新增,都会影响到join结果。Regular joins是通过把双流的输入数据都保存在flink的状态中,存在state过度膨胀的隐患,所以我们在使用时要合理设置table状态的TTL(table.exec.state.ttl),这要结合具体的业务场景,否则会影响join结果的正确性。

有两种join类型,内连接(INNER Equi-JOIN)和外连接(OUTER Equi-JOIN),两者都只支持等值连接,且至少一个连接条件。

Interval Joins

Interval join要求至少有一个等值谓词连接和一个时间约束条件,这个时间属性定义了流的时间范围,且作为WATERMARK

ltime = rtime
ltime >= rtime AND ltime < rtime + INTERVAL ‘10’ MINUTE
ltime BETWEEN rtime - INTERVAL ‘10’ SECOND AND rtime + INTERVAL ‘5’ SECOND

  • 与Regular join一样是双流,但是它加上了时间区间的概念,可以清理状态中较旧的数据,而不会影响join结果的正确性
  • 水位线来控制join的数据区间以及清理数据,所以两个输入流都要定义WATERMARK,否则会变回Regular join
  • WATERMARK可以定义为event-time或process-time
  • 只支持append-only的输入流,当尝试使用cdc作为输入源(Retract)时出报错
Exception in thread "main" org.apache.flink.table.api.TableException: StreamPhysicalIntervalJoin doesn't support consuming update and delete changes which is produced by node TableSourceScan(table=[[ tb_order]], fields=[order_id, price, currency, order_time])

实现方式
支持datastream和sql两种方式实现Interval join,推荐使用sql来实现,

  • DataStream:入口为KeyedStream#intervalJoin,两流必须为KeyedStream,算子为IntervalJoinOperator,实现了inner join的效果,关联不上的数据将被丢弃!!!
  • sql:使用sql时,核心算子为TimeIntervalJoin两表必需都定义WATERMARK,同时支持inner join|left join、right join效果

在这里插入图片描述

Temporal Joins

与时态表的join,通过上述时态表的描述可得知,可以关联得到记录的历史版本或只能得到最新版本,flink sql遵循SQL:2011的标准语法

    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

从使用形式划分,可以分为3种:Event Time Temporal Join、Processing Time Temporal Join、Temporal Table Function Join

Event Time Temporal Join

Event Time temporal join的是一个版本表,意味着可以根据主表的事件时间关联到当时维表的具体版本。

Temporal table join currently only supports ‘FOR SYSTEM_TIME AS OF’ left table’s time attribute field
Event-Time Temporal Table Join requires both primary key and row time attribute in versioned table, but no row time attribute can be found.
Temporal table’s primary key [currency] must be included in the equivalence condition of temporal join

  • 左表必需定义事件时间,右表除了定义事件时间外,还需要定义主键(即版本表)
  • 水位线替触发join,所以两侧流都需要设置正常的水位线
  • 右表的主键必需作为等值谓词连接
  • 与regular join相比,结果不会受到右侧流影响,即输出结果以左流为主,右流只是补充了左流的信息,效果与left join相似
  • 与interval join相比,不需要定义时间窗口,即可以关联到更久之前的维度,因为版本表会保存全量维度最新版本以及上一个水位线之后的变更
  • 一个水位线之前的版本数据将会被清理,因为随着水位线的推移,这些数据将不会再被用到

Processing Time Temporal Join

Processing-time temporal join is not supported yet

从flink 1.14开始已经不再支持这种方式,可以使用temporal table function语法替换

右表定义process-time属性,总是能关联到最新的右表记录,其实这和lookup join差不多,只不过右表的记录是以HashMap全部保存着最新的版本在状态中。这种方式的强大之处在于,可以直接对接不能变成flink动态表的外部表(例如hbase)

  • 与regular joins相比,右表的数据变化不会影响到之前已经join出的结果
  • 与interval joins相比,不需要定义时间窗口,且所有旧版本数据都不会保存在状态中

Temporal Table Function Join

Join key must be the same as temporal table’s primary key

    create table if not exists tb_order (
    `order_id` int,
    `price` int,
    `currency` string,
    `order_time` timestamp(3),
    `proc_time` AS PROCTIME(),
    primary key(order_id) not enforced)
    WITH (
    'connector'='mysql-cdc',
    'table-name'='tb_order',
    )
    create table if not exists tb_currency (
    `currency` string,
    `rate` int,
    `update_time` timestamp(3),
    `proc_time` AS PROCTIME(),
    )
    WITH (
    'connector'='mysql-cdc',
    'table-name'='tb_currency',
    )
    TemporalTableFunction rate = tEnv.from("tb_currency").createTemporalTableFunction("proc_time", "currency");
    tEnv.createTemporarySystemFunction("rate",rate);
    select * from tb_order o , LATERAL TABLE(rate(o.order_time)) c where o.currency=c.currency

上面例子实现了process time temporal join,两建表语句都不指定事件时间,且tb_currency无需指定primary key(即非版本表),但是在定义TemporalTableFunction可以指定任意字段为主键,所以如果建表语句指定了事件时间,且TemporalTableFunction也使用事件时间,那么相当于间接创建了版本表。

  • 先要定义table funtion,指定一个时间属性(event-time或process-time)和主键
  • TemporalTableFunction定义的主键必须作为等值谓词连接
  • 除了可以和版本表join,还能和普通的表示最新版本的表/视图join,即它包含了event-time/processing-time temporal join两种

Lookup Join

通过查询外部存储系统的数据以丰富流的属性,这种join方式要求流表必须有一个processing time属性,外部数据表的connector要实现LookupTableSource接口

    CREATE TEMPORARY TABLE Customers (
      id INT,
      name STRING,
      country STRING,
      zip STRING
    ) WITH (
      'connector' = 'jdbc',
      '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;
  • 外部表的数据变化不会影响到已经join出的结果
  • 上面所有join都是双流,而lookup join是单流
<think> 嗯,用户想了解Flink SQL实时双流join的实现方法、原理及案例。从用户提供的引用来看,已经有四个相关引用信息,需要从中提取关键点。 用户提到的引用[1]中介绍了Flink SQL三种join方式,特别指出双流join推荐使用interval join。引用[2]强调需要考虑数据延迟问题,需要用到水印、窗口等概念,还提到Join和coGroup两种算子。引用[3]说明双流join需要永久保持状态,可能无限增长,提到了状态TTL配置。引用[4]则讨论了JOIN历史维表的原理和优化措施。 用户上次询问的是flinksql进行实时双流join的内容,这次具体询问实现方法、原理及案例。需要结合这些引用信息来组织回答。 首先考虑介绍Interval Join作为核心实现方式,因为引用[1][3]都重点提到了它。需要说明其时间窗口约束机制,这是解决数据延迟的关键。然后准备说明Temporal Join作为补充方案,适合维表关联场景。 关于原理部分,引用[2][3][4]提供了重要线索。状态管理机制需要着重解释,特别是状态无限增长的风险和TTL的解决方案。事件时间语义和水印机制也很关键,它们共同解决了乱序问题。异步IO优化这点来自引用[4]的维表优化经验。 案例部分考虑设计两个典型场景:订单-支付流水匹配和广告曝光-点击分析。这两个案例能很好展示Interval JoinTemporal Join的应用差异。示例代码要包含完整SQL结构,特别是时间属性和水印定义这些易错点。 还需要补充优化建议,比如状态管理和异步IO这些实战要点。最后的问题设计要引导用户深入思考:对比传统数据库区别、状态管理实践、维表优化方案和水印配置细节,这样能覆盖用户可能遇到的进阶问题。 </think> 好的,我们来详细探讨 Flink SQL 实现实时双流 JOIN 的实现方法、核心原理以及应用案例。 ### 1. Flink SQL 实时双流 JOIN 的实现方法 Flink SQL 主要提供以下几种方式实现实时双流 JOIN,各有其适用场景: 1. **`INTERVAL JOIN` (时间区间 JOIN - 最常用)** * **原理:** 基于事件时间,将两条流中落在特定时间窗口内的数据进行关联。 * **语法:** ```sql SELECT ... FROM stream1 [AS t1] [LEFT/RIGHT/FULL] JOIN stream2 [AS t2] ON t1.key = t2.key AND t1.event_time BETWEEN t2.event_time - INTERVAL 'lower_bound' AND t2.event_time + INTERVAL 'upper_bound' ``` * **特点:** * 必须指定**事件时间属性字段** (`t1.event_time`, `t2.event_time`)。 * 使用 `BETWEEN ... AND ...` 精确限定两条流中记录可匹配的最大时间差范围 (`lower_bound` 和 `upper_bound`)。例如,`t1.event_time BETWEEN t2.event_time - INTERVAL '5' MINUTE AND t2.event_time + INTERVAL '1' MINUTE` 表示 `t1` 的记录可以与 `t2` 记录关联,只要 `t1` 的事件时间落在 `t2` 事件时间的前5分钟到后1分钟之内。 * 解决乱序问题依赖**水印 (Watermark)** 机制。 * **状态保留时间可控:** Flink 只需保留时间区间(`lower_bound + upper_bound`)内的状态,之后的状态可以清除。状态大小由时间窗口宽度和该时间段内的键值数量决定。 * 支持 `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL OUTER JOIN`。 * **适用场景:** 两条流中相关联的事件在时间上具有**有限偏差**(如订单创建事件与支付事件通常在几分钟内发生)。 2. **`TEMPORAL TABLE JOIN` (时态表 JOIN - 适用于流与维表,也可模拟双流)** * **原理:** 将其中一条流(通常是更新较慢或作为维度源的流)视为一个**随时间变化的表 (Temporal Table / Versioned Table)**,另一条流在事件时间点上查询该时态表的对应版本。 * **语法:** ```sql SELECT ... FROM stream1 [AS t1] LEFT JOIN DimensionTable FOR SYSTEM_TIME AS OF t1.event_time [AS dim] ON t1.key = dim.key ``` * **特点:** * 需要定义 `DimensionTable` 为时态表。 * 使用 `FOR SYSTEM_TIME AS OF t1.event_time` 指定在 `t1` 的事件时间点去查询 `DimensionTable` 的快照。 * 解决乱序问题同样依赖**水印 (Watermark)**。 * **状态保留:** 对于作为时态表的那条流,Flink 需要维护其所有版本的历史(直到配置的状态 TTL)。状态大小可能增长较快,需要谨慎设置 TTL。 * **适用场景:** * **主流与维表JOIN:** 这是其最典型的场景。 * **模拟双流JOIN:** 当其中一条流更新较慢(如用户资料变更流)、或者你需要查询另一条流在特定时间点的状态时,可以将这条流定义为时态表,让另一条流去关联它。本质上是将一条动态流“冻结”为另一条流事件时间点的快照。 3. **`Regular JOIN` (常规 JOIN - 需谨慎使用)** * **原理:** 类似于批处理的 JOIN,理论上会保留所有历史记录,尝试匹配所有可能的键值对。 * **语法:** 标准 SQL JOIN 语法。 * **特点:** * 需要**水印 (Watermark)** 和**状态 TTL** 来管理状态。 * **状态可能无限增长:** 默认情况下,Flink 会永久保留所有输入记录(左流和右流)的状态,以便与将来到达的记录进行匹配。这极易导致状态爆炸。 * **必须配置状态 TTL:** 为了避免状态无限增长,**必须**通过 `table.exec.state.ttl` 参数为JOIN操作的状态设置一个生存时间 (TTL)。超过TTL的状态会被清理。**但这会影响结果的正确性**,因为清理掉的旧状态无法再与新到达的记录进行匹配。 * **适用场景:** 需要捕获任意时间点的关联(如需要关联很久以前的一条记录),且能接受由于状态TTL清理导致的部分关联缺失(即结果不完全准确)的场景。**一般对于双流JOIN,不推荐首选此方式。** ### 2. Flink SQL 实时双流 JOIN 的核心原理 实现实时双流 JOIN 的关键挑战在于处理**无界数据流、事件乱序到达和状态管理**。Flink SQL 主要依赖以下机制: 1. **事件时间语义 (Event Time) & 水印机制 (Watermark):** * 每条记录携带一个**事件时间戳** (`event_time`),表示业务实际发生的时间。 * **水印是一种特殊的时间戳**,表示在该时间点之前的所有数据(事件时间 <= Watermark)**理论上**都已到达系统。水印由源算子生成并随数据流向下游传播。 * **作用:** 水印是推动**事件时间**前进的时钟,是处理乱序数据的基石。它告诉算子“在某个时间点之前的数据应该都到齐了”,算子可以进行计算并清理早于水印的状态(在允许的延迟范围内)。这对于 `INTERVAL JOIN` 和 `TEMPORAL JOIN` 决定何时输出结果和清理状态至关重要[^2][^3]。 2. **状态管理 (State Management):** * Flink 是一个有状态的流处理引擎。 * **JOIN 状态:** 执行双流 JOIN 的算子(如 `JoinOperator`)需要在其状态后端中存储: * **左流缓冲区:** 存储左流中尚未匹配到的记录(按 Key 和 Event Time 组织)。 * **右流缓冲区:** 存储右流中尚未匹配到的记录(按 Key 和 Event Time 组织)。 * **状态生命周期:** * 对于 `INTERVAL JOIN`:状态根据时间区间边界 (lower_bound + upper_bound) 和水印自动清理。当水印推进超过某条记录的 `event_time + upper_bound`(或 `event_time - lower_bound`)时,该记录的状态可以被安全清除,因为它已不可能再与未来到达的任何记录匹配。 * 对于 `TEMPORAL JOIN` (作为维表的一方):需要保留历史版本直到状态 TTL 到期。 * 对于 `Regular JOIN`:依赖配置的 `table.exec.state.ttl` 来控制状态存活时间,超时则清理,但会损失准确性[^3]。 3. **基于 Key 的分区:** * 双流 JOIN 操作通常在 `ON` 条件的 Key(如 `t1.order_id = t2.order_id`)上进行。Flink 会将具有相同 Key 的两条流的数据**路由到同一个物理Task实例 (TaskManager Slot 上的并行子任务)** 上执行 JOIN 操作。这是保证正确关联的基础。 4. **乱序处理与延迟数据处理:** * 水印机制允许定义**允许的最大延迟 (Allowed Lateness)**。在窗口操作(`INTERVAL JOIN` 隐式定义了一个时间窗口)中,即使水印已推进超过窗口结束时间 + 允许延迟,系统仍会为延迟到达但事件时间仍落在该窗口 + 延迟期内的数据保留状态并尝试关联,然后更新之前的结果或产生延迟结果。这提高了处理现实世界数据延迟的鲁棒性。 5. **维表 JOIN 优化 (针对 `TEMPORAL TABLE JOIN` 场景):** * **提高吞吐/降低数据库压力:** * **异步 I/O:** 避免同步查询维表数据库阻塞整个数据处理管道。 * **本地缓存 (Cache):** 在 Flink 算子本地(如 LRU Cache)缓存常用的维表数据条目,减少远程数据库查询次数。 * **批量读取 (Batching):** 对短时间内相同 Key 的查询或不同 Key 的查询进行攒批,减少数据库请求次数(需数据库支持批量查询)。 * **旁路缓存 (External Cache):** 使用 Redis 等作为维表数据的二级缓存[^4]。 ### 3. 应用案例 **案例 1:订单支付流水匹配 (`INTERVAL JOIN`)** * **场景:** 实时关联订单创建流 (`order_stream`) 和支付成功流 (`payment_stream`),判断订单是否在规定时间内支付。 * **需求:** 订单创建后,期望在 30 分钟内完成支付。 * **Flink SQL 实现:** ```sql -- 定义订单流 (假设已定义时间属性和水印) CREATE TABLE order_stream ( order_id STRING, user_id BIGINT, amount DECIMAL(10, 2), order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND -- 容忍5秒乱序 ) WITH (...); -- 定义支付流 CREATE TABLE payment_stream ( payment_id STRING, order_id STRING, pay_amount DECIMAL(10, 2), pay_time TIMESTAMP(3), WATERMARK FOR pay_time AS pay_time - INTERVAL '5' SECOND -- 容忍5秒乱序 ) WITH (...); -- INTERVAL JOIN 查询 (内连接,只找出30分钟内支付的订单) SELECT o.order_id, o.user_id, o.amount, o.order_time, p.payment_id, p.pay_time, TIMESTAMPDIFF(MINUTE, o.order_time, p.pay_time) AS pay_duration_min FROM order_stream o JOIN payment_stream p ON o.order_id = p.order_id AND p.pay_time BETWEEN o.order_time AND o.order_time + INTERVAL '30' MINUTE; ``` * **解释:** 该查询将输出所有 `order_id` 相同,且支付时间 (`pay_time`) 落在订单创建时间 (`order_time`) 之后的 30 分钟内的订单及其支付记录。水印机制确保延迟不超过 5 秒的数据也能被正确处理。Flink 只需保留最多约 `30m + 5s` 时间范围内的状态。 **案例 2:广告曝光与点击分析 (`TEMPORAL TABLE JOIN` 模拟双流)** * **场景:** 一条是广告曝光流 (`impression_stream`),记录广告被展示;另一条是用户点击流 (`click_stream`),记录用户点击广告。目标是实时统计曝光后最终被点击的情况(无论是立即点击还是延迟点击)。 * **需求:** 将点击流关联到其对应的最新一次曝光上(一条曝光可能被同一个用户多次看到或点击,这里简化)。 * **Flink SQL 实现:** ```sql -- 定义曝光流 (作为主流) CREATE TABLE impression_stream ( impression_id STRING, user_id BIGINT, ad_id STRING, impression_time TIMESTAMP(3), WATERMARK FOR impression_time AS impression_time - INTERVAL '10' SECOND ) WITH (...); -- 定义点击流,并将其定义为时态表 (注意主键和时间属性) CREATE TABLE click_stream ( click_id STRING, impression_id STRING, -- 假设点击知道对应哪个曝光ID user_id BIGINT, click_time TIMESTAMP(3), WATERMARK FOR click_time AS click_time - INTERVAL '10' SECOND, PRIMARY KEY (impression_id) NOT ENFORCED -- 声明主键,Flink用于版本管理 ) WITH (...); -- 将click_stream定义为时态表 CREATE VIEW versioned_click AS SELECT click_id, impression_id, user_id, click_time FROM click_stream; -- TEMPORAL JOIN 查询 SELECT i.impression_id, i.user_id, i.ad_id, i.impression_time, c.click_id, c.click_time FROM impression_stream i LEFT JOIN versioned_click FOR SYSTEM_TIME AS OF i.impression_time AS c ON i.impression_id = c.impression_id; ``` * **解释:** 1. 我们将 `click_stream` 通过视图 `versioned_click` 定义为时态表(需要主键 `impression_id`)。 2. 对于 `impression_stream` 中的每一条曝光记录,在它的事件时间点 (`impression_time`),查询 `versioned_click` 表,找出具有相同 `impression_id` 的**最新一条**点击记录(截至 `impression_time`)。 3. `LEFT JOIN` 确保所有曝光都会被输出,即使当时还没有点击(`click_*` 字段为 `NULL`)。如果在该曝光时间点之后发生了点击,后续到达的点击事件会被 `versioned_click` 表更新,但**不会**改变之前输出的那条曝光记录的结果(因为查询的是 `AS OF impression_time` 的快照)。如果需要追踪后续点击,通常需要将曝光和点击都写入一个可更新的存储,然后在后续查询中关联。 4. 状态管理:Flink 需要维护 `click_stream` (`versioned_click` 表) 的所有历史版本,直到状态 TTL 到期(通过 `table.exec.state.ttl` 配置)。 ### 总结与建议 * **首选 `INTERVAL JOIN`:** 对于绝大多数需要限定时间偏差的双流 JOIN(如订单-支付、登录-操作、传感器事件关联),`INTERVAL JOIN` 是**最推荐**的方式,因为它状态可控、语义清晰。 * **`TEMPORAL TABLE JOIN` 适用维表和慢变流:** 主流与维度表 JOIN 是其核心场景。用于双流 JOIN 时,适用于其中一条流更新较慢或需要查询历史快照的场景,但要注意状态增长问题。 * **慎用 `Regular JOIN`:** 除非你明确需要关联任意时间点的数据且能接受 TTL 带来的不准确性,否则尽量避免。务必配置合理的 `table.exec.state.ttl`。 * **关键配置:** * **水印生成策略:** 根据数据延迟情况合理设置水印延迟(如 `WATERMARK FOR ts AS ts - INTERVAL '5' SECOND`)。 * **状态 TTL (`table.exec.state.ttl`):** 对于 `TEMPORAL JOIN` (作为时态表的一方) 和 `Regular JOIN` **极其重要**,用于控制状态保留时长(如 `'1 hour'`)。`INTERVAL JOIN` 的状态生命周期由时间区间自动管理。 * **允许延迟 (Allowed Lateness):** 在 `INTERVAL JOIN` 的窗口语义下,配合侧输出可以捕获超过水印的延迟数据(示例SQL未体现,需在Table API中配置)。 * **维表JOIN优化:** 如果使用 `TEMPORAL TABLE JOIN` 关联外部数据库维表,务必启用**异步 I/O** 和**缓存** (`'lookup.async' = 'true'`, `'lookup.cache' = 'PARTIAL'` 等参数) 以大幅提升性能和减轻数据库压力[^4]。 通过合理选择 JOIN 类型、正确配置时间属性和水印、管理好状态生命周期并结合必要的优化手段,Flink SQL 能够高效、准确地实现各种复杂的实时双流 JOIN 需求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值