0 前言
遇到了问题,一定不要在网上乱找答案,要看它第一次出现错误的位置(Cause by xxx),然后一层一层看。
1.1 初体验
1.1.1 程序一般流程
- 获取Flink流处理执行环境
- 构建source
- 数据处理
- 构建sink
1.1.2 示例
需求:编写Flink程序,用来统计单词的数量。
步骤:
- 获取Flink流处理运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
- 构建一个socket源(此时在node1上使用nc -lk 9999开启一个监听端口),没有nc的使用以下命令安装:
yum install nc -y
- 使用flink操作进行单词统计
- 打印(和批处理里面不太一样)
完整代码如下:
val env = StreamExecutionEnvironment.getExecutionEnvironment
val text = env.socketTextStream("node1", 9999)
val counts = text.flatMap { _.toLowerCase.split("\\W+") filter { _.nonEmpty } }
.map { (_, 1) }
.keyBy(_._1)
.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
.sum(1)
counts.print()
env.execute("Window Stream WordCount")
1.1.3 小结
与批处理对比:
1 运行环境对象不同,streamexecutionenviroment
2 有些算子是不同
3 程序是一直运行,除非我们手动停止。
需要注意的点:
1. socketTextStream本身就是一个addsource方法
可通过调试进入查看
还可以通过相应的Java jar包
1.2 DataSource基础
1.2.1 数据分类
Flink在流处理上的source和在批处理上的source基本一致。大致有4大类
基于本地集合的source(Collection-based-source)--->不常用,不现实
基于文件的source(File-based-source)- 读取文本文件,即符合 TextInputFormat 规范的文件,并将其作为字符串返回
基于网络套接字的source(Socket-based-source)- 从 socket 读取。元素可以用分隔符切分。
自定义的source(Custom-source)
Flink的stream程序都是通过addSource(sourcefunction)来添加数据源,我们可以自定义数据源,通过继承ParallelSourceFunction RichParallelSourceFunction 来实现自己的数据源。
1.2.2 基于本地集合source演示
// 1 获取流处理运行环境
val senv = StreamExecutionEnvironment.getExecutionEnvironment
//0.用element创建DataStream(fromElements)
val ds0: DataStream[String] = senv.fromElements("spark", "flink")
ds0.print()
//1.用Tuple创建DataStream(fromElements)
val ds1: DataStream[(Int, String)] = senv.fromElements((1, "spark"), (2, "flink"))
ds1.print()
//2.用Array创建DataStream
val ds2: DataStream[String] = senv.fromCollection(Array("spark", "flink"))
ds2.print()
//3.用ArrayBuffer创建DataStream
val ds3: DataStream[String] = senv.fromCollection(ArrayBuffer("spark", "flink"))
ds3.print()
//4.用List创建DataStream
val ds4: DataStream[String] = senv.fromCollection(List("spark", "flink"))
ds4.print()
//5.用List创建DataStream
val ds5: DataStream[String] = senv.fromCollection(ListBuffer("spark", "flink"))
ds5.print()
//6.用Vector创建DataStream
val ds6: DataStream[String] = senv.fromCollection(Vector("spark", "flink"))
ds6.print()
//7.用Queue创建DataStream
val ds7: DataStream[String] = senv.fromCollection(Queue("spark", "flink"))
ds7.print()
//8.用Stack创建DataStream
val ds8: DataStream[String] = senv.fromCollection(Stack("spark", "flink"))
ds8.print()
//9.用Stream创建DataStream(Stream相当于lazy List,避免在中间过程中生成不必要的集合)
val ds9: DataStream[String] = senv.fromCollection(Stream("spark", "flink"))
ds9.print()
//10.用Seq创建DataStream
val ds10: DataStream[String] = senv.fromCollection(Seq("spark", "flink"))
ds10.print()
//11.用Set创建DataStream(不支持)
//val ds11: DataStream[String] = senv.fromCollection(Set("spark", "flink"))
//ds11.print()
//12.用Iterable创建DataStream(不支持)
//val ds12: DataStream[String] = senv.fromCollection(Iterable("spark", "flink"))
//ds12.print()
//13.用ArraySeq创建DataStream
val ds13: DataStream[String] = senv.fromCollection(mutable.ArraySeq("spark", "flink"))
ds13.print()
//14.用ArrayStack创建DataStream
val ds14: DataStream[String] = senv.fromCollection(mutable.ArrayStack("spark", "flink"))
ds14.print()
//15.用Map创建DataStream(不支持)
//val ds15: DataStream[(Int, String)] = senv.fromCollection(Map(1 -> "spark", 2 -> "flink"))
//ds15.print()
//16.用Range创建DataStream
val ds16: DataStream[Int] = senv.fromCollection(Range(1, 9))
ds16.print()
//17.用fromElements创建DataStream
println("-------------------")
val ds17: DataStream[Long] = senv.generateSequence(1, 9)
ds17.print()
// 执行
senv.execute()
1.2.3 基于文件source演示
//TODO 2.基于文件的source(File-based-source)
//0.创建运行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//TODO 1.读取本地文件
val text1 = env.readTextFile("data2.csv")
text1.print()
//TODO 2.读取hdfs文件
val text2 = env.readTextFile("hdfs://hadoop01:9000/input/flink/README.txt")
text2.print()
env.execute()
1.2.4 基于网络套接字source演示
上面有,略。
1.2.5 基于自定义source演示
Flink stream中我们可以通过实现sourcefunction(不并行的)或者实现parallesourcefunction(并行的)来定义我们自己的数据源方法,然后通过senv.addSource(自定义sourcefunction),就可以读取数据进行转换处理
Ctrl + Alt + B查看SourceFunction接口的所有实现类
步骤:
1.首先通过实现SourceFunction或者它的子类RichSourceFunction来自定义Source;
StreamExecutionEnvironment.addSource(sourceFunction)添加进来。
addSource(new FlinkKafkaConsumer011<>(…)) 从 Apache Kafka 读取数据。当然你也可以自定义。
查看InputFormatSourceFunction中接口方法实现
写一个非并行的SourceFunction,代码如下:
/*
演示自定义非并行数据源实现
*/
object MySourceNoParalle {
def main(args: Array[String]): Unit = {
//1 创建一个流处理的运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 2 添加自定义的数据源
val myDs: DataStream[Long] = env.addSource(new MyNoParalleSourceFunction)//.setParallelism(3)
//3 打印数据
myDs.print()
//启动
env.execute()
}
}
//SourceFunction泛型是我们自定义source的返回数据类型
class MyNoParalleSourceFunction extends SourceFunction[Long] {
var ele: Long = 0
var isRunning = true
//发送数据,生产数据的方法
override def run(ctx: SourceFunction.SourceContext[Long]): Unit = {
while (isRunning) {
ele += 1
//通过上下文对象发送数据
ctx.collect(ele)
//降低发送速度
Thread.sleep(1000)
}
}
// 取消方法,取消是通过控制一个变量来影响run方法中的while循环
override def cancel(): Unit = {
isRunning = false //取消发送数据
}
}
说明:上述代码只能实现1个并行度,可以查看run和cancel方法的官方示例;如果需要实现多并行度,那就只能实现ParallelSourceFunction接口。
1.2.6 小结
1 创建一个class实现sourcefunction接口;
2 重写run方法,定义生产数据的业务逻辑,重写cancel方法(参考源码示例);
3 senv.addSource()添加自定义的source;
4 并行数据源:可以在source operator设置大于1的并行度;发送数据是重复;只需要将上面非并行自定义数据源实现的接口改为ParallelSourceFunction即可。
5 富ParallelSourceFunction可以提供open,close等方法(可以通过ctrl+o快捷键生成定义;如果操作数据库可以实现在open或者close打开关闭连接)。
非并行数据源:sourceFunction, source不能设置大于1的并行度,效率会比较低;
并行数据源:ParallelSourceFunction,source可以设置大于1的并行度,效率会更高;
富有的并行数据源:RichParallelSourceFunction,source可以设置大于1的并行度,此外还提供了open,close等高效的方法,效率会更高。
1.3 基础代码练习
自定义数据源, 每1秒钟随机生成一条订单信息(订单ID、用户ID、订单金额、时间戳)
要求:
- 随机生成订单ID(UUID)
- 随机生成用户ID(0-2)
- 随机生成订单金额(0-100)
- 时间戳为当前系统时间
开发步骤:
1. 创建订单样例类
2. 获取流处理环境
3. 创建自定义数据源
- 循环1000次
- 随机构建订单信息
- 上下文收集数据
- 每隔一秒执行一次循环
4. 打印数据
5. 执行任务
代码如下:
package com.sjxy.flink.stream.source.customsource
import java.util.UUID
import java.util.concurrent.TimeUnit
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.source.{RichParallelSourceFunction, SourceFunction}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import scala.util.Random
/*
自定义数据源,练习 生成订单数据
*/
//订单信息(订单ID、用户ID、订单金额、时间戳)
case class Order(id: String, userId: Int, money: Long, createTime: Long)
object OrderCustomSource {
def main(args: Array[String]): Unit = {
/*
1. 创建订单样例类
2. 获取流处理环境
3. 创建自定义数据源
- 循环1000次
- 随机构建订单信息
- 上下文收集数据
- 每隔一秒执行一次循环
4. 打印数据
5. 执行任务
*/
//1 获取流处理环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 2 加载自定义的order数据源,RichParallelSourceFunction泛型是 生产的数据类型,order
val orderDs: DataStream[Order] = env.addSource(new RichParallelSourceFunction[Order] {
var isRunning = true
//2.1生成订单数据方法
override def run(ctx: SourceFunction.SourceContext[Order]): Unit = {
//2.1.1 生成订单 业务逻辑
while (isRunning) {
//orderid
val orderId = UUID.randomUUID().toString
//userid
val userId = Random.nextInt(3)
//money
val money = Random.nextInt(101)
//createTime
val createTime = System.currentTimeMillis()
ctx.collect(Order(orderId, userId, money, createTime))
//每隔一秒中执行一次
TimeUnit.SECONDS.sleep(1)
}
}
//2.2 取消数据的生成方法
override def cancel(): Unit = {
isRunning = false
}
}).setParallelism(1)
//3 打印数据
orderDs.print()
// 4 启动
env.execute()
}
}
1.4 DataSource进阶
写一个从 MySQL 中读取数据的 Source
1. 打开DataGrip,连接集群里的MySQL数据库(file->new->datasource->mysql);
2. 生成一张测试的表,添加一些基本的数据;脚本如下:
CREATE TABLE `t_student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
INSERT INTO `t_student` VALUES ('1', 'jack', '18');
INSERT INTO `t_student` VALUES ('2', 'tom', '19');
INSERT INTO `t_student` VALUES ('3', 'rose', '20');
INSERT INTO `t_student` VALUES ('4', 'tom', '19');
INSERT INTO `t_student` VALUES ('5', 'jack', '18');
INSERT INTO `t_student` VALUES ('6', 'rose', '20');
3. 增加pom依赖,编写代码;
pom依赖如下:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
代码如下:
package com.sjxy.flink.stream.source.customsource
import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}
import java.util.concurrent.TimeUnit
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.source.{RichParallelSourceFunction, SourceFunction}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
/*
演示自定义并行数据源读取mysql
*/
//定义student 样例类
case class Student(id: Int, name: String, age: Int)
object MysqlRichParallelSource {
def main(args: Array[String]): Unit = {
//1 创建一个流处理的运行环境
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 2 添加自定义的数据源
val stuDs: DataStream[Student] = env.addSource(new MysqlRichParalleleSource).setParallelism(1)
//3 打印数据
stuDs.print()
//4 启动
env.execute()
}
}
// 2自定义mysql并行数据源
class MysqlRichParalleleSource extends RichParallelSourceFunction[Student] {
var ps: PreparedStatement = null
var connection: Connection = null
//2.1 开启mysql连接
override def open(parameters: Configuration): Unit = {
//驱动方式
connection = DriverManager.getConnection("jdbc:mysql://node1:3306/ke", "root", "123456")
//准备sql语句查询表中全部数据
var sql = "select id ,name,age from t_student";
//准备执行语句对象
ps = connection.prepareStatement(sql)
}
//2.3 释放资源,关闭连接
override def close(): Unit = {
if (connection != null) {
connection.close()
}
if (ps != null) ps.close()
}
var isRunning = true
// 2.2 读取mysql数据
override def run(ctx: SourceFunction.SourceContext[Student]): Unit = {
while (isRunning) {
//读取mysql中的数据
val result: ResultSet = ps.executeQuery()
while (result.next()) {
val userId = result.getInt("id")
val name = result.getString("name")
val age = result.getInt("age")
//收集并发送
ctx.collect(Student(userId, name, age))
}
//休眠5s,执行一次
TimeUnit.SECONDS.sleep(5)
}
}
//取消方法
override def cancel(): Unit = {
isRunning = false
}
}