一个Flink程序,就是对DataStream的各种转换。代码基本由以下几部分构成:
- 获取执行环境
- 读取数据源
- 定义基于数据的各种转换操作
- 定义计算结果的输出位置
- 触发程序执行
执行环境
创建执行环境
执行环境是StreamExecutionEnvironment类的对象,是Flink程序的基础
- getExecutionEnvironment
最简单的方式,就是直接调用getExecutionEnvironment方法。它会根据当前运行的上下文直接得到正确的结果//此处的 env 是 StreamExecutionEnvironment 对象 val env = StreamExecutionEnvironment.getExecutionEnvironment
- createLocalEnvironment
这个方法返回一个本地执行环境。可以在调用时传入一个参数,指定默认的并行度;如果不传入,则默认并行度是本地的CPU核心数//此处的 localEnvironment 是 StreamExecutionEnvironment 对象 val localEnvironment = StreamExecutionEnvironment.createLocalEnvironment()
- createRemoteEnvironment
这个方法返回集群执行环境。需要在调用时指定JobManager的主机名和端口号,并指定要在集群中运行的Jar包//此处的 remoteEnv 是 StreamExecutionEnvironment 对象 val remoteEnv = StreamExecutionEnvironment .createRemoteEnvironment( "host", // JobManager 主机名 1234, // JobManager 进程端口号 "path/to/jarFile.jar" // 提交给 JobManager 的 JAR 包 )
执行模式
- 流执行模式(STREAMING)
一般用于需要持续实时处理的无界数据流。默认情况下,程序使用的就是STREAMING执行模式 - 批执行模式(BATCH)
专门用于批处理的执行模式。Flink处理作业的方式类似MapReduce - 自动模式(AUTOMATIC)
由程序根据输入数据源是否有界,自动选择执行模式
BATCH模式
- 通过命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH ...
在提交作业时,增加execution.runtime-mode参数,指定值为BATCH
2. 通过代码配置
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setRuntimeMode(RuntimeExecutionMode.BATCH)
在代码中,直接执行环境调用setRuntimeMode方法,传入BATCH模式
触发程序执行
需要显式调用执行环境的exeute()方法 ,来触发程序的执行。execute方法将会一直等待作业完成,然后返回一个在执行结果(JobExecutionResult)。
env.execute()
Source数据源(源算子)
DataStream API中直接提供了对一些基本数据源的支持,例如文件系统、Socket连接等;也提供了非常丰富的高级数据源连接器(Connector),例如Kafka Connector、Elasticsearch Connector等。用户也可以实现自定义Connector数据源,以便使Flink能够与其他外部系统进行数据交互
文件数据源
Flink可以将文件内容读取到系统中,并转换成分布式数据集DataStream进行处理。使用readTextFile(path)方法可以逐行读取文本文件内容,并作为字符串返回,代码如下:
//第一步:创建流处理的执行环境
val env=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:读取流数据,创建DataStream
val data:DataStream[String]=env.readTextFile("hdfs://127.0.0.1:9000/input/words.txt")
Socket数据源
通过监听Socket端口接收数据创建DataStream。例如以下代码从本地的9999端口接收数据:
//第一步:创建流处理的执行环境
val senv=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:读取流数据,创建DataStream
val data:DataStream[String]=senv.socketTextStream("localhost",9999)
集合数据源
- 集合数据源
从java.util.collection集合创建DataStream。集合中的所有元素必须是相同类型的,例如以下代码:
//第一步:创建流处理的执行环境
val senv=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:读取流数据,创建DataStream
val data:DataStream[String]=senv.fromCollection(
List("hello","flink","scala")
)
//当然,也可以从迭代器中创建DataStream,例如以下代码:
//第一步:创建流处理的执行环境
val senv=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:读取流数据,创建DataStream
val it = Iterator("hello","flink","scala")
val data:DataStream[String]=senv.fromCollection(it)
//还可以直接从元素集合中创建DataStream,例如以下代码:
//第一步:创建流处理的执行环境
val senv=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:读取流数据,创建DataStream
val data:DataStream[String]=senv.fromElements("hello","flink","scala")
高级数据源
Flink可以从Kafka、Flume、Kinesis等数据源读取数据,使用时需要引入第三方依赖库。例如,在Maven工程中引入Flink针对Kafka的API依赖库,代码如下:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.12</artifactId>
<version>1.13.0</version>
</dependency>
然后使用addSource()方法接入Kafka数据源,代码示例如下:
val senv = StreamExecutionEnvironment.getExecutionEnvironment()
val myConsumer = new FlinkKafkaConsumer08[String](...)
val stream = senv.addSource(myConsumer)
自定义数据源
在Flink中,用户也可以自定义数据源,以满足不同数据源的接入需求。自定义数据源有3种方式:
1)实现SourceFunction接口定义非并行数据源(单线程)。SourceFunction是Flink中所有流数据源的基本接口。
2)实现ParallelSourceFunction接口定义并行数据源。
3)继承RichParallelSourceFunction抽象类定义并行数据源。该类已经实现了ParallelSourceFunction接口,是实现并行数据源的基类,在执行时,Flink Runtime将执行与该类源代码配置的并行度一样多的并行实例。
4)继承RichSourceFunction抽象类定义并行数据源。该类是实现并行数据源的基类,该数据源可以通过父类AbstractRichFunction的getRuntimeContext()方法访问上下文信息,通过父类AbstractRichFunction的open()和close()方法访问生命周期信息。
数据源定义好后,可以使用StreamExecutionEnvironment.addSource(sourceFunction)将数据源附加到程序中。这样就可以将外部数据转换为DataStream。
例如,自定义MySQL数据源,读取MySQL中的表数据,实现步骤如下。
(1)引入数据库驱动
在Maven工程中引入MySQL数据库连接驱动的依赖库,代码如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
(2)创建表
在MySQL数据库中创建一张student表并添加测试数据
(3)定义样例类
定义样例类Student用于存储数据,代码如下:
package flink.demo
object Domain {
case class Student(id: Int, name: String, age: Int)
}
(4)创建JDBC工具类
创建一个JDBC工具类,用于获得MySQL数据库连接,代码如下:
import java.sql.DriverManager
import java.sql.Connection
/**
* JDBC工具类
*/
object JDBCUtils {
//数据库驱动类
private val driver = "com.mysql.jdbc.Driver"
//数据库连接地址
private val url = "jdbc:mysql://localhost:3306/demo_db"
//数据库账号
private val username = "root"
//数据库密码
private val password = "xxxxxx"
/**
* 获得数据库连接
*/
def getConnection(): Connection = {
Class.forName(driver)//加载驱动
val conn = DriverManager.getConnection(url, username, password)
conn
}
}
5)创建自定义数据源类
创建自定义MySQL数据源类MySQLSource,继承RichSourceFunction类,并重写open()、run()、cancel()方法,代码如下:
import java.sql.{Connection, PreparedStatement}
import flink.demo.Domain.Student
import org.apache.flink.streaming.api.functions.source.{RichSourceFunction, SourceFunction}
import org.apache.flink.configuration.Configuration
/**
* 自定义MySQL数据源
*/
class MySQLSource extends RichSourceFunction[Student] {
var conn: Connection = _//数据库连接对象
var ps: PreparedStatement = _//SQL命令执行对象
var isRunning=true//是否运行(是否持续从数据源读取数据)
/**
* 初始化方法
* @param parameters 存储键/值对的轻量级配置对象
*/
override def open(parameters: Configuration): Unit = {
//获得数据库连接
conn = JDBCUtils.getConnection
//获得命令执行对象
ps = conn.prepareStatement("select * from student")
}
/**
* 当开始从数据源读取元素时,该方法将被调用
* @param ctx 用于从数据源发射元素
*/
override def run(ctx: SourceFunction.SourceContext[Student]): Unit = {
//执行查询
val rs = ps.executeQuery()
//循环读取集合中的数据并发射出去
while (isRunning&&rs.next()) {
val student = Student(
rs.getInt("id"),
rs.getString("name"),
rs.getInt("age")
)
//从数据源收集一个元素数据并发射出去,而不附加时间戳(默认方式)
ctx.collect(student)
}
}
/**
* 取消数据源读取
*/
override def cancel(): Unit = {
this.isRunning=false
}
}
6)测试程序
创建测试类StreamTest,从自定义数据源中读取流数据,打印到控制台,代码如下:
import org.apache.flink.streaming.api.scala.{DataStream, _}
/**
* 测试类
*/
object StreamTest {
def main(args: Array[String]): Unit = {
//创建流处理执行环境
val senv=StreamExecutionEnvironment.getExecutionEnvironment
//从自定义数据源中读取数据,创建DataStream
val dataStream: DataStream[Domain.Student] = senv.addSource(new MySQLSource)
//打印流数据到控制台
dataStream.print()
//触发任务执行,指定作业名称
senv.execute("StreamMySQLSource")
}
}
直接在IDEA中运行上述测试类,控制台输出结果如下:
3> Student(2,李四,22)
2> Student(1,张三,19)
4> Student(3,王五,20)
结果前面的数字表示执行线程的编号。