介绍
Flink的DataStream API编程风格其实是一致的:基本上都是基于DataStream调用一个方
法,表示要做一个转换操作;方法需要传入一个参数,这个参数都是需要实现一个接口。
这些接口有一个共同特点:全部都以算子操作名称 + Function命名,例如源算子需要实
现SourceFunction接口,map算子需要实现MapFunction接口,reduce()算子需要实现
ReduceFunction接口。我们不仅可以通过自定义函数类或者匿名类来实现接口,也可以直接传
入Lambda表达式。这就是所谓的用户自定义函数(user-defined function,UDF)。
接下来我们就对这几种编程方式做一个梳理总结。
1. 函数类(Function Classes)
对于大部分操作而言,都需要传入一个用户自定义函数(UDF),实现相关操作的接口,
来完成处理逻辑的定义。Flink暴露了所有UDF函数的接口,具体实现方式为接口或者抽象类,
例如MapFunction、FilterFunction、ReduceFunction等。
所以最简单直接的方式,就是自定义一个函数类,实现对应的接口。
下面例子实现了FilterFunction接口,用来筛选url中包含“home”的内容
import org.apache.flink.api.common.functions.FilterFunction
import org.apache.flink.streaming.api.scala._
object TransFunctionUDFTest {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val clicks = env
.fromElements(
Event("Mary", "./home", 1000L),
Event("Bob", "./cart", 2000L)
)
//通过传入自定义FilterFunction实现过滤
val stream1 = clicks.filter(new FlinkFilter)
stream1.print()
env.execute()
}
//自定义FilterFunction函数类
class FlinkFilter extends FilterFunction[Event] {
override def filter(value: Event): Boolean = value.url.contains("home")
}
}
当然还可以通过匿名类来实现FilterFunction接口:
val filterdStream = stream.filter(new FilterFunction[Event] {
override def filter(value: Event): Boolean = value.url.contains("home")
})
为了类可以更加通用,我们还可以将用于过滤的关键字“home”抽象出来作为类的属性,
调用构造方法时传进去。
stream.filter(new KeywordFilter("home")).print()
//自定义FilterFunction函数类,将需要用到的过滤参数作为类的构造参数传入
class KeywordFilter(keyword: String) extends FilterFunction[Event] {
override def filter(value: Event): Boolean = value.url.contains(keyword)
}
对于Scala这样的函数式编程语言,更为简单的写法是直接传入一个Lambda表达式: stream.filter(_.url.contains("home")).print()
这样我们用一行代码就可以搞定,显得更加简洁明晰。
2. 富函数类(Rich Function Classes)
“富函数类”也是DataStream API提供的一个函数类的接口,所有的Flink函数类都有其
Rich 版本。富函数类一般是以抽象类的形式出现的。例如:RichMapFunction、RichFilterFunction、
RichReduceFunction 等。
与常规函数类的不同主要在于,富函数类可以获取运行环境的上下文,并拥有一些生命周
期方法,所以可以实现更复杂的功能。
典型的生命周期方法有:
⚫ open()方法,是Rich Function 的初始化方法,也就是会开启一个算子的生命周期。当
一个算子的实际工作方法例如 map()或者 filter()方法被调用之前,open()会首先被调
用。所以像文件IO流的创建,数据库连接的创建,配置文件的读取等等这样一次性
的工作,都适合在open()方法中完成。
⚫ close()方法,是生命周期中的最后一个调用的方法,类似于解构方法。一般用来做一
些清理工作。
需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;而对应的,
实际工作方法,例如RichMapFunction中的map(),在每条数据到来后都会触发一次调用。
来看一个例子:
import org.apache.flink.api.common.functions.RichMapFunction
64
65
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._
object RichFunctionTest{
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(2)
env.fromElements(
Event("Mary", "./home", 1000L),
Event("Bob", "./cart", 2000L),
Event("Alice", "./prod?id=1", 5 * 1000L),
Event("Cary", "./home", 60 * 1000L)
)
.map(new RichMapFunction[Event, Long]() {
//在任务生命周期开始时会执行open方法,在控制台打印对应语句
override def open(parameters: Configuration): Unit = {
println("索引为 " + getRuntimeContext.getIndexOfThisSubtask + " 的任务开
始")
}
// 将点击事件转换成长整型的时间戳输出
override def map(value: Event): Long = value.timestamp
//在任务声明周期结束时会执行close方法,在控制台打印对应语句
override def close(): Unit = {
println("索引为 " + getRuntimeContext.getIndexOfThisSubtask + " 的任务结
束")
}
})
.print()
env.execute()
}
}
输出结果是:
索引为 0 的任务开始
索引为 1 的任务开始
1> 1000
2> 2000
2> 60000
1> 5000
索引为 0 的任务结束
索引为 1 的任务结束
一个常见的应用场景就是,如果我们希望连接到一个外部数据库进行读写操作,推荐的最
佳实践如下:
class MyFlatMap extends RichFlatMapFunction[IN,OUT]{
override def open(parameters: Configuration): Unit = {
// 做一些初始化工作
// 例如建立一个和MySQL的连接
}
override def flatMap(value: IN, out: Collector[OUT]): Unit = {
// 对数据库进行读写
}
override def close(): Unit = {
// 清理工作,关闭和MySQL数据库的连接。
}
}
另外,富函数类提供了 getRuntimeContext()方法(我们在本节的第一个例子中使用了一
下),可以获取到运行时上下文的一些信息,例如程序执行的并行度,任务名称,以及状态
(state)。这使得我们可以大大扩展程序的功能,特别是对于状态的操作,使得Flink中的算子
具备了处理复杂业务的能力。