本博客以scala编程概念为主
1、概念
官网链接
Flink中的DataSet程序是常规程序,可对数据集进行转换(filtering, mapping, joining, grouping)。最初从某些来源(sources)(by reading files, or from local collections)创建数据集。结果通过接收器返回,接收器可以例如将数据写入(分布式)文件或标准输出(例如命令行终端)。Flink程序可以在各种上下文中运行,独立运行或嵌入其他程序中。执行可以在本地JVM或许多计算机的群集中进行。
Source=>Flink(transformations)=>sink
2、flink综合java和scala开发的项目构建creenflow
1、创建一个空的scala工程
2、pom文件里添加
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<flink.version>1.9.0</flink.version>
<scala.binary.version>2.11</scala.binary.version>
<scala.version>2.11.12</scala.version>
</properties>
可以在项目中新建java包;完整项目结构如下:
3、Data Source概述
1、基于文件
- readTextFile(path)/ TextInputFormat-逐行读取文件,并将它们作为字符串返回。
- readTextFileWithValue(path)/ TextValueInputFormat-逐行读取文件,并将它们作为StringValues返回。StringValues是可变字符串。
- readCsvFile(path)/ CsvInputFormat-解析以逗号(或其他字符)分隔的字段的文件。返回元组,案例类对象或POJO的数据集。支持基本的Java类型及其与Value相对应的字段类型。
- readFileOfPrimitives(path, delimiter)// PrimitiveInputFormat-解析以换行符(或其他char序列)定界的原始数据类型的文件,例如String或Integer使用给定的定界符。
- readSequenceFile(Key, Value, path)// SequenceFileInputFormat-创建JobConf并从指定的路径中读取类型为SequenceFileInputFormat,Key类和Value类的文件,并将它们作为Tuple2 <Key,Value>返回。
2、基于集合
-
fromCollection(Iterable)-从Iterable创建数据集。Iterable返回的所有元素都必须是同一类型。
-
fromCollection(Iterator)-从迭代器创建数据集。该类指定迭代器返回的元素的数据类型。
-
fromElements(elements: _*)-从给定的对象序列创建数据集。所有对象必须具有相同的类型。
-
fromParallelCollection(SplittableIterator)-从迭代器并行创建数据集。该类指定迭代器返回的元素的数据类型。
-
generateSequence(from, to) -并行生成给定间隔中的数字序列。
3、通用
-
readFile(inputFormat, path)/ FileInputFormat-接受文件输入格式。
-
createInput(inputFormat)/ InputFormat-接受通用输入格式。
4、从集合创建DataSet的scala实现
package com.kun.flink.chapter04
import org.apache.flink.api.scala._
object DataSetDataSourceApp {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
fromCollection(env)
}
def fromCollection(env: ExecutionEnvironment): Unit = {
val data = 1 to 10
env.fromCollection(data).print()
}
}
//结果
1
2
3
4
5
6
7
8
9
10
5、从集合创建DataSet的java实现
package com.kun.flink.chapter04;
import org.apache.flink.api.java.ExecutionEnvironment;
import java.util.ArrayList;
import java.util.List;
public class JavaDataSetDataSourceApp {
public static void main(String[] args) throws Exception {
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
fromCollection(env);
}
public static void fromCollection(ExecutionEnvironment env) throws Exception {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i <10 ; i++) {
list.add(i);
}
env.fromCollection(list).print();
}
}
//结果
0
1
2
3
4
5
6
7
8
9
6、从文件创建DataSet的scala实现
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
textFile(env)
}
def textFile(env:ExecutionEnvironment):Unit = {
//可以是也给文件也可以是一个文件夹
val filePath="test_files\\test_file\\test01.txt"
env.readTextFile(filePath).print()
}
//结果
hello,welcome
hello,world,welcome
7、从文件创建DataSet的java实现
public static void main(String[] args) throws Exception {
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
textFile(env);
}
public static void textFile(ExecutionEnvironment env) {
String filePath="test_files\\test_file";
try {
env.readTextFile(filePath).print();
} catch (Exception e) {
e.printStackTrace();
}
}
//结果
hello,welcome
hello,world,welcome
8、从csv文件创建DataSet的scala实现
原始数据
name,age,job
zhangsan,30,Developer
lisi,32,Developer
//这一行代码是在类的外部
case class MyCaseClass(name:String,age:Int)
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
csvFile(env)
}
def csvFile(env: ExecutionEnvironment): Unit = {
val filePath = "test_files\\test_csv"
//ignoreFirstLine=true表示忽略第一行;readCsvFile源码有解释
env.readCsvFile[(String, Int, String)](filePath, ignoreFirstLine = true).print()
env.readCsvFile[(String, Int)](filePath, ignoreFirstLine = true).print()
env.readCsvFile[(String, Int)](filePath, ignoreFirstLine = true,includedFields=Array(0,1)).print()
env.readCsvFile[MyCaseClass](filePath, ignoreFirstLine = true,includedFields=Array(0,1)).print()
env.readCsvFile[Person](filePath, ignoreFirstLine = true,pojoFields=Array("name","age","work")).print()
//结果
(lisi,32,Developer)
(zhangsan,30,Developer)
(lisi,32)
(zhangsan,30)
(zhangsan,30)
(lisi,32)
MyCaseClass(zhangsan,30)
MyCaseClass(lisi,32)
Person{name='zhangsan', age=30, work='Developer'}
Person{name='lisi', age=32, work='Developer'}
其中pojo类为
package com.kun.flink.chapter04;
public class Person {
private String name;
private int age;
private String work;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", work='" + work + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getWork() {
return work;
}
public void setWork(String work) {
this.work = work;
}
}
8、从递归文件夹创建DataSet的scala实现
文件夹结构
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
readRecursiveFiles(env)
}
def readRecursiveFiles(env: ExecutionEnvironment):Unit={
val filePath="test_files\\test_file"
//默认不使用递归
env.readTextFile(filePath).print()
println("======================")
//使用递归
val parameters = new Configuration
parameters.setBoolean("recursive.file.enumeration", true)
env.readTextFile(filePath).withParameters(parameters).print()
}
结果
hello,world,welcome
hello,welcome
======================
hello,welcome
hello,world,welcome
hello,welcome
hello,world,welcome
9、从压缩文件创建DataSet的scala实现
如果输入文件标记有适当的文件扩展名,Flink当前支持自动的对输入文件解压缩。特别是,这意味着无需进一步配置输入格式,并且不需要任何FileInputFormat压缩支持,包括自定义输入格式。请注意,压缩文件可能无法并行读取,从而影响作业的可伸缩性。
10,transformation
1、概述
数据转换将一个或多个数据集转换为新的数据集。程序可以将多种转换组合成复杂的程序集。
2、map函数
获取一个元素并生成一个元素。
data.map { x => x.toInt }
3、Filter函数
为每个元素计算布尔函数,并保留函数返回true的元素。
重要提示:系统假设函数不修改应用谓词的元素。违反这一假设可能导致不正确的结果。
data.filter { _ > 1000 }
4、MapPartition函数
在单个函数调用中转换并行分区。函数以“迭代器”的形式获取分区,并可以生成任意数量的结果值。每个分区中的元素数量取决于并行度和以前的操作。
常用于输出到数据库里;比如输出到MySQL会调用同等分区数的connection
data.mapPartition { in => in map { (_, 1) } }
5、First-n函数
返回数据集的第一个n(任意)元素。第一个n可以应用于常规数据集、分组数据集或分组排序数据集。分组键可以指定为键选择器函数、元组位置或case类字段。
val in: DataSet[(Int, String)] = // [...]
// 返回前三个
val result1 = in.first(3)
// 按照key分组后求每组的前3个
val result2 = in.groupBy(0).first(3)
// 按照key分组后对于每组按照value做升序排序
val result3 = in.groupBy(0).sortGroup(1, Order.ASCENDING).first(3)
6、FlatMap函数
获取一个元素并生成零个、一个或多个元素。
可以把string类型的list压扁成一个string
//data是一个集合类型;
data.flatMap { str => str.split(" ") }
//比如求一个list的以逗号分隔的集合的单词次数
data.flatMap(_.split(",")).map((_,1)).groupBy(0).sum(1).print()
6、Distinct函数
返回数据集中不同的元素。它从输入数据集中删除与元素的所有字段或字段子集相关的重复条目。
data.distinct ()
7、join函数(内链接)
通过创建键上相等的所有成对的元素来联接两个数据集。使用JoinFunction将一对元素转换为单个元素,或者使用FlatJoinFunction将一对元素转换为任意多个(包括无元素)。
//在这种情况下,元组字段用作键。“0”是第一个元组的join字段,"1"是第二个元组的join字段。
val result = input1.join(input2).where(0).equalTo(1)
您可以通过连接提示指定运行时执行连接的方式。这些提示描述了连接是通过分区还是广播进行的,以及连接是使用基于排序的算法还是基于散列的算法。
如果没有指定提示,系统将尝试估计输入大小,并根据这些估计选择最佳策略。
//通过广播第一个数据集执行连接
//对广播数据使用哈希表
val result = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST).where(0).equalTo(1)
8、OuterJoin函数(外连接)
val joined = left.leftOuterJoin(right).where(0).equalTo(1) {
(left, right) =>
val a = if (left == null) "none" else left._1
(a, right)
}
//全连接
val joined = left.fullOuterJoin(right).where(0).equalTo(1) {
(left, right) =>{
if(left == null){
("_",right)
}else if (right == null){
(left,"_")
}else{
(left,right)
}
}
}
9、Cross函数
构建两个输入的笛卡尔积(叉乘),创建所有对元素。可选地使用交叉函数将一对元素转换为单个元素
val data1: DataSet[Int] = // [1,2,3]3个元素
val data2: DataSet[String] = // [a,b]2个元素
//cross后结果会是6个元素 (1,a)(1,b)(2,a)(2,b)(3,a)(3,b)
val result: DataSet[(Int, String)] = data1.cross(data2)
11、Data Sinks(数据输出)
Flink自带多种内置输出格式,当然也可以自定义输出格式
内置的输出格式:
- writeAsText()/ TextOutputFormat-将元素按行写为字符串。通过调用每个元素的toString()方法获得字符串。
- writeAsCsv(…)/ CsvOutputFormat-将元组写为逗号分隔的值文件。行和字段定界符是可配置的。每个字段的值来自对象的toString()方法。
- print()/ printToErr()- 在标准输出/标准错误流上打印每个元素的toString()值。
- write()/ FileOutputFormat-自定义文件输出的方法和基类。支持自定义对象到字节的转换。
- output()// OutputFormat-最通用的输出方法,用于不基于文件的数据接收器(例如将结果存储在数据库中)。
// text data
val textData: DataSet[String] = // [...]
// write DataSet to a file on the local file system
textData.writeAsText("file:///my/result/on/localFS")
// write DataSet to a file on a HDFS with a namenode running at nnHost:nnPort
textData.writeAsText("hdfs://nnHost:nnPort/my/result/on/localFS")
// write DataSet to a file and overwrite the file if it exists
textData.writeAsText("file:///my/result/on/localFS", WriteMode.OVERWRITE)
// tuples as lines with pipe as the separator "a|b|c"
val values: DataSet[(String, Int, Double)] = // [...]
values.writeAsCsv("file:///path/to/the/result/file", "\n", "|")
// this writes tuples in the text formatting "(a, b, c)", rather than as CSV lines
values.writeAsText("file:///path/to/the/result/file")
// this writes values as strings using a user-defined formatting
values map { tuple => tuple._1 + " - " + tuple._2 }
.writeAsText("file:///path/to/the/result/file")
12、flink的计数器
官方概念
类似于spark的累加器
package com.kun.flink.chapter04
import org.apache.flink.api.common.accumulators.LongCounter
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
import org.apache.flink.core.fs.FileSystem.WriteMode
object CountApp {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val data = env.fromElements("dingyi", "zhagnsan", "wangwu", "lisi")
/*data.map(new RichMapFunction[String, Long]() {
var counter = 0l
override def map(in: String): Long = {
counter = counter + 1
println("counter:" + counter)
counter
}
}).setParallelism(3)//设置并行度为3
.print()*/ //结果不为4
//如果并行度大于1得话,counter得结果就不准确;就好比java多线程同时操控一个未加锁的变量
//解决:
val info = data.map(new RichMapFunction[String,String]() {
//step1:定义累加器
val counter = new LongCounter()
//重新open方法
override def open(parameters: Configuration): Unit = {
//step2:注册计数器
getRuntimeContext.addAccumulator("ele-counts-scala",counter)
}
override def map(in: String): String = {
counter.add(1)
in
}
})
info.writeAsText("test_files\\output\\sink-scala-count\\",WriteMode.OVERWRITE).setParallelism(3)
val jobResult = env.execute("CountApp")
//拿到我们注册的计数器
val num = jobResult.getAccumulatorResult[Long]("ele-counts-scala")
println("num:"+num)//结果为4
}
}
13、分布式缓存
Flink提供了一个分布式缓存,类似于Apache Hadoop,使文件本地可访问用户函数的并行实例。此功能可用于共享包含静态外部数据(如字典或机器学习的回归模型)的文件。
缓存的工作原理如下。程序在其执行环境中以特定名称注册本地或远程文件系统(如HDFS或S3)的文件或目录作为缓存文件。当程序执行时,Flink自动将文件或目录复制到所有worker的本地文件系统。用户函数可以查找指定名称下的文件或目录,并从worker的本地文件系统中访问它。
package com.kun.flink.chapter04
import org.apache.commons.io.FileUtils
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
object DistributedCacheApp {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val filePath = "test_files\\test_file\\test01.txt"
//step1:注册一个本地文件
env.registerCachedFile(filePath, "kun-scala-dc")
val data = env.fromElements("dingyi", "zhagnsan", "wangwu", "lisi")
data.map(new RichMapFunction[String, String] {
//step2:在open方法中获取到分布式缓存的内容即可
override def open(config: Configuration): Unit = {
val dcFile = getRuntimeContext.getDistributedCache.getFile("kun-scala-dc")
val lines = FileUtils.readLines(dcFile)
/**
* 此时会出现一个异常:java集合和scala结婚不兼容
* 需要添加import scala.collection.JavaConversions._
*/
import scala.collection.JavaConversions._
for (ele <- lines) {
println(ele)
}
}
override def map(value: String): String = {
value
}
}).print()
}
}
14、广播变量
package com.kun.flink.chapter04
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.scala._
import org.apache.flink.configuration.Configuration
object BroadcastApp {
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
// 1. The DataSet to be broadcast
val toBroadcast = env.fromElements(1, 2, 3)
val data = env.fromElements("a", "b")
data.map(new RichMapFunction[String, (String, String)]() {
var broadcastSet: Traversable[Int] = null
override def open(config: Configuration): Unit = {
// 3. Access the broadcast DataSet as a Collection
import scala.collection.JavaConverters._ //asScala需要使用隐式转换
broadcastSet = getRuntimeContext().getBroadcastVariable[Int]("broadcastSetName").asScala
}
def map(in: String): (String, String) = {
var result = ""
for (broadVariable <- broadcastSet) {
result = result+broadVariable+" "
}
(in, result)
}
}).withBroadcastSet(toBroadcast, "broadcastSetName").print() // 2. Broadcast the DataSet
}
}
结果
将广播变量1 2 3 广播了出去
(a,1 2 3 )
(b,1 2 3 )