Apache Flink 漫談系列 - Time Interval JOIN

說什麼

JOIN 算子是數據處理的核心算子,前面我們在《Apache Flink 漫談系列 - JOIN 算子》介紹了UnBounded的雙流JOIN,在《Apache Flink 漫談系列 - JOIN LATERAL》介紹了單流與UDTF的JOIN操作,在《Apache Flink 漫談系列 - Temporal Table JOIN》又介紹了單流與版本表的JOIN,本篇將介紹在UnBounded數據流上按時間維度進行數據劃分進行JOIN操作 - Time Interval(Time-windowed)JOIN, 後面我們叫做Interval JOIN。

實際問題

前面章節我們介紹了Flink中對各種JOIN的支持,那麼想想下面的查詢需求之前介紹的JOIN能否滿足?需求描述如下:

比如有一個訂單表Orders(orderId, productName, orderTime)和付款表Payment(orderId, payType, payTime)。假設我們要統計下單一小時內付款的訂單信息。

傳統數據庫解決方式

在傳統劉數據庫中完成上面的需求非常簡單,查詢sql如下::

1
2
3
4
5
6
7
8
9
SELECT
  o.orderId,
  o.productName,
  p.payType,
  o.orderTime,
  payTime
FROM
  Orders AS o JOIN Payment AS p ON
  o.orderId = p.orderId AND p.payTime >= orderTime AND p.payTime < orderTime + 3600 // 秒

上面查詢可以完美的完成查詢需求,那麼在Apache Flink裏面應該如何完成上面的需求呢?

Apache Flink解決方式

UnBounded 雙流 JOIN

上面查詢需求我們很容易想到利用《Apache Flink 漫談系列(09) - JOIN 算子》介紹了UnBounded的雙流JOIN,SQL語句如下:

1
2
3
4
5
6
7
8
9
SELECT
   o.orderId,
   o.productName,
   p.payType,
   o.orderTime,
   payTime
 FROM
   Orders AS o JOIN Payment AS p ON
   o.orderId = p.orderId AND p.payTime >= orderTime AND p.payTime as timestamp < TIMESTAMPADD(SECOND, 3600, orderTime)

UnBounded雙流JOIN可以解決上面問題,這個示例和本篇要介紹的Interval JOIN有什麼關係呢?

性能問題

雖然我們利用UnBounded的JOIN能解決上面的問題,但是仔細分析用戶需求,會發現這個需求場景訂單信息和付款信息並不需要長期存儲,比如2018-12-27 14:22:22的訂單隻需要保持1小時,因爲超過1個小時的訂單如果沒有被付款就是無效訂單了。同樣付款信息也不需要長期保持,2018-12-27 14:22:22的訂單付款信息如果是2018-12-27 15:22:22以後到達的那麼我們也沒有必要保存到State中。而對於UnBounded的雙流JOIN我們會一直將數據保存到State中,如下示意圖:

這樣的底層實現,對於當前需求有不必要的性能損失。所以我們有必要開發一種新的可以清除State的JOIN方式(Interval JOIN)來高性能的完成上面的查詢需求。

功能擴展

目前的UnBounded的雙流JOIN是後面是沒有辦法再進行Event-Time的Window Aggregate的。也就是下面的語句在Apache Flink上面是無法支持的:

1
2
3
4
5
6
7
SELECT COUNT(*) FROM (
 SELECT
  ...,
  payTime
  FROM Orders AS o JOIN Payment AS p ON
   o.orderId = p.orderId
 ) GROUP BY TUMBLE(payTime, INTERVAL '15' MINUTE)

因爲在UnBounded的雙流JOIN中無法保證payTime的值一定大於WaterMark(WaterMark相關可以查閱<<Apache Flink 漫談系列(03) - Watermark>>). Apache Flink的Interval JOIN之後可以進行Event-Time的Window Aggregate。

Interval JOIN

爲了完成上面需求,並且解決性能和功能擴展的問題,Apache Flink在1.4開始開發了Time-windowed Join,也就是本文所說的Interval JOIN。接下來我們詳細介紹Interval JOIN的語法,語義和實現原理。

什麼是Interval JOIN

Interval JOIN 相對於UnBounded的雙流JOIN來說是Bounded JOIN。就是每條流的每一條數據會與另一條流上的不同時間區域的數據進行JOIN。對應Apache Flink官方文檔的 Time-windowed JOIN(release-1.7之前都叫Time-Windowed JOIN)。 

Interval JOIN 語法

1
SELECT ... FROM t1 JOIN t2  ON t1.key = t2.key AND TIMEBOUND_EXPRESSION

TIMEBOUND_EXPRESSION 有兩種寫法,如下:

  • L.time between LowerBound(R.time) and UpperBound(R.time)

  • R.time between LowerBound(L.time) and UpperBound(L.time)

  • 帶有時間屬性(L.time/R.time)的比較表達式。

Interval JOIN 語義

Interval JOIN 的語義就是每條數據對應一個 Interval 的數據區間,比如有一個訂單表Orders(orderId, productName, orderTime)和付款表Payment(orderId, payType, payTime)。假設我們要統計在下單一小時內付款的訂單信息。SQL查詢如下:

1
2
3
4
5
6
7
8
9
10
11
SELECT
  o.orderId,
  o.productName,
  p.payType,
  o.orderTime,
  cast(payTime as timestamp) as payTime
FROM
  Orders AS o JOIN Payment AS p ON
  o.orderId = p.orderId AND
  p.payTime BETWEEN orderTime AND
  orderTime + INTERVAL '1' HOUR
  • Orders訂單數據

orderIdproductNameorderTime
001iphone2018-12-26 04:53:22.0
002mac2018-12-26 04:53:23.0
003book2018-12-26 04:53:24.0
004cup2018-12-26 04:53:38.0
  • Payment付款數據

orderIdpayTypepayTime
001alipay2018-12-26 05:51:41.0
002card2018-12-26 05:53:22.0
003card2018-12-26 05:53:30.0
004alipay2018-12-26 05:53:31.0

符合語義的預期結果是 訂單id爲003的信息不出現在結果表中,因爲下單時間2018-12-26 04:53:24.0, 付款時間是 2018-12-26 05:53:30.0超過了1小時付款。
那麼預期的結果信息如下:

orderIdproductNamepayTypeorderTimepayTime
001iphonealipay2018-12-26 04:53:22.02018-12-26 05:51:41.0
002maccard2018-12-26 04:53:23.02018-12-26 05:53:22.0
004cupalipay2018-12-26 04:53:38.02018-12-26 05:53:31.0

這樣Id爲003的訂單是無效訂單,可以更新庫存繼續售賣。

接下來我們以圖示的方式直觀說明Interval JOIN的語義,我們對上面的示例需求稍微變化一下: 訂單可以預付款(不管是否合理,我們只是爲了說明語義)也就是訂單 前後 1小時的付款都是有效的。SQL語句如下:

1
2
3
4
5
6
7
SELECT
  ...
FROM
  Orders AS o JOIN Payment AS p ON
  o.orderId = p.orderId AND
  p.payTime BETWEEN orderTime - INTERVAL '1' HOUR AND
  orderTime + INTERVAL '1' HOUR

這樣的查詢語義示意圖如下:

上圖有幾個關鍵點,如下:

  • 數據JOIN的區間 - 比如Order時間爲3的訂單會在付款時間爲[2, 4]區間進行JOIN。

  • WaterMark - 比如圖示Order最後一條數據時間是3,Payment最後一條數據時間是5,那麼WaterMark是根據實際最小值減去UpperBound生成,即:Min(3,5)-1 = 2

  • 過期數據 - 出於性能和存儲的考慮,要將過期數據清除,如圖當WaterMark是2的時候時間爲2以前的數據過期了,可以被清除。

Interval JOIN 實現原理

由於Interval JOIN和雙流JOIN類似都要存儲左右兩邊的數據,所以底層實現中仍然是利用State進行數據的存儲。流計算的特點是數據不停的流入,我們可以不停的進行增量計算,也就是我們每條數據流入都可以進行JOIN計算。我們還是以具體示例和圖示來說明內部計算邏輯,如下圖:

簡單解釋一下每條記錄的處理邏輯如下:

實際的內部邏輯會比描述的複雜的多,大家可以根據如上簡述理解內部原理即可。

示例代碼

我們還是以訂單和付款示例,將完整代碼分享給大家,具體如下(代碼基於flink-1.7.0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import java.sql.Timestamp

import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.table.api.TableEnvironment
import org.apache.flink.table.api.scala._
import org.apache.flink.types.Row

import scala.collection.mutable

object SimpleTimeIntervalJoin {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val tEnv = TableEnvironment.getTableEnvironment(env)
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    // 構造訂單數據
    val ordersData = new mutable.MutableList[(String, String, Timestamp)]
    ordersData.+=(("001", "iphone", new Timestamp(1545800002000L)))
    ordersData.+=(("002", "mac", new Timestamp(1545800003000L)))
    ordersData.+=(("003", "book", new Timestamp(1545800004000L)))
    ordersData.+=(("004", "cup", new Timestamp(1545800018000L)))

    // 構造付款表
    val paymentData = new mutable.MutableList[(String, String, Timestamp)]
    paymentData.+=(("001", "alipay", new Timestamp(1545803501000L)))
    paymentData.+=(("002", "card", new Timestamp(1545803602000L)))
    paymentData.+=(("003", "card", new Timestamp(1545803610000L)))
    paymentData.+=(("004", "alipay", new Timestamp(1545803611000L)))
    val orders = env
      .fromCollection(ordersData)
      .assignTimestampsAndWatermarks(new TimestampExtractor[String, String]())
      .toTable(tEnv, 'orderId, 'productName, 'orderTime.rowtime)
    val ratesHistory = env
      .fromCollection(paymentData)
      .assignTimestampsAndWatermarks(new TimestampExtractor[String, String]())
      .toTable(tEnv, 'orderId, 'payType, 'payTime.rowtime)

    tEnv.registerTable("Orders", orders)
    tEnv.registerTable("Payment", ratesHistory)

    var sqlQuery =
      """
        |SELECT
        |  o.orderId,
        |  o.productName,
        |  p.payType,
        |  o.orderTime,
        |  cast(payTime as timestamp) as payTime
        |FROM
        |  Orders AS o JOIN Payment AS p ON o.orderId = p.orderId AND
        | p.payTime BETWEEN orderTime AND orderTime + INTERVAL '1' HOUR
        |""".stripMargin
    tEnv.registerTable("TemporalJoinResult", tEnv.sqlQuery(sqlQuery))

    val result = tEnv.scan("TemporalJoinResult").toAppendStream[Row]
    result.print()
    env.execute()
  }

}

class TimestampExtractor[T1, T2]
  extends BoundedOutOfOrdernessTimestampExtractor[(T1, T2, Timestamp)](Time.seconds(10)) {
  override def extractTimestamp(element: (T1, T2, Timestamp)): Long = {
    element._3.getTime
  }
}

運行結果如下:

小節

本篇由實際業務需求場景切入,介紹了相同業務需求既可以利用Unbounded 雙流JOIN實現,也可以利用Time Interval JOIN來實現,Time Interval JOIN 性能優於UnBounded的雙流JOIN,並且Interval JOIN之後可以進行Window Aggregate算子計算。然後介紹了Interval JOIN的語法,語義和實現原理,最後將訂單和付款的完整示例代碼分享給大家。期望本篇能夠讓大家對Apache Flink Time Interval JOIN有一個具體的瞭解!

最後

想買正版書的小夥伴,可以使用小程序,噹噹羊毛????,一年兩次機會不能錯過。

優惠碼:【Q2WHXE】(實際支付200減30)

使用渠道:噹噹小程序

使用時間:4.10-4.15   4.20-4.23

本活動滿減與禮券均不支持團購,同一賬號、同一地址、同一手機號、同一IP反覆購買本活動商品,噹噹有權取消訂單,終結交易千萬別多次下單。

羊毛雖好,一定要注意上面⬆️的規則

我買六本,書的紙張非常清晰,看起來很舒服,用了優惠碼接近4折多

推薦8本書????,當然你們也可以搜索自己喜歡的書籍,一定要注意"書香節"的書纔會參與????️100減50活動,今天好多粉絲私信我說買的書沒有半價


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章