Flink 流处理 API
前言
前面做了一个Flink里的hello world,实际上该学习Flink的架构原理了,但是感觉比较难,还是先学习API的使用,之后再回过头学习架构原理。
一、API类型
API分为上图中几个部分,source读取数据源,transform做转换计算,sink做输出写入到web系统,在这三步前还有一步,创建执行环境。
二、Environment
1.getExecutionEnvironment
getExecutionEnvironment用来创建执行环境,表示当前执行程序的上下文。如果程序是独立调用的(在IDEA写的),那么此方法返回本地的执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回集群的执行环境。
getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境,这是最常用的一种创建执行环境的方式。
批处理创建环境:
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
流处理创建环境:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
如果没有设置并行度,会以 flink-conf.yaml 中的配置为准,默认是 1。
2.createLocalEnvironment
这个方法用来指定返回本地执行环境,需要在调用时指定默认的并行度。
LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(1);
3.createRemoteEnvironment
这个方法用来指定返回集群的执行环境,将Jar提交到远程服务器。需要在调用时指JobManager的IP和端口号,并指定要在集群中运行的Jar包。
StreamExecutionEnvironment env = StreamExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123, "YOURPATH//WordCount.jar");
三、Source
1.从集合中读取数据
public class SourceTest1_Collection {
public static void main(String[] args) throws Exception {
//创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//从集合读取数据
DataStream<SensorReading> sensorReadingDataStream = env.fromCollection(Arrays.asList(
new SensorReading("sensor_1", 1547718199L, 35.8),
new SensorReading("sensor_6", 1547718201L, 15.4),
new SensorReading("sensor_7", 1547718202L, 6.7),
new SensorReading("sensor_10", 1547718205L, 38.1)
));
sensorReadingDataStream.print().setParallelism(1);
env.execute();
}
}
说明:
1)env.fromCollection就是从集合中读取数据
2)在不设置并行度时,输出的结果可能是乱序的
3)设置输出并行度为1时,输出的结果是正常顺序的,因为这里只是读取了数据并没有做其他操作,所以把读取和输出这两部分合成了一部分,都设置为了1
2.从文件中读取数据
public class SourceTest1_Collection {
public static void main(String[] args) throws Exception {
//创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//从集合读取数据
/*
DataStream<SensorReading> sensorReadingDataStream = env.fromCollection(Arrays.asList(
new SensorReading("sensor_1", 1547718199L, 35.8),
new SensorReading("sensor_6", 1547718201L, 15.4),
new SensorReading("sensor_7", 1547718202L, 6.7),
new SensorReading("sensor_10", 1547718205L, 38.1)
));
*/
DataStream<String> stringDataStream = env.readTextFile("D:\\opt\\idea-workspace\\Flume_Interceptor\\src\\main\\java\\com\\atguigu\\flinkTest\\hello.txt");
stringDataStream.print();
env.execute();
}
}
3.从Kafka中读取数据
首先,要引入Kafka连接器的依赖:
public class SourceTest3_Kafka {
public static void main(String[] args) throws Exception {
//创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties properties=new Properties();
properties.setProperty("bootstrap.servers", "localhost: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");
DataStream<String> sensorDataStream = env.addSource(new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties));
sensorDataStream.print();
env.execute();
}
}
说明:
1)env提供了addSource的方法可以添加数据源,同时Flink-Kafka连接器提供了Kafka数据源的方法
4.自定义Source
除了以上的 source 数据来源,我们还可以自定义 source。需要做的,只是传入一个 SourceFunction 就可以。具体调用如下:DataStream<SensorReading> dataStream = env.addSource( new MySensor());
需求:自定义一个数据源,要求可以随机生成传感器数据。
代码如下:
public class SourceTest4_UDF {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<SensorReading> sensorReadingDataStream = env.addSource(new MySensor());
sensorReadingDataStream.print();
env.execute();
}
public static class MySensor implements SourceFunction<SensorReading>{
//定义一个标志位,用来控制数据的产生
private boolean running=true;
@Override
public void run(SourceContext<SensorReading> sourceContext) throws Exception {
Random random=new Random();
HashMap<String,Double> sensorTempMap=new HashMap<String,Double>();
for(int i=0;i<10;i++){
sensorTempMap.put("sensor_"+(i+1),60+random.nextGaussian()*2);
}
while(running){
for(String sensorId: sensorTempMap.keySet()){
Double newTemp=sensorTempMap.get(sensorId)+ random.nextGaussian();
sensorTempMap.put(sensorId,newTemp);
sourceContext.collect(new SensorReading(sensorId,System.currentTimeMillis(),newTemp));
}
}
}
@Override
public void cancel() {
running=false;
}
}
}
代码说明:
1)addSource方法允许我们自己创建数据源,但是要求我们实现SourceFunction方法,T是我们要生成的数据类型,也就是Flink实时读取到的数据类型
2)collect方法将输出发送出去
四、Transform
Flink中必须先进行分组,然后才能进行聚合操作。
文档内容:
1.Map算子
DataStream<Integer> result = stringDataStream.map(new MapFunction<String, Integer>() {
@Override
public Integer map(String s) throws Exception {
return s.length();
}
});
运行结果:
2.FlatMap算子
DataStream<String> result = stringDataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String s, Collector<String> collector) throws Exception {
String[] v = s.split(" ");
for (String m : v) {
collector.collect(m);
}
}
});
flatMap算子是一条数据输出多条数据,故内部类里面函数返回值为void,参数类型设置为Collector
运行结果:
3.Filter算子
DataStream<String> result = stringDataStream.filter(new FilterFunction<String>() {
@Override
public boolean filter(String s) throws Exception {
return s.endsWith("k");
}
});
运行结果:
4.KeyBy算子
这里实际上有两个操作,先算key的hashCode,然后对分区数取余,相同结果的放在一个分区。
最终,相同key的数据肯定在一个分区,但是在一个分区的数据的key不一定都相同。
而且最终的数据类型发生了改变,从DataStream→KeyedStream,原先DataStream不能调用的一些操作在分组后可以调用了
一个例子:
按id进行分组
DataStream<String> stringDataStream = env.readTextFile("D:\\opt\\idea-workspace\\Flume_Interceptor\\src\\main\\java\\com\\atguigu\\flinkTest\\Sensor.txt");
DataStream<SensorReading> sensorResult = stringDataStream.map(new MapFunction<String, SensorReading>() {
@Override
public SensorReading map(String s) throws Exception {
String[] fields = s.split(",");
return new SensorReading(fields[0], Long.valueOf(fields[1]), Double.valueOf(fields[2]));
}
});
KeyedStream<SensorReading, Tuple> result = sensorResult.keyBy("id");
result.print();
env.execute();
使用注意事项:
必须是元组类型,keyBy的参数才能写0、1、2…
5.滚动聚合算子(Rolling Aggregation)
使用的数据集:
5.1 sum()
sum()方法只会进行累加,其余字段均用分组内第一条数据中字段的值。
使用方法:
DataStream<SensorReading> temperatureResult = result.sum("temperature");
执行结果:
5.2 min()
min()函数只会替换要求的最小的值的那个字段,其余字段均用分组内第一条数据中字段的值。
使用方法:
DataStream<SensorReading> temperatureResult = result.min("temperature");
执行结果:
5.3 max()
max()函数只会替换要求的最大的值的那个字段,其它的字段用自己本身的值。
在分组后,按照温度求最大值(如果传过来的数据类型是元组,参数可以写位置0、1、2…):
使用方法:
DataStream<SensorReading> temperatureResult = result.max("temperature");
执行结果:
5.4 minBy()
minBy()再找到最小值的时候,其它的字段用自己本身的值。
使用方法:
DataStream<SensorReading> temperatureResult = result.minBy("temperature");
执行结果:
5.5 maxBy()
maxBy()再找到最大值的时候,会替换所有字段的值。
使用方法:
DataStream<SensorReading> temperatureResult = result.maxBy("temperature");
执行结果:
6.Reduce
Reduce可以合并该条数据和前面聚合的数据的结果,产生一个新的值。
使用方法(需要先用keyBy进行分组):
DataStream<SensorReading> reduceResult = result.reduce(new ReduceFunction<SensorReading>() {
@Override
public SensorReading reduce(SensorReading oldData, SensorReading newData) throws Exception {
return new SensorReading(oldData.getId(), newData.getTime(), Math.max(oldData.getTemperature(), newData.getTemperature()));
}
});
说明:
reduce方法中第一个参数是之前聚合的结果,第二个参数是当前拿到的该条数据。
执行结果:
7.Split和Select
调用Split后,返回值的类型从DataStream—>SplitStream,根据某些特征把一个DataStream划分成两个或多个DataStream(但是实际上还是一个流)。
调用Select后,数据类型从SplitStream—>DataStream,可以从一个SplitStream中获取一个或多个DataStream,根据select中传递的参数获取对应的DataStream。
需求:传感器数据按照温度高低(以30度为界),拆分成两个流。
代码如下:
public class TransformTest4_MultipleStreams {
public static void main(String[] args) throws Exception {
//创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> stringDataStream = env.readTextFile("D:\\opt\\idea-workspace\\Flume_Interceptor\\src\\main\\java\\com\\atguigu\\flinkTest\\Sensor.txt");
DataStream<SensorReading> sensorResult = stringDataStream.map(new MapFunction<String, SensorReading>() {
@Override
public SensorReading map(String s) throws Exception {
String[] fields = s.split(",");
return new SensorReading(fields[0], Long.valueOf(fields[1]), Double.valueOf(fields[2]));
}
});
//分流,按照温度值30度为界分为两条流
SplitStream<SensorReading> result = sensorResult.split(new OutputSelector<SensorReading>() {
@Override
public Iterable<String> select(SensorReading sensorReading) {
return (sensorReading.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
}
});
DataStream<SensorReading> high = result.select("high");
DataStream<SensorReading> low = result.select("low");
DataStream<SensorReading> all = result.select("high","low");
high.print("high");
low.print("low");
all.print("all");
env.execute();
}
}
实际上就是把每一条数据打上一个标签。然后可以根据标签选取对应的数据。(filter也可以实现相应的功能)当然,一个数据可以有多个标签。
执行结果:
8.Connect和CoMap
使用connect算子,可以将两个DataStream连接起来,这两个DataStream被connect之后,只是被放在了一个同一个流之中,它们内部各自的数据和形式都不发生任何变化,两个流相互独立。
数据类型的变化:DataStream,DataStream—>ConnectedStreams
CoMap和CoFlatMap作用于ConnectedStreams上,功能与map 和flatMap一样,对ConnectedStreams中的每一个Stream分别进行map和flatMap 处理。对两个DataStream可以进行不同的处理操作。
使用方法:
public class TransformTest4_MultipleStreams {
public static void main(String[] args) throws Exception {
//创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DataStream<String> stringDataStream = env.readTextFile("D:\\opt\\idea-workspace\\Flume_Interceptor\\src\\main\\java\\com\\atguigu\\flinkTest\\Sensor.txt");
DataStream<SensorReading> sensorResult = stringDataStream.map(new MapFunction<String, SensorReading>() {
@Override
public SensorReading map(String s) throws Exception {
String[] fields = s.split(",");
return new SensorReading(fields[0], Long.valueOf(fields[1]), Double.valueOf(fields[2]));
}
});
//分流,按照温度值30度为界分为两条流
SplitStream<SensorReading> result = sensorResult.split(new OutputSelector<SensorReading>() {
@Override
public Iterable<String> select(SensorReading sensorReading) {
return (sensorReading.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
}
});
DataStream<SensorReading> high = result.select("high");
DataStream<SensorReading> low = result.select("low");
DataStream<SensorReading> all = result.select("high","low");
high.print("high");
low.print("low");
all.print("all");
//2.合流connect 将高温流转换为二元组类型,与低温流连接合并之后,输出状态信息
DataStream<Tuple2<String, Double>> warningStream = high.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
@Override
public Tuple2<String, Double> map(SensorReading sensorReading) throws Exception {
return new Tuple2<>(sensorReading.getId(), sensorReading.getTemperature());
}
});
ConnectedStreams<Tuple2<String, Double>, SensorReading> connect = warningStream.connect(low);
DataStream<Object> resultStream = connect.map(new CoMapFunction<Tuple2<String, Double>, SensorReading, Object>() {
@Override
public Object map1(Tuple2<String, Double> stringDoubleTuple2) throws Exception {
return new Tuple3<>(stringDoubleTuple2.f0, stringDoubleTuple2.f1, "high temp warning");
}
@Override
public Object map2(SensorReading sensorReading) throws Exception {
return new Tuple2<>(sensorReading.getId(), "normal");
}
});
resultStream.print();
env.execute();
}
}
首先必须要进行分流,然后才能进行合流。
两个流经过map,flatMap后的类型是DataStream。
这里第一个流里面的数据类型是Tuple2<String, Double>,第二个流里面的数据类型是SensorReading,经过connect后的数据类型是 ConnectedStreams<Tuple2<String, Double>, SensorReading>,对合流后的数据进行map操作,有三个参数,第一个参数是第一个流的数据类型,第二个参数是第二个流里面的数据类型,第三个参数是经过map处理后返回的数据类型,这里写的是Object类型,因为两个流返回的类型不一样,如果把类型写死,那么两个流返回的类型必须一致。
9.Union
前面的connect算子只能限制把两个流整合为一个流,但是两个流的数据类型可以不一样。
Union可以将多个流整合为一个流,但是要求这多个流里面的数据类型得一致。
数据类型:从DataStream—>DataStream
使用方法:
//分流,按照温度值30度为界分为两条流
SplitStream<SensorReading> result = sensorResult.split(new OutputSelector<SensorReading>() {
@Override
public Iterable<String> select(SensorReading sensorReading) {
return (sensorReading.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
}
});
DataStream<SensorReading> high = result.select("high");
DataStream<SensorReading> low = result.select("low");
DataStream<SensorReading> all = result.select("high","low");
//2.合流union 将高温流转换为二元组类型,与低温流连接合并之后,输出状态信息
DataStream<SensorReading> unionResult = high.union(low);
unionResult.print();
env.execute();
执行结果:
五、Sink
Flink没有类似于spark中foreach方法,让用户进行迭代的操作。虽有对外的输出操作都要利用Sink完成。最后通过类似如下方式完成整个任务最终输出操作。
stream.addSink(new MySink(xxxx))
Flink官方提供了一部分框架的sink,如果需要用到的没有提供就需要自定义sink,下图是Flinl1.10版本提供的sink:
1.Kafka
需要添加依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.11_2.12</artifactId>
<version>1.10.1</version>
</dependency>
Flink消费Kafka里的数据:
Properties properties_consumer=new Properties();
properties_consumer.setProperty("bootstrap.servers", "hadoop102:9092");
properties_consumer.setProperty("group.id","consumer-group");
properties_consumer.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties_consumer.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties_consumer.setProperty("auto.offset.reset", "latest");
DataStream<String> kafka_consumer = env.addSource(new FlinkKafkaConsumer011<String>("sensor", new SimpleStringSchema(), properties_consumer));
Flink向Kafka中发送数据:
DataStream<String> mapResult = kafka_consumer.map(new MapFunction<String, String>() {
@Override
public String map(String s) throws Exception {
String[] fields = s.split(",");
return new SensorReading(fields[0], Long.valueOf(fields[1]), Double.valueOf(fields[2])).toString();
}
});
Properties properties_producer=new Properties();
properties_producer.setProperty("bootstrap.servers", "hadoop102:9092");
mapResult.addSink(new FlinkKafkaProducer011<String>("test", new SimpleStringSchema(), properties_producer));
FlinkKafkaProducer011有很多重载方法,也可以如下方式:
dataStream.addSink(new FlinkKafkaProducer011[String]("hadoop102:9092", "test", new SimpleStringSchema()));
其实,可以将上面消费和生产数据合并,这样能实现类似于管道的效果,从Kafka进,从Kafka出,但是topic不一样。在Kafka集群的sensor这个topic中手动输入数据,Flink会消费输入的数据,然后生成DataStream,然后再把消费到的数据发送到Kafka中的test这个topic。
2.Redis
首先要添加依赖:
<dependency>
<groupId>org.apache.bahir</groupId>
<artifactId>flink-connector-redis_2.11</artifactId>
<version>1.0</version>
</dependency>
下面是使用Redis的代码:
//redis连接的配置,采用了构造者模式
FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
.setHost("hadoop102")
.setPort(6379)
.build();
mapResult.addSink(new RedisSink<>(config,new MyRedisMapper()));
//自定义RedisMapper
public static class MyRedisMapper implements RedisMapper<SensorReading> {
//定义保存数据到Redis的命令,一个id对应一个温度,采用hset的数据格式
@Override
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.HSET,"sensor_temp");
}
@Override
public String getKeyFromData(SensorReading sensorReading) {
return sensorReading.getId();
}
@Override
public String getValueFromData(SensorReading sensorReading) {
return sensorReading.getTemperature().toString();
}
}
代码说明:
RedisSink要求传两个参数。
第一个参数是Redis连接的配置,可以使用FlinkJedisPoolConfig,它使用了构造者模式创建对象;
第二个参数是Redis保存数据的命令的对象,需要我们实现RedisMapper这个接口,里面三个方法,getCommandDescription()函数的的作用是声明一个保存数据的命令,里面第一个参数是要保存数据的命令,这是保存为hash类型,故使用hset,第二个参数是要保存的表名;getKeyFromData()函数是从要处理的数据中获取要保存到redis里的key;getValueFromData()函数是从要处理的数据中获取要保存到redis里的value。
3.ElasticSearch
首先,添加依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch6_2.12</artifactId>
<version>1.10.1</version>
</dependency>
使用方法:
List<HttpHost> hosts=new ArrayList<>();
hosts.add(new HttpHost("hadoop102",9200));
mapResult.addSink(new ElasticsearchSink.Builder<SensorReading>(hosts, new MyESFunction()).build());
env.execute();
public static class MyESFunction implements ElasticsearchSinkFunction<SensorReading> {
@Override
public void process(SensorReading sensorReading, RuntimeContext runtimeContext, RequestIndexer requestIndexer) {
HashMap hashMap=new HashMap<>();
hashMap.put("id",sensorReading.getId());
hashMap.put("ts",sensorReading.getTime());
hashMap.put("temperature",sensorReading.getTemperature());
IndexRequest indexRequest = Requests.indexRequest()
.index("sensor")
.type("readingData")
.source(hashMap);
requestIndexer.add(indexRequest);
}
}
代码说明:
ElasticsearchSink的构造方法是private类型的,所以无法直接new一个对象,
1)我们要使用ElasticsearchSink里面Builder这个类,调用Builder里的build方法,这样能够给我们返回一个ElasticsearchSink的对象。
2)Builder这个类要求我们传两个参数,第一个参数是ES的集群地址和端口;第二个参数要求我们实现ElasticsearchSinkFunction这个类,这个类里面的process方法就是定义我们自己向ES写入数据的过程
4.JDBC
JDBC例如从Flink向MySQL写入数据。
这里以Flink->MySQL为例,首先导入MySQL的依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
使用方法:
mapResult.addSink(new MySinkFunction());
env.execute();
public static class MySinkFunction extends RichSinkFunction<SensorReading>{
Connection conn=null;
PreparedStatement insertStmt=null;
PreparedStatement updateStmt=null;
@Override
public void open(Configuration parameters) throws Exception {
conn = DriverManager.getConnection("jdbc://mysql://hadoop102:3306/test","root","123456");
//创建预编译器,有占位符,可传入参数
insertStmt = conn.prepareStatement("INSERT INTO sensor_temp (id,temp) VALUES(?,?)");
updateStmt = conn.prepareStatement("UPDATE TABLE sensor_temp set temp=? HWERE id=?");
}
@Override
public void close() throws Exception {
insertStmt.close();
updateStmt.close();
conn.close();
}
//调度,执行sql
@Override
public void invoke(SensorReading value, Context context) throws Exception {
//执行更新语句,如果更新失败,就插入
updateStmt.setDouble(1,value.getTemperature());
updateStmt.setString(2,value.getId());
updateStmt.execute();
//如果update语句没有改变行,就插入
if(updateStmt.getUpdateCount()==0){
insertStmt.setString(1, value.getId());
insertStmt.setDouble(2,value.getTemperature());
insertStmt.execute();
}
}
}
代码说明:
1)自定义sink需要实现SinkFunction类,但是因为是JDBC,需要连接数据库,如果对每一个数据都进行连接和释放,会大大消耗资源,所以实现RichSinkFunction类,利用生命周期的特点,在open函数里创建连接,在close函数里关闭连接,会提高性能。
2)再插入数据的时候,使用了预编译的方法,也提高了jdbc的性能。