4.fink-DataStream(四)
1.source
1.基于文件
readTextFile(path)
读取文本文件,文件遵循TextInputFormat读取规则,逐行读取并返回。
2.基于socket
socketTextStream从socker中读取数据,元素可以通过一个分隔符切开。
3.基于集合
fromCollection(Collection)
通过java 的collection集合创建一个数据流,集合中的所有元素必须是相同类型的。
4.自定义输入
addSource可以实现读取第三方数据源的数据
系统内置提供了一批connectors,连接器会提供对应的source支持【kafka】
以 kafka 消息队列的数据作为来源
1.添加pom依赖
<! – https://mvnrepository.com/artifact/org.apache.flink/flink - connector - kafka - 0.11 – >
org.apache.flink
flink-connector-kafka-0.11_2.11
1.7.2
val properties = new Properties()
properties.setProperty("bootstrap.servers", "master:9092,slaver1:9092,slaver2: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")
val stream3 = env.addSource(new FlinkKafkaConsumer011[String]("sensor", new
SimpleStringSchema(), properties))
自定义 Source
val stream4 = env.addSource( new MySensorSource() )
class MySensorSource extends SourceFunction[SensorReading]{
// flag: 表示数据源是否还在正常运行
var running: Boolean = true
override def cancel(): Unit = {
running = false
}
//实现run方法
override def run(ctx: SourceFunction.SourceContext[SensorReading]): Unit
= { }
}
2.transformations
map:输入一个元素,然后返回一个元素,中间可以做一些清洗转换等操作
flatmap:输入一个元素,可以返回零个,一个或者多个元素(压平的效果)
filter:过滤函数,对传入的数据进行判断,符合条件的数据会被留下
keyBy:根据指定的key进行分组,相同key的数据会进入同一个分区
两种典型用法:
dataStream.keyBy("someKey") // 指定对象中的 "someKey"字段作为分组key
dataStream.keyBy(0) //指定Tuple中的第一个元素作为分组key
注意:以下类型是无法作为key的
1:一个实体类对象,没有重写hashCode方法,并且依赖object的hashCode方法
2:一个任意形式的数组类型
3:基本数据类型,int,long
reduce:对数据进行聚合操作,结合当前元素和上一次reduce返回的值进行聚合操作,然后返回一个新的值
aggregations:sum(),min(),max()等
window:在后面单独详解
union:合并多个流,新的流会包含所有流中的数据,但是union是一个限制,就是所有合并的流类型必须是一致的。
connect:和union类似,但是只能连接两个流,两个流的数据类型可以不同,会对两个流中的数据应用不同的处理方法。
CoMap, CoFlatMap:在ConnectedStreams中需要使用这种函数,类似于map和flatmap
Split:根据规则把一个数据流切分为多个流
Select:和split配合使用,选择切分后的流
1.这些算子可以针对 KeyedStream 的每一个支流做聚合。
sum()
min()
max()
minBy()
maxBy()
2.keyBy:DataStream → KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分
区包含具有相同 key 的元素,在内部以 hash 的形式实现的。
3.Reduce :KeyedStream → DataStream:一个分组数据流的聚合操作,合并当前的元素
和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是
只返回最后一次聚合的最终结果。
4. Split 和 Select :
- split:DataStream → SplitStream:根据某些特征把一个 DataStream 拆分成两个或者
多个 DataStream。
- select :SplitStream→DataStream:从一个 SplitStream中获取一个或者多个 DataStream。
5.Connect 和 CoMap
- connect:DataStream,DataStream → ConnectedStreams:连接两个保持他们类型的数据
流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各
自的数据和形式不发生任何变化,两个流相互独立。
- coMap/coFlatMap:作用于 ConnectedStreams 上,功能与 map
和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap处理
6.Union :DataStream → DataStream:对两个或者两个以上的 DataStream 进行 union 操
作,产生一个包含所有 DataStream 元素的新 DataStream。
7。Connect 与 Union 区别:
- Union 之前两个流的类型必须是一样,Connect 可以不一样,在之后的 coMap
中再去调整成为一样的。
- Connect 只能操作两个流,Union 可以操作多个。
3.sink
writeAsText():将元素以字符串形式逐行写入,这些字符串通过调用每个元素的toString()方法来获取
print()/printToErr():打印每个元素的toString()方法的值到标准输出或者标准错误输出流中
自定义输出addSink:Flink支持多种数据源的输出
比如下面内置的Connectors:
Apache Kafka (source/sink)
Apache Cassandra (sink)
Elasticsearch (sink)
Hadoop FileSystem (sink)
RabbitMQ (source/sink)
Apache ActiveMQ (source/sink)
Redis (sink)
1.kafka-sink
pom
<! -- https://mvnrepository.com/artifact/org.apache.flink/flink - connector - kafka - 0.11 -- >
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.11</artifactId>
<version>1.7.2</version>
</dependency
//设置kafka参数
var properties = new Properties()
properties.setProperty("bootstrap.servers", "master:9092,slaver1:9092,slaver2: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")
//获取 kafka source ->dataStream
val stream = env.addSource(new FlinkKafkaConsumer011[String(
"flink_source_of_kafka_test",//topic name
new SimpleStringSchema(),//固定写法
properties//kafka 参数
))
//transform
val dataStream: DataStream[String] = stream.map(data => {
val split: Array[String] = data.split(",")
SensorReading(split(0).trim, split(1).trim.toLong, split(2).trim.toDouble).toString
})
//sink
dataStream.addSink(new FlinkKafkaProducer011[String](
"master:9092,slaver1:9092,slaver2:9092",//kakfa params
"sink",//topic name
new SimpleStringSchema()//固定写法
))
2.redis-sink
pom
<! -- https://mvnrepository.com/artifact/org.apache.bahir/flink - connector - redis -- >
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
redis集群模式
//source
val stream = env.addSource(new FlinkKafkaConsumer011[String]("flink_source_of_kafka_test", new SimpleStringSchema(), properties))
//transform
val dataStream= stream.map(data => {
val split: Array[String] = data.split(",")
SensorReading(split(0).trim, split(1).trim.toLong, split(2).trim.toDouble)
})
//redis conf
//redis集群模式
val nodes = new java.util.HashSet[InetSocketAddress]()
nodes.add(new InetSocketAddress("master", 7001))
nodes.add(new InetSocketAddress("slaver1", 7002))
nodes.add(new InetSocketAddress("slaver2", 7003))
nodes.add(new InetSocketAddress("slaver1", 7004))
nodes.add(new InetSocketAddress("slaver2", 7005))
nodes.add(new InetSocketAddress("master", 7006))
val redisConf = new FlinkJedisClusterConfig
.Builder()
.setNodes(nodes)
.build()
//sink
dataStream.addSink(new RedisSink(redisConf,new MyRedisMapper))
redis-单节点模式核心代码
//redis配置
val conf = new FlinkJedisPoolConfig
.Builder()
.setHost("localhost")
.setPort(6379)
.build()
//redis-sink
//使用flink自带的redissink
//也可以自定义redis sink 实现RichSinkFunction
dataStream.addSink( new RedisSink[SensorReading](conf, new MyRedisMapper) )
//自定义
//Person为自定义的bean
class MyRedisMapper extends RedisMapper[Person]{
override def getCommandDescription: RedisCommandDescription = {
new RedisCommandDescription(RedisCommand.HSET, "sensor_temperature")
}
override def getValueFromData(t: Person): String =
t.temperature.toString
override def getKeyFromData(t: Person): String = t.id
}
3.jdbc-sink
// source
val stream = env.readTextFile("D:\\code\\flink_first\\src\\main\\scala\\sensor.txt")
val dataStream: DataStream[SensorReading] = stream.map(data => {
val split: Array[String] = data.split(",")
SensorReading(split(0).trim, split(1).trim.toLong, split(2).trim.toDouble)
})
//sink
dataStream.addSink( new MyJdbcSink() )//传入自定义的jdbc sink
dataStream.print()
env.execute("kafka sink test")
//自定义function
class MyJdbcSink() extends RichSinkFunction[SensorReading]{
var conn: Connection = _
var insertStmt: PreparedStatement = _
var updateStmt: PreparedStatement = _
// open 主要是创建连接
override def open(parameters: Configuration): Unit = {
super.open(parameters)
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test",
"root", "123456")
insertStmt = conn.prepareStatement("INSERT INTO temperatures (sensor,
temp) VALUES (?, ?)")
updateStmt = conn.prepareStatement("UPDATE temperatures SET temp = ? WHERE
sensor = ?")
}
// 调用连接,执行 sql
override def invoke(value: SensorReading, context:
SinkFunction.Context[_]): Unit = {
updateStmt.setDouble(1, value.temperature)
updateStmt.setString(2, value.id)
updateStmt.execute()
if (updateStmt.getUpdateCount == 0) {
insertStmt.setString(1, value.id)
insertStmt.setDouble(2, value.temperature)
insertStmt.execute()
}
}
override def close(): Unit = {
insertStmt.close()
updateStmt.close()
conn.close()
}
}
4.Elasticsearch
pom
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch6_2.11</artifactId>
<version>1.7.2</version>
</dependency>
//相当于发送http请求
val httpHosts = new util.ArrayList[HttpHost]()
httpHosts.add(new HttpHost("localhost", 9200))
//flink自带的es sink
val esSinkBuilder = new ElasticsearchSink.Builder[SensorReading]( httpHosts,
new ElasticsearchSinkFunction[SensorReading] {
override def process(t: SensorReading, runtimeContext: RuntimeContext,
requestIndexer: RequestIndexer): Unit = {
println("saving data: " + t)
val json = new util.HashMap[String, String]()
json.put("data", t.toString)
val indexRequest =
Requests.indexRequest().index("sensor").`type`("readingData").source(json)
requestIndexer.add(indexRequest)
println("saved successfully")
}
} )
dataStream.addSink( esSinkBuilder.build() )
自定义sin:Java-redis-sink
实现SinkFunction接口或者继承RichSinkFunction
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> text = env.socketTextStream("node01", 9999, "\n"); // 连接netcat
//对数据进行组装,把string转化为tuple2<String,String>
DataStream<Tuple2<String, String>> l_wordsData = text.map(new MapFunction<String, Tuple2<String, String>>() {
@Override
public Tuple2<String, String> map(String value) throws Exception {
return new Tuple2<>("l_words", value);
}
});
//创建redis的配置
FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("node02").setPort(6379).build();
//创建redissink
RedisSink<Tuple2<String, String>> redisSink = new RedisSink<>(conf, new MyRedisMapper());
l_wordsData.addSink(redisSink);
env.execute("StreamingDemoToRedis");
}
public static class MyRedisMapper implements RedisMapper<Tuple2<String, String>>{
// 表示从接收的数据中获取需要操作的redis key
@Override
public String getKeyFromData(Tuple2<String, String> data) {
return data.f0;
}
// 表示从接收的数据中获取需要操作的redis value
@Override
public String getValueFromData(Tuple2<String, String> data) {
return data.f1;
}
// 持久化方式
@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.LPUSH);
}
}
4.重分区
可以事先思考一个问题:让我们自己设计重分区可以有哪几种规则?
Random partitioning:随机分区
实现方法:dataStream.shuffle()
查看源码,selectChannels方法使用random.nextInt实现随机分布数据到某个分区中
Rebalancing:对数据集进行再平衡,重分区,消除数据倾斜
实现方法:dataStream.rebalance()
查看源码,selectChannels方法使用“++”的方式使得数据均匀的分布到各个分区中
Rescaling:用在上下游并发不等的时候,决定一对多或多对一的分发策略
实现方法:dataStream.rescale()
举个例子:
如果上游操作有2个并发,而下游操作有4个并发,那么上游的一个并发结果分配给下游的两个并发操作,另外的一个并发结果分配给了下游的另外两个并发操作。另一方面,下游有两个并发操作而上游又4个并发操作,那么上游的其中两个操作的结果分配给下游的一个并发操作,而另外两个并发操作的结果则分配给另外一个并发操作。
注意:在不同并行度不是彼此的倍数的情况下,一个或多个下游 算子操作将具有来自上游 算子操作的不同数量的输入。由于这种方式仅发生在一个单一的节点,因此没有跨网络的数据传输。
Rescaling与Rebalancing的区别:
Rebalancing会产生全量重分区,而Rescaling不会。
关于Rescaling与Rebalancing的分区策略,如下图。
Custom partitioning:自定义分区
自定义分区需要实现Partitioner接口
dataStream.partitionCustom(partitioner, "someKey")
或者dataStream.partitionCustom(partitioner, 0);
实现demo如下
分区函数:
1、global:上游的分区的数据全部分发到下游的第一个分区中
2、shuffle:上游的每个分区的数据随机分发到下游的分区中
3、rebalance:上游的第一个记录分发到下游的第一个分区,第二条记录分发到下游的第二个分区
4、rescale:(用在上下游并发不等的时候,决定一对多或多对一的分发策略)
5、partitionCustom:自定义分区,实现类需要继承Partitioner接口
6、broadcast:广播分区将上游数据集输出到下游Operator的每个实例中。适合于大数据集Join小数据集的场景。
7、forward:将记录输出到下游本地的operator实例。ForwardPartitioner分区器要求上下游算子并行度一样。上下游Operator同属一个SubTasks。
8、keyby:将记录按Key的Hash值输出到下游Operator实例。
UDF 函数 更细粒度的控制流
Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。例如
MapFunction, FilterFunction, ProcessFunction、RichMapFunction、RichFilterFunction 等等
富函数(Rich Functions
“富函数”是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都
有其 Rich 版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一
些生命周期方法,所以可以实现更复杂的功能。
- RichMapFunction
- RichFlatMapFunction
- RichFilterFunction
- Rich Function 有一个生命周期的概念。典型的生命周期方法有:
- open()方法是 rich function 的初始化方法,当一个算子例如 map 或者 filter
被调用之前 open()会被调用。 - lose()方法是生命周期中的最后一个调用的方法,做一些清理工作。
- getRuntimeContext()方法提供了函数的 RuntimeContext 的一些信息,例如函
数执行的并行度,任务的名字,以及 state 状态
- open()方法是 rich function 的初始化方法,当一个算子例如 map 或者 filter