Flink DataStream API

实时分析是当前一个比较热门的数据处理技术,因为许多不同领域的数据都需要进行实时处理、计算。到目前为止,有很多技术提供实时的解决方案,包括Storm、Spark Streaming等。这些需求源自于物联网的应用程序需要存储、处理和实时或近实时分析,为了满足这种需求,Flink提供了数据流处理API即DataStream API。

在总结DataStream API之前,我们先简单的了解一下Flink程序的基本运行流程,不论是Data Streaming还是DataSet,Flink程序都主要由以下几个部分组成:1、获取运行时环境(getExecutionEnvironment());2、绑定数据源(addSource);3、对接收的书进行transformation操作;4、dataSink,指定数据计算的输出结果方式(输出到文件或直接打印);5、程序触发执行(execute(jobName))。从上述步骤中,可以看出,我们真正需要账务及操作的就三个过程:Source、Transformation、Sink。DataStream API,即对数据流进行流处理操作,将流式的数据抽象成分布式的数据流,用户可以方便地对分布式数据流进行各种操作,支持Java和Scala。

 

Flink DataStream API_第1张图片

Flink中的DataStream程序是实现在数据流上的transformation(如filtering, updating state, defining windows, aggregating)的普通程序。创建数据流的来源多种多样(如消息队列,Socket流、文件等)。程序通过Data Sink返回结果,如将数据写入文件,或标准输出。

一、DataStream Sources

Flink作为一款优秀的流式计算框架,可以实时的处理实时产生的数据流,只要数据不断,Flink就可以一直计算下去,Data Source就是数据源输入,你可以通过StreamExecutionEnvironment.addSource(sourceFunction)来为你的程序绑定一个数据源。flink提供了大量的已经实现好的source方法,此外我们也可以实现sourcefunction来自定义并行度为1的Source,也可以实现 ParallelSourceFunction 接口或者扩展 RichParallelSourceFunction 来自定义并行的 source。

1、基于socket

env.socketTextStream(hostname, port, delimiter) 从socker指定的ip地址和端口号处读取数据,元素可以通过一个分隔符切开。

package com.bigdata.flink.Stream;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

/**
 * 滑动窗口的计算
 *
 * 通过socket模拟产生单词数据 flink对其进行统计计数
 * 实现时间窗口:
 *              每隔1秒统计前两秒的数据
 */
public class SocketWindowWordCount {
    public static void main(String[] args) throws Exception{
        //定义端口号,通过cli接收
        int port;
        try{
            ParameterTool parameterTool = ParameterTool.fromArgs(args);
            port = parameterTool.getInt("port");
        }catch(Exception e){
            System.err.println("No port Set, use default port---java");
            port = 9000;
        }

        //获取运行时环境,必须要
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //绑定Source,通过master的nc -l 900 产生单词
        String hostname = "192.168.83.129";
        String delimiter = "\n";
        //连接socket 绑定数据源
        DataStreamSource socketWord = env.socketTextStream(hostname, port, delimiter);

        DataStream windowcounts = socketWord.flatMap(new FlatMapFunction() {
            public void flatMap(String value, Collector out) throws Exception {
                String[] splits = value.split("\\s");
                for (String word : splits) {
                    out.collect(new WordWithCount(word, 1));
                }
            }
        }).keyBy("word")
                .timeWindow(Time.seconds(2), Time.seconds(1))
                //.sum("count");//这里求聚合 可以用reduce和sum两种方式
                .reduce(new ReduceFunction() {
                    public WordWithCount reduce(WordWithCount a, WordWithCount b) throws Exception {
                        return new WordWithCount(a.word, a.count + b.count);
                    }
                });
        windowcounts.print().setParallelism(1);
        env.execute("socketWindow");
    }

    public static class  WordWithCount{
        public String word;
        public int count;
        //无参的构造函数
        public WordWithCount(){

        }
        //有参的构造函数
        public WordWithCount(String word, int count){
            this.count = count;
            this.word = word;
        }

        @Override
        public String toString() {
            return "WordWithCount{" +
                    "word='" + word + '\'' +
                    ", count=" + count +
                    '}';
        }
    }
}

 2、基于文件

readTextFile(path) , 读取指定路径文本文件,即符合 TextInputFormat 规范的文件,逐行读取并返回。这个方法一般用于测试环境。

3、基于集合

fromCollection(Collection) ,从 Java 的 Java.util.Collection 创建数据流。集合中的所有元素类型必须相同。

4、自定义数据源

addSource 可以实现读取第三方数据源的数据,目前个人觉得最重要的还是读取Kafka数据,这个在之后的章节再写。这里实现两种,第一种实现sourcefunction来自定义并行度为1的Source,第二种实现 ParallelSourceFunction 接口来自定义并行的 source。注意:这两种方法都需要指定数据类型,都在会报类型不匹配。

SourceFunction作为Flink中Source的根接口,定义了两种方法run()和cancel()。run (): 启动一个 source,即对接一个外部数据源然后 emit 元素形成 stream(大部分情况下会通过在该方法里运行一个 while 循环的形式来产生 stream);cancel(): 取消一个 source,也即将 run 中的循环 emit 元素的行为终止。

第一种:无并行度的source,编写数据源,实现run()和cancel()方法;

package com.bigdata.flink.CustormSource;

import org.apache.flink.streaming.api.functions.source.SourceFunction;

/**
 * 自定义实现并行度为1的source
 * 模拟产生从1开始的递增数字
 *
 * 注意:
 * sourceFunction 和 SourceContext都需要指定数据类型,如果不指定
 * 代码运行时会报类型不匹配
 */
public class MyNoParalleSource implements SourceFunction {

    private Long count = 0L;
    private boolean isRunning = true;
    /**
     * 主要的方法
     * 启动一个Source
     * 大部分情况下,都需要在这个run方法里实现一个循环,这样就可以循环产生数据了
     * @param ctx
     * @throws Exception
     */
    @Override
    public void run(SourceContext ctx) throws Exception {
        while (isRunning){
            ctx.collect(count);
            count++;
            Thread.sleep(1000);
        }
    }

    /**
     * 取消一个cancel的时候会调用的方法
     */
    @Override
    public void cancel() {

        isRunning = false;
    }
}

 获取运行时环境,绑定自定义的数据源MyNoParalleSource()

package com.bigdata.flink.CustormSource;

import com.bigdata.flink.CustormSource.MyNoParalleSource;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;

/**
 * 使用并行度为1的Source
 * Author ambrose
 * @Date 2019/3/12 16:07
 */
public class StreamingDemoWithMyNoParalleSource {

    public static void main(String[] args) throws Exception {
        //获取Flink运行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        //绑定数据源
        DataStreamSource text  = env.addSource(new MyNoParalleSource());

        //map操作
        DataStream num = text.map(new MapFunction() {

            @Override
            public Long map(Long value) throws Exception {
                System.out.println("接收数据:" + value);
                return value;
            }
        });

        //每两秒处理一次数据
        DataStream sum = num.timeWindowAll(Time.seconds(2)).sum(0);

        //打印到控制台,并行度为1
        sum.print().setParallelism(2);
        env.execute( "StreamingDemoWithMyNoParalleSource");
    }
}

 第二种:实现 ParallelSourceFunction 接口

package com.bigdata.flink.CustormSource;

import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;

/**
 * 与并行度为1的实现方式区别在于 标志位不一样
 * Author ambrose
 * @Date 2019/3/12 16:32
 */
public class MyParalleSource implements ParallelSourceFunction {
    private Long count = 0L;
    private boolean isRunning = true;
    /**
     * 主要的方法
     * 启动一个Source
     * 大部分情况下,都需要在这个run方法里实现一个循环,这样就可以循环产生数据了
     * @param ctx
     * @throws Exception
     */
    @Override
    public void run(SourceContext ctx) throws Exception {
        while (isRunning){
            ctx.collect(count);
            count++;
            Thread.sleep(1000);
        }
    }

    /**
     * 取消一个cancel的时候会调用的方法
     */
    @Override
    public void cancel() {

        isRunning = false;
    }
}

获取运行时环境,绑定数据源 new MyParalleSource(),设置并行度

package com.bigdata.flink.CustormSource;

import com.bigdata.flink.CustormSource.MyParalleSource;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;

/**
 * 使用并行度为1的Source
 * Author ambrose
 * @Date 2019/3/12 16:07
 */

public class StreamingDemoWithMyParalleSource {

    public static void main(String[] args) throws Exception {
        //获取Flink运行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        //绑定数据源
        DataStreamSource text  = env.addSource(new MyParalleSource()).setParallelism(2);

        //map操作
        DataStream num = text.map(new MapFunction() {

            @Override
            public Long map(Long value) throws Exception {
                System.out.println("接收数据为:" + value);
                return value;
            }
        });

        //每两秒处理一次数据
        DataStream sum = num.timeWindowAll(Time.seconds(2)).sum(0);

        //打印到控制台,并行度为1
        sum.print().setParallelism(1);
        env.execute( "StreamingDemoWithMyNoParalleSource");
    }
}

二、DataStream Transformation

Data transformation会将一或多个DataStream转换成一个新的DataStream。程序可以将多个transformation结合形成复杂的拓扑结构(topology)。Transformation其实在flink中是处于核心位置,所有数据的转换、处理都是依靠这个部分。

Transformation 描述

Map

输入一个element,返回一个element。中间可以做一些清洗转换等操作

FlapMap 输入一个element,可以返回出0、1或多个element。
Filter 过滤函数,对传入的数据进行判断,符合条件的数据会被留下

KeyBy

DataStream -> KeyedStream

根据指定的key进行分组,相同key的数据会进入同一个分区

dataStream.keyBy("someKey"// Key by field "someKey"
dataStream.keyBy(0// Key by the first element of a Tuple

Reduce 对数据进行聚合操作,结合当前元素和上一次reduce返回的值进行聚合操作,然后返回一个新的值
Aggregations sum(),min(),max()等聚合操作

Window

KeyedStream - > WindowedStream

Window可以定义在已经分区的KeyedStream上。窗口将根据一些特征(如最近5秒到达的数据)将数据按其各自的key集合在一起。

dataStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(5)));

WindowAll

DataStream -> AllWindowedStream

Window可以定义在普通的DataStream上。窗口将根据一些特征(如最近5秒到达的数据)将所有Stream事件集合在一起。

dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));

Union

将2个或多个data stream合并创建出一个新的包含所有stream的element的stream

,要求合并的两个流类型必须一致

Connect 和union类似,但是只能连接两个流,两个流的数据类型可以不同,会对两个流中的数据应用不同的处理方法。
Split 根据某些标准将Stream分割成2个或更多的stream

Select

SplitStream -> DataStream

从SplitStream中选择1个或多个stream

SplitStream<Integer> split;
DataStream<Integer> even = split.select("even");
DataStream<Integer> odd = split.select("odd");
DataStream<Integer> all = split.select("even","odd");

三、DataStream Sink

Sink是Flink处理的最后一步,即将实时transformation后的计算结果“落地”到某个地方,可以是Mysql、ES、Kafka、redis等。对应Source的SourceFunction(),Sink也对应有SinkFunction(),SinkFunction()也是sink的根接口,在自定义SinkFunction时要继承RichSinkFunction抽象类,实现其中的方法,包括open()、invoke()、close(),其中最重要的实现就是invoke方法。

下面我们通过平时经常用的.print()方法的源码,简单认识一下SinkFunction()的使用

@PublicEvolving
public class PrintSinkFunction extends RichSinkFunction {
	private static final long serialVersionUID = 1L;
	private static final boolean STD_OUT = false;
	private static final boolean STD_ERR = true;
	private boolean target;
	private transient PrintStream stream;
	private transient String prefix;

	//实例化标准输出的sink方法
	public PrintSinkFunction() {}

	//如果格式化打印为stdErr而不是stdOut,置stderr true
	public PrintSinkFunction(boolean stdErr) {
		target = stdErr;
	}

	public void setTargetToStandardOut() {
		target = STD_OUT;
	}

	public void setTargetToStandardErr() {
		target = STD_ERR;
	}
    //重写open方法
	@Override
	public void open(Configuration parameters) throws Exception {
		super.open(parameters);
		StreamingRuntimeContext context = (StreamingRuntimeContext) getRuntimeContext();
		// get the target stream
		stream = target == STD_OUT ? System.out : System.err;

		// set the prefix if we have a >1 parallelism
		prefix = (context.getNumberOfParallelSubtasks() > 1) ?
				((context.getIndexOfThisSubtask() + 1) + "> ") : null;
	}

	@Override
	public void invoke(IN record) {
		if (prefix != null) {
			stream.println(prefix + record.toString());
		}
		else {
			stream.println(record.toString());
		}
	}

	@Override
	public void close() {
		this.stream = null;
		this.prefix = null;
	}

	@Override
	public String toString() {
		return "Print to " + (target == STD_OUT ? "System.out" : "System.err");
	}
}

我们可以通过使用result.addSink(new PrintSinkFunction<>());的方式将结果集直接答应在控制台上,也可以使用result.print();打印结果集,这两种方法的效果是一样的。

你可能感兴趣的:(大数据-Flink,Flink实时计算)