一、joinedStream与coGroupedStream简介
在实际的流计算中,我们经常会遇到多个流进行join的情况,Flink提供了2个Transformations来实现。
如下图:
注意:Join(Cogroups) two data streams on a given key and a common window。这里很明确了,我们要在2个DataStream中指定连接的key以及window下来运算。
二、SQL比较
我们最熟悉的SQL语言中,如果想要实现2个表join,可以如下实现:
select T1.* , T2.*
from T1
join T2 on T1.key = T2.key;
这个SQL是一个inner join的形式。稍微复杂点的带有group by与order by的SQL如下:
select T1.key , sum(T2.col)
from T1
join T2 on T1.key = T2.key
group by T1.key
order by T1.col;
通过这2个SQL,我们想要在Flink中实现实时的流计算,就可以通过joinedStream或coGroupedStream来实现。但是在join之后实施更复杂的运算,例如判断、迭代等,仅仅通过SQL实现,恐怕会很麻烦,只能通过PL/SQL块来实现,而Flink提供了apply方法,用户可以自己编写复杂的函数来实现。
三、join与coGroup的区别
先来看下源码中提供的类与方法比较下:
1、join
通过结构可以发现,在JoinedStreams提供了where方法,在where类中提供了equalTo方法,下一层就是window,之后是trigger、evictor以及apply方法。
这里贴出一个代码模板共参考:
* val one: DataStream[(String, Int)] = ...
* val two: DataStream[(String, Int)] = ...
*
* val result = one.join(two)
* .where {t => ... }
* .equal {t => ... }
* .window(TumblingEventTimeWindows.of(Time.of(5, TimeUnit.SECONDS)))
* .apply(new MyJoinFunction())
2、coGroup
仔细观察我们发现,实现上与join几乎一样,唯一的区别在于apply方法提供的参数类型。
代码模板如下:
* val one: DataStream[(String, Int)] = ...
* val two: DataStream[(String, Int)] = ...
*
* val result = one.coGroup(two)
* .where(new MyFirstKeySelector())
* .equalTo(new MyFirstKeySelector())
* .window(TumblingEventTimeWindows.of(Time.of(5, TimeUnit.SECONDS)))
* .apply(new MyCoGroupFunction())
3、区别
刚才提到的apply方法中的参数类型不一样,join中提供的apply方法,参数是T1与T2这2种数据类型,对应到SQL中就是T1.* 与 T2.*的一行数据。而coGroup中提供的apply方法,参数是Iterator[T1]与Iterator[2]这2种集合,对应SQL中类似于Table[T1]与Table[T2]。
基于这2种方式,如果我们的逻辑不仅仅是对一条record做处理,而是要与上一record或更复杂的判断与比较,甚至是对结果排序,那么join中的apply方法显得比较困难。
四、程序实践
下面开始实际演示程序的编写与代码的打包并发布到集群,最后输出结果的一步步的过程。
说明:由于是双流,我模拟Kafka的Topic,自定义了2个socket,其中一个指定“transaction”的实时交易输入流,另一个socket指定“Market”的快照输入流,原则上每3秒(时间戳)生成1个快照。
1、join
由于是2个DataStream,且我的逻辑是要根据各自流产生的时间戳去限制window,因此这里要对2个流都分配时间戳并emit水位线(采用EventTime):
val eventMarketStream = marketDataStream.assignAscendingTimestamps(_._2)
val eventTransactionStream = transactionDataStream.assignAscendingTimestamps(_._2)
join操作后,apply方法接收的只是T1与T2的类型:
val joinedStreams = eventTransactionStream
.join(eventMarketStream)
.where(_._1)
.equalTo(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
.apply{
(t1 : (Long, Long, Long, Long), t2 : (Long, Long, Long), out : Collector[(Long,String,String,Long,Long,Long)]) =>
val transactionTime = t1._2
val marketTime = t2._2
val differ = transactionTime - marketTime
val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
if(differ >=0 && differ <= 3 * 1000) {
out.collect(t1._1,format.format(marketTime) + ": marketTime", format.format(transactionTime) + ": transactionTime",t2._3,t1._3,t1._4)
}
}
这里实现的逻辑就是每个key在10秒的EventTime窗口中join,且只需要那些交易时间在快照时间之后,且在3秒的间隔内的数据。
详细的代码如下:
import java.text.SimpleDateFormat
import org.apache.flink.api.common.functions.MapFunction
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.util.Collector
/**
* Created by zhe on 2016/6/21.
*/
object JoinedOperaion {
case class Transaction(szWind