Apache Flink 入门 (第四篇) (DataStream API 示例)

1. Flink DataStream API 概览

https://ci.apache.org/projects/flink/flink-docs-release-1.10/zh/dev/datastream_api.html

我们先是从一个简单的例子开始看起。下面是一个流式 Word Count 的示例,虽然它只有 5 行代码,但是它给出了基于 Flink DataStream API 开发程序的基本结构。

▼ 示例: 基于 Flink DataStream API 的 Word Count 示例。

//1、设置运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//2、配置数据源读取数据
DataStream<String> text = env.readTextFile ("input");
//3、进行一系列转换
DataStream<Tuple2<String, Integer>> counts = text.flatMap(new Tokenizer()).keyBy(0).sum(1);
//4、配置数据汇写出数据
counts.writeAsText("output");
//5、提交执行
env.execute("Streaming WordCount");

为了实现流式 Word Count,我们首先要先获得一个 StreamExecutionEnvironment 对象。它是我们构建图过程中的上下文对象。基于这个对象,我们可以添加一些算子。对于流处理程度,我们一般需要首先创建一个数据源去接入数据。
在这个例子中,我们使用了 Environment 对象中内置的读取文件的数据源。这一步之后,我们拿到的是一个 DataStream 对象,它可以看作一个无限的数据集,可以在该集合上进行一序列的操作。例如,在 Word Count 例子中,我们首先将每一条记录(即文件中的一行)分隔为单词,这是通过 FlatMap 操作来实现的。调用 FlatMap 将会在底层的 DAG 图中添加一个 FlatMap 算子。然后,我们得到了一个记录是单词的流。我们将流中的单词进行分组(keyBy),然后累积计算每一个单词的数据(sum(1))。计算出的单词的数据组成了一个新的流,我们将它写入到输出文件中。

最后,我们需要调用 env#execute 方法来开始程序的执行。需要强调的是,前面我们调用的所有方法,都不是在实际处理数据,而是在构通表达计算逻辑的 DAG 图。只有当我们将整个图构建完成并显式的调用 Execute 方法后,框架才会把计算图提供到集群中,接入数据并执行实际的逻辑。

基于流式 Word Count 的例子可以看出,基于 Flink 的 DataStream API 来编写流处理程序一般需要三步:
通过 Source 接入数据、进行一系统列的处理以及将数据写出。最后,不要忘记显式调用 Execute 方式,否则前面编写的逻辑并不会真正执行

在这里插入图片描述
从上面的例子中还可以看出,Flink DataStream API 的核心,就是代表流数据的 DataStream 对象。整个计算逻辑图的构建就是围绕调用 DataStream 对象上的不同操作产生新的 DataStream 对象展开的。
整体来说,DataStream 上的操作可以分为四类。第一类是对于单条记录的操作,比如筛除掉不符合要求的记录(Filter 操作),或者将每条记录都做一个转换(Map 操作)。第二类是对多条记录的操作。比如说统计一个小时内的订单总成交量,就需要将一个小时内的所有订单记录的成交量加到一起。为了支持这种类型的操作,就得通过 Window 将需要的记录关联到一起进行处理。第三类是对多个流进行操作并转换为单个流。例如,多个流可以通过 Union、Join 或 Connect 等操作合到一起。这些操作合并的逻辑不同,但是它们最终都会产生了一个新的统一的流,从而可以进行一些跨流的操作。最后, DataStream 还支持与合并对称的操作,即把一个流按一定规则拆分为多个流(Split 操作),每个流是之前流的一个子集,这样我们就可以对不同的流作不同的处理。
在这里插入图片描述

为了支持这些不同的流操作,Flink 引入了一组不同的流类型,用来表示某些操作的中间流数据集类型。完整的类型转换关系如上图所示。

我们再看一个更复杂的例子。假设我们有一个数据源,它监控系统中订单的情况,当有新订单时,它使用 Tuple2<String, Integer> 输出订单中商品的类型和交易额。然后,我们希望实时统计每个类别的交易额,以及实时统计全部类别的交易额。

▼ 示例: 实时订单统计示例。

package com.wzw.streaming;

import org.apache.flink.api.common.functions.FoldFunction;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple;
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.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;

import java.util.HashMap;
import java.util.Random;

/**
 * @description 实时订单处理 Streaming API演示
 * @author: ZhiWen
 * @create: 2020-07-05 17:12
 **/
public class OrderProcessSample {

    /**
     *功能描述 首先,在该实现中,我们首先实现了一个模拟的数据源,
     * 它继承自 RichParallelSourceFunction,它是可以有多个实例的 SourceFunction 的接口。
     * 它有两个方法需要实现,一个是 Run 方法,Flink 在运行时对 Source 会直接调用该方法,该方法需要不断的输出数据,从而形成初始的流。
     * 在 Run 方法的实现中,我们随机的产生商品类别和交易量的记录,然后通过 ctx#collect 方法进行发送。
     * 另一个方法是 Cancel 方法,当 Flink 需要 Cancel Source Task 的时候会调用该方法,我们使用一个 Volatile 类型的变量来标记和控制执行的状态。
     * @author ZhiWen
     * @param  * @param null
     * @return
     */
    private static class DataSource extends RichParallelSourceFunction<Tuple2<String,Integer>>{

        private volatile boolean isRunning = true;
        @Override
        public void run(SourceContext<Tuple2<String, Integer>> sourceContext) throws Exception {
            Random random = new Random();
            while (isRunning) {
                Thread.sleep((getRuntimeContext().getIndexOfThisSubtask() + 1) * 1000 * 5);
                String key = "类别" + (char) ('A' + random.nextInt(3));
                int value = random.nextInt(10) + 1;

                System.out.println(String.format("Emits\t(%s, %d)", key, value));
                sourceContext.collect(new Tuple2<>(key, value));
            }
        }

        @Override
        public void cancel() {
            isRunning = false;
        }
    }

    /**
     *功能描述
     * @author ZhiWen
     * @param  * @param args
     * @return
     */
    public static void main(String[] args) throws Exception {
        /**我们首先创建了一个 StreamExecutioniEnviroment 对象。
         * 创建对象调用的 getExecutionEnvironment 方法会自动判断所处的环境,从而创建合适的对象。
         * 例如,如果我们在 IDE 中直接右键运行,则会创建 LocalStreamExecutionEnvironment 对象;
         * 如果是在一个实际的环境中,则会创建 RemoteStreamExecutionEnvironment 对象。*/
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);

        /**基于 Environment 对象,我们首先创建了一个 Source,从而得到初始的<商品类型,成交量>流。
         * 然后,为了统计每种类别的成交量,我们使用 KeyBy 按 Tuple 的第 1 个字段(即商品类型)对输入流进行分组,并对每一个 Key 对应的记录的第 2 个字段(即成交量)进行求合。
         * 在底层,Sum 算子内部会使用 State 来维护每个Key(即商品类型)对应的成交量之和。
         * 当有新记录到达时,Sum 算子内部会更新所维护的成交量之和,并输出一条<商品类型,更新后的成交量>记录*/
        DataStreamSource<Tuple2<String, Integer>> ds = env.addSource(new DataSource());
        KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = ds.keyBy(0);

        keyedStream.sum(1)
        /**如果只统计各个类型的成交量,则程序可以到此为止,我们可以直接在 Sum 后添加一个 Sink 算子对不断更新的各类型成交量进行输出。
         * 但是,我们还需要统计所有类型的总成交量。
         * 为了做到这一点,我们需要将所有记录输出到同一个计算节点的实例上。
         * 我们可以通过 KeyBy 并且对所有记录返回同一个 Key,将所有记录分到同一个组中,从而可以全部发送到同一个实例上。
         */
                .keyBy(new KeySelector<Tuple2<String,Integer>, Object>() {
            @Override
            public Object getKey(Tuple2<String, Integer> stringIntegerTuple2) throws Exception {
                return "";
            }
        })
        /** 然后,我们使用 Fold 方法来在算子中维护每种类型商品的成交量。
         * 注意虽然目前 Fold 方法已经被标记为 Deprecated,但是在 DataStream API 中暂时还没有能替代它的其它操作,所以我们仍然使用 Fold 方法。
         * 这一方法接收一个初始值,然后当后续流中每条记录到达的时候,算子会调用所传递的 FoldFunction 对初始值进行更新,并发送更新后的值。
         * 我们使用一个 HashMap 来对各个类别的当前成交量进行维护,当有一条新的<商品类别,成交量>到达时,我们就更新该 HashMap。
         * 这样在 Sink 中,我们收到的是最新的商品类别和成交量的 HashMap,我们可以依赖这个值来输出各个商品的成交量和总的成交量。*/
        .fold(new HashMap<String, Integer>(), new FoldFunction<Tuple2<String, Integer>, HashMap<String, Integer>>() {
            @Override
            public HashMap<String, Integer> fold(HashMap<String, Integer> accumulator, Tuple2<String, Integer> value) throws Exception {
                accumulator.put(value.f0,value.f1);
                return accumulator;
            }
        }).addSink(new SinkFunction<HashMap<String, Integer>>() {
            @Override
            public void invoke(HashMap<String, Integer> value, Context context) throws Exception {
                // 每个类型的商品成交量
                System.out.println(value);
                // 商品成交总量
                System.out.println(value.values().stream().mapToInt(v -> v).sum());
            }
        });

        env.execute("sample");

    }
    /*Emits	(类别A, 9)
    {类别A=9}
    9
    Emits	(类别B, 5)
    Emits	(类别B, 2)
    {类别A=9, 类别B=5}
    14
    {类别A=9, 类别B=7}
    16
    Emits	(类别A, 5)
    {类别A=14, 类别B=7}
    21
    Emits	(类别C, 4)
    Emits	(类别A, 8)
    {类别A=14, 类别C=4, 类别B=7}
    25
    {类别A=22, 类别C=4, 类别B=7}
    33*/

}

最后,我们对 DataStream API 的原理进行简要的介绍。当我们调用 DataStream#map 算法时,Flink 在底层会创建一个 Transformation 对象,这一对象就代表我们计算逻辑图中的节点。它其中就记录了我们传入的 MapFunction,也就是 UDF(User Define Function)。随着我们调用更多的方法,我们创建了更多的 DataStream 对象,每个对象在内部都有一个 Transformation 对象,这些对象根据计算依赖关系组成一个图结构,就是我们的计算图。后续 Flink 将对这个图结构进行进一步的转换,从而最终生成提交作业所需要的 JobGraph。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值