前言
通过第一篇Flink简介我们对Flink有了基本的了解,接下来我们进行实操上手写代码。Flink底层是Java编写的,并为开发者提供了完整的Java和Scala API。本文编写Flink项目环境及工具:Java 8 ,IDEA ,Git和Maven工具。
一、创建项目
1.1 使用Git创建仓库
1.1.1这里只是再一次演示创建的步骤,可以看出flink-study仓库名是已经存在的了,点击创建即创建仓库完成。
1.1.2 复制仓库的URL,打开IDEA点击File->Project from Version Control->
输入URL可项目对应的目录点击克隆即可完成项目初始化。
flink-study的gitee地址:https://gitee.com/Fh_1214/flink-study.git
1.2 创建maven工程
1.2.1 File->new->Project->Maven->Next 填写项目信息后点击finish。
1.2.2 拷贝quick-start的pom.xml文件粘贴到flink-study目录下,编写pom,xml信息如下(若pom.xml变红色则点击pom.xml右击选择Add as maven project即可),使用module标签flink-study项目作为quick-start的父项目统一管理。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.huang</groupId>
<artifactId>flink-study</artifactId>
<version>1.0-SNAPSHOT</version>
<name>flink-study</name>
<description>聚合服务</description>
<packaging>pom</packaging>
<modules>
<module>quick-start</module>
</modules>
</project>
1.2.3 添加flink和scala等依赖到quick-start的pom.xml文件中
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>flink-study</artifactId>
<groupId>com.huang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quick-start</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<flink.version>1.13.0</flink.version>
<java.version>1.8</java.version>
<scala.binary.version>2.12</scala.binary.version>
<slf4j.version>1.7.30</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-runtime-web_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.14.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
1.2.4 在quick-start的resources目录新建log4j.properties日在配置
log4j.rootLogger=error, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
1.2.5 在quick-start目录下创建input目录,新建文本words.txt。文本信息如下
hello atguigu
hello flink
hello java
hello shanghai
nihao flink
1.2.6 注意在flink-study目录下创建.gitignore文件,避免提交二外代码到gitee仓库。
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
**/mvnw
**/mvnw.cmd
**/.mvn
**/target
.idea
**/.gitignore
二、批处理
2.1 统计单词频次
统计一段文字中,每个单词出现的频次,WordCount 程序——大数据领域入门案例等同于初学编程语言时的Hello World。input文件夹下的文本文件words.txt即是作数据集。词频次统计的基本思路是:先逐行读入文件数据,然后将每一行文字拆分成单词;接着按照单词分组,统计每组数据的个数,就是对应单词的频次。
2.2 代码实现
/**
* 批处理单词个数
* @Author huang.bX
* @Date 2022/9/29
*/
public class BatchWordCount {
public static void main(String[] args) {
//1.创建执行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//2.从文件读取数据
DataSource<String> dataSource = env.readTextFile("quick-start/input/words.txt");
//3.将每行数据进行分词,换成二元组类型
FlatMapOperator<String, Tuple2<String, Long>> wordAndOne = dataSource
.flatMap((String line, Collector<Tuple2<String, Long>> out) -> {
//将一行文本进行分词
String[] split = line.split(" ");
//将每个单词转换成二元组输出
for (String word : split) {
out.collect(Tuple2.of(word, 1L));
}
})
.returns(Types.TUPLE(Types.STRING, Types.LONG)); //当Lambda表达式使用 java 泛型的
//4.按照word进行分组
UnsortedGrouping<Tuple2<String, Long>> group = wordAndOne.groupBy(0);
//5.分组内聚合统计
AggregateOperator<Tuple2<String, Long>> sum = group.sum(1);
//6. 打印结果
try {
sum.print();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3 打印输出
(flink,2)
(hello,4)
(nihao,1)
(atguigu,1)
(java,1)
(shanghai,1)
Process finished with exit code 0
三、流处理
对于 Flink 而言,流才是整个处理逻辑的底层核心,所以流批统一之后的 DataStream API 更加强大,可以直接处理批处理和流处理的所有场景。
3.1 读取文件
我们同样试图读取文档 words.txt 中的数据,并统计每个单词出现的频次。这是一个“有界流”的处理,整体思路与之前的批处理非常类似,代码模式也基本一致。
3.1.1 代码实现
package com.huang.wordcount;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import java.util.Arrays;
public class BoundedStreamWordCount {
public static void main(String[] args) throws Exception {
//1.创建流式执行环境
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
//2.读取文件
DataStreamSource<String> lineDSS = env.readTextFile("quick-start/input/words.txt");
//3 转换数据格式
SingleOutputStreamOperator<Tuple2<String, Long>> wordAndOne = lineDSS
.flatMap((String line, Collector<String> words) -> {
Arrays.stream(line.split(" ")).forEach(words::collect);
})
.returns(Types.STRING)
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
//4.分组
KeyedStream<Tuple2<String, Long>, String> wordAndOneKS = wordAndOne
.keyBy(t -> t.f0);
//5.求和
SingleOutputStreamOperator<Tuple2<String, Long>> result = wordAndOneKS
.sum(1);
//6.打印
result.print();
//7.执行
env.execute();
}
}
3.1.2 打印输出
7> (flink,1)
1> (nihao,1)
3> (hello,1)
7> (flink,2)
2> (java,1)
8> (atguigu,1)
3> (hello,2)
5> (shanghai,1)
3> (hello,3)
3> (hello,4)
Process finished with exit code 0
主要观察与批处理程序 BatchWordCount 的不同:
- 创建执行环境的不同,流处理程序使用的是 StreamExecutionEnvironment。
- 每一步处理转换之后,得到的数据对象类型不同。
- 分组操作调用的是 keyBy 方法,可以传入一个匿名函数作为键选择器(KeySelector),指定当前分组的 key 是什么。
- 代码末尾需要调用 env 的 execute 方法,开始执行任务。
3.2 读取文本流
在实际的生产环境中,真正的数据流其实是无界的,有开始却没有结束,这就要求我们需要保持一个监听事件的状态,持续地处理捕获的数据。为了模拟这种场景,我们就不再通过读取文件来获取数据了,而是监听数据发送端主机的指定端口,统计发送来的文本数据中出现过的单词的个数。具体实现上,我们只要对BoundedStreamWordCount 代码中读取数据的步骤稍做修改,就可以实现对真正无界流的处理。
3.2.1 代码实现
package com.huang.wordcount;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
import java.util.Arrays;
public class StreamWordCount {
public static void main(String[] args) {
//1.创建流式执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2. 读取文本流
DataStreamSource<String> lineDSS = env.socketTextStream("localhost", 7777);
//3. 转换数据格式
SingleOutputStreamOperator<Tuple2<String, Long>> wordAndOne = lineDSS
.flatMap((String line, Collector<String> words) -> {
Arrays.stream(line.split(" ")).forEach(words::collect);
})
.returns(Types.STRING).map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
//4.分组
KeyedStream<Tuple2<String, Long>, String> wordAndOneKS = wordAndOne
.keyBy(t -> t.f0);
//5.求和
SingleOutputStreamOperator<Tuple2<String, Long>> result = wordAndOneKS
.sum(1);
//6.打印
result.print();
//7.执行
try {
env.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2.2 输入流数据
3.2.3 控制台输出
3.2.4 代码说明和注意事项
- socket 文本流的读取需要配置两个参数:发送端主机名和端口号。这里代码中指定了主机“localhost”的 7777 端口作为发送数据的 socket 端口,读者可以根据测试环境自行配置。
- 在实际项目应用中,主机名和端口号这类信息往往可以通过配置文件,或者传入程序运行参数的方式来指定。
- socket文本流数据的发送,可以通过Linux系统自带的netcat工具进行模拟。
(1)在 Windows 环境的主机 localhost 上,执行下列命令,发送数据进行测试:nc -l -p 7777
(2)启动 StreamWordCount 程序我们会发现程序启动之后没有任何输出、也不会退出。这是正常的——因为 Flink 的流处理是事件驱动的,当前程序会一直处于监听状态,只有接收到数据才会执行任务、输出统计结果。
总结
本章主要实现一个 Flink 开发的入门程序——词频统计 WordCount。通过批处理和流处理两种不同模式的实现,可以对 Flink 的 API 风格和编程方式有所熟悉,并且更加深刻地理解批处理和流处理的不同。另外,通过读取有界数据(文件)和无界数据(socket 文本流)进行流处理的比较,我们也可以更加直观地体会到 Flink 流处理的方式和特点。