Window Join(窗口连接)
窗口连接是指将同一个窗口中的两个流数据通过同一个key连接起来。这些窗口可以使用Window Assigner定义,并根据两个流中的数据进行计算。
连接两边的数据被传给一个用户定义好的JoinFunction或者FlatJoinFunction,将满足条件的结果数据输出来。
通用用法总结如下:
stream.join(otherStream)
.where(<KeySelector>)
.equalTo(<KeySelector>)
.window(<WindowAssigner>)
.apply(<JoinFunction>)
语法上的一些注意点:
- 两个流元素的成对组合的创建就像是内连接,这意味着如果一个流中的元素没有来自另一个流的相应元素要连接,则不会输出它们。
- 那些连接上的数据会将窗口的最大时间戳作为他们的时间戳。例如一个[5, 10)的窗口,将会导致连接上的数据将9作为他们的时间戳。
接下来的部分我们将通过例子大概介绍一下不同的join类型是如何工作的。
Tumbling Window Join(滚动窗口连接)
当使用滚动窗口连接时,同一个滚动窗口内的相同join key的数据会被连接成对并且传递给一个JoinFunction或者FlatJoinFunction。因为这就像内连接,因此滚动窗口内一个流的数据在另一个流中没有相对应的数据时将不会被输出。
就像图中所描述的,我们定义一个2毫秒的滚动窗口,形成的窗口诸如[0,1], [2,3] …。该图显示了每个窗口中所有数据的联队组合,这些数据将传递给JoinFunction。注意在滚动窗口[6,7],什么都不会输出,因为在绿色的窗口中没有数据可以和橙色的窗口中的数据连接。
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
...
DataStream<Integer> orangeStream = ...
DataStream<Integer> greenStream = ...
orangeStream.join(greenStream)
.where(<KeySelector>)
.equalTo(<KeySelector>)
.window(TumblingEventTimeWindows.of(Time.milliseconds(2)))
.apply (new JoinFunction<Integer, Integer, String> (){
@Override
public String join(Integer first, Integer second) {
return first + "," + second;
}
});
Sliding Window Join(滑动窗口)
当使用滑动窗口连接时,同一个滚动窗口内的相同join key的数据会被连接成对并且传递给一个JoinFunction或者FlatJoinFunction。当前滑动窗口中,在一个窗口总存在而在另一个窗口中不存在的数据将不会被输出。注意一些数据可能在一个滑动窗口中可以被连接上,但是在另一个窗口却连接不上。
在这个例子中,我们使用一个每毫秒滑动一次的2毫秒窗口,会生成诸如[-1, 0],[0,1],[1,2],[2,3]…的窗口。X轴下面的数据是会在每个滑动窗口内被传递给JoinFunction的。这里你也可以看到,比如,在窗口[2,3]橙色的2是如何跟绿色的3连接的,然而在窗口[1,2]中2却没有连接上。
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
...
DataStream<Integer> orangeStream = ...
DataStream<Integer> greenStream = ...
orangeStream.join(greenStream)
.where(<KeySelector>)
.equalTo(<KeySelector>)
.window(SlidingEventTimeWindows.of(Time.milliseconds(2) /* size */, Time.milliseconds(1) /* slide */))
.apply (new JoinFunction<Integer, Integer, String> (){
@Override
public String join(Integer first, Integer second) {
return first + "," + second;
}
});
Session Window Join(会话窗口连接)
当使用会话窗口连接时,如果连接满足会话条件,则所有的相同join key的数据被成对连接并传递给了JoinFunction和FlatJoinFunction。这次又是一个内连接,因此如果只有一个会话窗口包含数据而另一个不包含,就不会有输出。
这里我们定义一个每个会话被至少1ms的gap分割的会话窗口连接。这里有三个会话,头两个会话中两个流中的连接数据被传递给了JoinFunction。在第三个会话中,在绿色的流中没有数据,因此8和9不会被连接。
``
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.windowing.assigners.EventTimeSessionWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
DataStream<Integer> orangeStream = ...
DataStream<Integer> greenStream = ...
orangeStream.join(greenStream)
.where(<KeySelector>)
.equalTo(<KeySelector>)
.window(EventTimeSessionWindows.withGap(Time.milliseconds(1)))
.apply (new JoinFunction<Integer, Integer, String> (){
@Override
public String join(Integer first, Integer second) {
return first + "," + second;
}
});
Interval Join(间隔连接)
间隔连接通过一个共同的key连接两个流(A&B)中的数据,流 B 的数据具有时间戳位于流 A 中元素的相对时间间隔中。
这也可以更正式地表达成 b.timestamp ∈ [a.timestamp + lowerBound; a.timestamp + upperBound] 或者 a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound
a和b是A,B流中有共同key的数据。只要满足下界(lowerBound)小于等于上界(upperBound),下界和上界都是可以是正数或者负数。间隔连接目前只支持内连接。
当一对数据被传递给了ProcessJoinFunction,他们会被指定为两个数据中更大的时间戳(可以通过ProcessJoinFunction.Context访问)。
间隔连接目前只支持事件时间。
在上面的例子中,我们连接两个‘绿色’和‘橙色’的数据流,下界是-2ms,上界是1ms。默认情况下,这些界限是包含在内的,但是可以通过使用lowerBoundExclusive() 或者upperBoundExclusive()函数修改。
再次使用更正式的形式,可以将图中的三角形表示为:
orangeElem.ts + lowerBound <= greenElem.ts <= orangeElem.ts + upperBound
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.streaming.api.functions.co.ProcessJoinFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
...
DataStream<Integer> orangeStream = ...
DataStream<Integer> greenStream = ...
orangeStream
.keyBy(<KeySelector>)
.intervalJoin(greenStream.keyBy(<KeySelector>))
.between(Time.milliseconds(-2), Time.milliseconds(1))
.process (new ProcessJoinFunction<Integer, Integer, String(){
@Override
public void processElement(Integer left, Integer right, Context ctx, Collector<String> out) {
out.collect(first + "," + second);
}
});