目录
前言
一、执行环境
1、创建执行环境
2、执行模式(Execution Mode)
3、触发执行
二、源算子(Source)
1、读取数据的算子就是源算子。
2、源算子种类
3、Flink 支持的数据类型
三、转换算子(Transformation)
1、基本转换算子
2、聚合算子(Aggregation)
3、匿名函数(Lambda)
4、富函数类(Rich Function Classes)
5、物理分区
四、输出算子(Sink)
1、连接到外部系统
2、输出到文件
3、输出到 Kafka
4、输出到 Redis
5、输出到 Elasticsearch
6、输出到 MySQL(JDBC)
7、自定义 Sink 输出
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
StreamExecutionEnvironment localEnv =
StreamExecutionEnvironment.createLocalEnvironment();
StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment
.createRemoteEnvironment(
"host", // JobManager 主机名
1234, // JobManager 进程端口号
"path/to/jarFile.jar" // 提交给 JobManager 的 JAR 包
);
(1)设置方式
建议 : 不要在代码中配置,而是使用命令行。这同设置并行度是类似的:在提交作业时指定参数可以更加灵活,同一段应用程序写好之后,既可以用于批处理也可以用于流处理。而在 代码中硬编码(hard code )的方式可扩展性比较差,一般都不推荐。
DataStream stream = env.addSource(...);
POJO:一个简单的Java类,这个类没有实现/继承任何特殊的java接口或者类,不遵循任何主要java模型,约定或者框架的java对象。在理想情况下,POJO不应该有注解。方便数据的解析和序列化。
(1)从集合中读取数据
// 构建集合
ArrayList clicks = new ArrayList<>();
clicks.add(new Event("Mary","./home",1000L));
clicks.add(new Event("Bob","./cart",2000L));
DataStream stream = env.fromCollection(clicks);
// 不构建集合,直接列出元素
DataStreamSource stream2 = env.fromElements(
new Event("Mary", "./home", 1000L),
new Event("Bob", "./cart", 2000L)
);
DataStream stream = env.readTextFile("clicks.csv");
(3)从 Socket 读取数据
DataStream stream = env.socketTextStream("localhost", 7777);
(4)从 Kafka 读取数据
org.apache.flink
flink-connector-kafka_2.12
1.13.0
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "hadoop102:9092");
properties.setProperty("group.id", "consumer-group");
properties.setProperty("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty("auto.offset.reset", "latest");
DataStreamSource stream = env.addSource(new FlinkKafkaConsumer(
"clicks",
new SimpleStringSchema(),
properties
));
(5)自定义 Source
(1)类型系统
TypeInformation 类是 Flink 中所有类型描述符的基类。 它涵盖了类型的一些基本属性,并为每个数据类型生成特定的序列化器、反序列化器和比较器。
(2)基本类型
(5)辅助类型
Option、Either、List、Map 等
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));
(1)映射(map) :就是一个“一一映射”,消费一个元素就产出一个元素
// 传入匿名类,实现 MapFunction
stream.map(new MapFunction() {
@Override
public String map(Event e) throws Exception {
return e.user;
}
});
// 传入 MapFunction 的实现类
stream.map(new UserExtractor()).print();
(2)过滤:filter 转换操作,顾名思义是对数据流执行一个过滤,通过一个布尔条件表达式设置过滤条件,对于每一个流内元素进行判断,若为 true 则元素正常输出,若为 false 则元素被过滤掉。
// 传入匿名类实现 FilterFunction
stream.filter(new FilterFunction() {
@Override
public boolean filter(Event e) throws Exception {
return e.user.equals("Mary");
}
});
(3)扁平映射:
flatMap 操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个一个的个体使用。消费一个元素,可以产生 0 到多个元素。flatMap 可以认为是“扁平化”(flatten) 和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,再对拆分后的元素做转换处理。 flatMap 并没有直接定义返回值类型,而是通过一个(Collector)来 指定输出。
同 map 一样,flatMap 也可以使用 Lambda 表达式或者 FlatMapFunction 接口实现类的方式来进行传参,返回值类型取决于所传参数的具体逻辑,可以与原数据流相同,也可以不同。
(1)按键分区(keyBy):在 Flink 中,需要先进行分区,再做聚合; 这个操作就是通过 keyBy 来完成的。keyBy 是聚合前必须要用到的一个算子。keyBy 通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务,也就对应 着任务槽(task slot)。
// 使用 Lambda 表达式
KeyedStream keyedStream = stream.keyBy(e -> e.user);
// 使用匿名类实现 KeySelector
KeyedStream keyedStream1 = stream.keyBy(new KeySelector() {
@Override
public String getKey(Event e) throws Exception {
return e.user;
}
});
(2)简单聚合
stream.keyBy(r -> r.f0).sum(1).print();
stream.keyBy(r -> r.f0).sum("f1").print();
stream.keyBy(r -> r.f0).max(1).print();
stream.keyBy(r -> r.f0).max("f1").print();
stream.keyBy(r -> r.f0).min(1).print();
stream.keyBy(r -> r.f0).min("f1").print();
stream.keyBy(r -> r.f0).maxBy(1).print();
stream.keyBy(r -> r.f0).maxBy("f1").print();
stream.keyBy(r -> r.f0).minBy(1).print();
stream.keyBy(r -> r.f0).minBy("f1").print();
// 如果数据流的类型是 POJO 类,那么就只能通过字段名称来指定,不能通过位置来指定了。
(3)归约聚合(reduce)
public interface ReduceFunction extends Function, Serializable {
T reduce(T value1, T value2) throws Exception;
}
//map 函数使用 Lambda 表达式,返回简单类型,不需要进行类型声明
DataStream stream1 = clicks.map(event -> event.url);
// flatMap 使用 Lambda 表达式,必须通过 returns 明确声明返回类型
DataStream stream2 = clicks.flatMap((Event event, Collector
out) -> {
out.collect(event.url);
}).returns(Types.STRING);
// 使用显式的 ".returns(...)"
DataStream> stream3 = clicks
.map( event -> Tuple2.of(event.user, 1L) )
.returns(Types.TUPLE(Types.STRING, Types.LONG));
stream3.print();
一个常见的应用场景就是,如果我们希望连接到一个外部数据库进行读写操作,那么将连接操作放在 map() 中显然不是个好选择——因为每来一条数据就会重新连接一次数据库;所以 我们可以在 open() 中建立连接,在 map()中读写数据,而在 close() 中关闭连接。
public class MyFlatMap extends RichFlatMapFunction> {
@Override
public void open(Configuration configuration) {
// 做一些初始化工作
// 例如建立一个和 MySQL 的连接
}
@Override
public void flatMap(IN in, Collector
5、物理分区
keyBy 是一种逻辑分区( logical partitioning)操作。物理分区与 keyBy 另一大区别在于,keyBy 之后得到的是一个 KeyedStream,而物理分区之后结果仍是 DataStream,且流中元素数据类型保持不变。常见的物理分区策略有随机分配(Random)、轮询分配(Round-Robin)、重缩放(Rescale)和广播(Broadcast)。
// 经洗牌后打印输出,并行度为 4
stream.shuffle().print("shuffle").setParallelism(4);
(2) 轮询分区( Round-Robin )
轮询也是一种常见的重分区方式。简单来说就是“发牌”,按照先后顺序将数据做依次分发。通过调用 DataStream 的 .rebalance() 方法,就可以实现轮询重分区。 rebalance 使用的是 Round-Robin 负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去。
// 经轮询重分区后打印输出,并行度为 4
stream.rebalance().print("rebalance").setParallelism(4);
(3) 重缩放分区( rescale )
重缩放分区和轮询分区非常相似。当调用 rescale() 方法时,其实底层也是使用 Round-Robin 算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中,如图 5-11 所示。也就是说,“发牌人”如果有多个,那么 rebalance 的方式是每个发牌人都面向所有人发牌;而 rescale 的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌。
从底层实现上看, rebalance 和 rescale 的根本区别在于任务之间的连接机制不同。 rebalance
将会针对所有上游任务(发送数据方)和所有下游任务(接收数据方)之间建立通信通道,这
是一个笛卡尔积的关系;而 rescale 仅仅针对每一个任务和下游对应的部分任务之间建立通信
通道,节省了很多资源。
(4)广播( broadcast )
这种方式其实不应该叫做“重分区”,因为经过广播之后,数据会在不同的分区都保留一份,可能进行重复处理。可以通过调用 DataStream 的 broadcast() 方法,将输入数据复制并发送到下游算子的所有并行任务中去。
(5)全局分区( global )
全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用 .global()方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。
(6) 自定义分区( Custom )
在调用时,方法需要传入两个参数,第一个是自定义分区器( Partitioner )对象,第二个
是应用分区器的字段,它的指定方式与 keyBy 指定 key 基本一样:可以通过字段名称指定,
也可以通过字段位置索引来指定,还可以实现一个 KeySelector 。
// 将自然数按照奇偶分区
env.fromElements(1, 2, 3, 4, 5, 6, 7, 8)
.partitionCustom(new Partitioner() {
@Override
public int partition(Integer key, int numPartitions) {
return key % 2;
}
}, new KeySelector() {
@Override
public Integer getKey(Integer value) throws Exception {
return value;
}
})
.print().setParallelism(2);
四、输出算子(Sink)
Flink 程序中所有对外的输出操作,一般都是利用 Sink 算子完成的。Sink 在 Flink 中代表了将结果数据收集起来、输出到外部的意思, print 方法其实就是一种 Sink,它表示将数据流写入标准控制台打印输出。
stream.addSink(new SinkFunction(…));
addSink 方法同样需要传入一个参数,实现的是 SinkFunction 接口。在这个接口中只需要重写一个方法 invoke(), 用来将指定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用:
1、连接到外部系统
像 Kafka 之类流式系统, Flink 提供了完美对接, source/sink 两端都能连接,可读可写;而对于 Elasticsearch 、文件系统( FileSystem )、 JDBC 等数据存储系统,则只提供了输出写入的 sink 连接器。
2、输出到文件
Flink 为此专门提供了一个流式文件系统的连接器: StreamingFileSink ,它继承自抽象类 RichSinkFunction,而且集成了 Flink 的检查点( checkpoint )机制,用来保证精确一次( exactly
once)的一致性语义。StreamingFileSink 为批处理和流处理提供了一个统一的 Sink,它可以将分区文件写入 Flink支持的文件系统。它可以保证精确一次的状态一致性,大大改进了之前流式文件 Sink 的方式。
它的主要操作是将数据写入桶( buckets ),每个桶中的数据都可以分割成一个个大小有限的分
区文件,这样一来就实现真正意义上的分布式文件存储。我们可以通过各种配置来控制“分桶”
的操作;默认的分桶方式是基于时间的,我们每小时写入一个新的桶。换句话说,每个桶内保
存的文件,记录的都是 1 小时的输出数据。
3、输出到 Kafka
Flink 与 Kafka 的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项目中是最高级别的一致性保证。具体步骤如下:
(1)添加 Kafka 连接器依赖
(2)启动 Kafka 集群
(3)编写输出到 Kafka 的示例代码
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
Properties properties = new Properties();
properties.put("bootstrap.servers", "hadoop102:9092");
DataStreamSource stream = env.readTextFile("input/clicks.csv");
stream
.addSink(new FlinkKafkaProducer(
"clicks",
new SimpleStringSchema(),
properties
));
env.execute();
}
FlinkKafkaProducer 继承了抽象类 TwoPhaseCommitSinkFunction,这是一个实现了 “ 两阶段提交 ” 的 RichSinkFunction 。两阶段提交提供了 Flink 向 Kafka 写入数据的事务性保证,能够真正做到精确一次( exactly once )的状态一致性。
数据管道:Flink 从 Kakfa 的一个 topic 读取消费数据,然后进行处理转换,最终将结果数据写入 Kafka 的另一个 topic——数据从 Kafka 流入、经 Flink处理后又流回到 Kafka 去,这就是所谓的“数据管道”应用。
4、输出到 Redis
Redis 是一个开源的内存式的数据存储,提供了像字符串( string )、哈希表( hash )、列表
( list )、集合( set )、排序集合( sorted set )、位图( bitmap )、地理索引和流( stream )等一系 列常用的数据结构。因为它运行速度快、支持的数据类型丰富,在实际项目中已经成为了架构
优化必不可少的一员,一般用作数据库、缓存,也可以作为消息代理。
5、输出到 Elasticsearch
ElasticSearch 是一个分布式的开源搜索和分析引擎,适用于所有类型的数据。ElasticSearch 有着简洁的 REST 风格的 API,以良好的分布式特性、速度和可扩展性而闻名,在大数据领域 应用非常广泛。
6、输出到 MySQL(JDBC)
尽管在大数据处理中直接与 MySQL 交互的场景不多,但最终处理的计算结果是要给外部应用消费使用的,而外部应用读取的数据存储往往就是 MySQL 。所以我们也需要知道如何将数据输出到 MySQL 这样的传统数据库。
(1)添加依赖
org.apache.flink
flink-connector-jdbc_${scala.binary.version}
${flink.version}
mysql
mysql-connector-java
5.1.47
(2)启动 MySQL,在 database 库下建表 clicks
mysql> create table clicks(
-> user varchar(20) not null,
-> url varchar(100) not null);
(3)编写输出到 MySQL 的代码
stream.addSink(JdbcSink.sink(
"INSERT INTO clicks (user, url) VALUES (?, ?)",
(statement, r) -> {
statement.setString(1, r.user);
statement.setString(2, r.url);
},
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
// MySQL5.7的写法
.withUrl("jdbc:mysql://localhost:3306/ct_2022")
.withDriverName("com.mysql.jdbc.Driver")
.withUsername("root")
.withPassword("root")
.build()
)
);
7、自定义 Sink 输出
例如, Flink 并没有提供 HBase 的连接器,所以需要我们自己写。
(1)导入依赖
org.apache.hbase
hbase-client
${hbase.version}
(2)编写输出到 HBase 的代码
你可能感兴趣的:(Flink,大数据,flink,分布式,big,data)