Flink DataStream API編程指南

Flink DataStream API主要分爲三個部分,分別爲Source、Transformation以及Sink,其中Source是數據源,Flink內置了很多數據源,比如最常用的Kafka。Transformation是具體的轉換操作,主要是用戶定義的處理數據的邏輯,比如Map,FlatMap等。Sink(數據匯)是數據的輸出,可以把處理之後的數據輸出到存儲設備上,Flink內置了許多的Sink,比如Kafka,HDFS等。另外除了Flink內置的Source和Sink外,用戶可以實現自定義的Source與Sink。考慮到內置的Source與Sink使用起來比較簡單且方便,所以,關於內置的Source與Sink的使用方式不在本文的討論範圍之內,本文會先從自定義Source開始說起,然後詳細描述一些常見算子的使用方式,最後會實現一個自定義的Sink。

數據源

Flink內部實現了比較常用的數據源,比如基於文件的,基於Socket的,基於集合的等等,如果這些都不能滿足需求,用戶可以自定義數據源,下面將會以MySQL爲例,實現一個自定義的數據源。本文的所有操作將使用該數據源,具體代碼如下:

/**
 *  @Created with IntelliJ IDEA.
 *  @author : jmx
 *  @Date: 2020/4/14
 *  @Time: 17:34
 * note: RichParallelSourceFunction與SourceContext必須加泛型
 */
public class MysqlSource extends RichParallelSourceFunction<UserBehavior> {
    public Connection conn;
    public PreparedStatement pps;
    private String driver;
    private String url;
    private String user;
    private String pass;

    /**
     * 該方法只會在最開始的時候被調用一次
     * 此方法用於實現獲取連接
     *
     * @param parameters
     * @throws Exception
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        //初始化數據庫連接參數
        Properties properties = new Properties();
        URL fileUrl = TestProperties.class.getClassLoader().getResource("mysql.ini");
        FileInputStream inputStream = new FileInputStream(new File(fileUrl.toURI()));
        properties.load(inputStream);
        inputStream.close();
        driver = properties.getProperty("driver");
        url = properties.getProperty("url");
        user = properties.getProperty("user");
        pass = properties.getProperty("pass");
        //獲取數據連接
        conn = getConection();
        String scanSQL = "SELECT * FROM user_behavior_log";
        pps = conn.prepareStatement(scanSQL);
    }

    @Override
    public void run(SourceContext<UserBehavior> ctx) throws Exception {
        ResultSet resultSet = pps.executeQuery();
        while (resultSet.next()) {
            ctx.collect(UserBehavior.of(
                    resultSet.getLong("user_id"),
                    resultSet.getLong("item_id"),
                    resultSet.getInt("cat_id"),
                    resultSet.getInt("merchant_id"),
                    resultSet.getInt("brand_id"),
                    resultSet.getString("action"),
                    resultSet.getString("gender"),
                    resultSet.getLong("timestamp")));
        }
    }
    @Override
    public void cancel() {

    }
    /**
     * 實現關閉連接
     */
    @Override
    public void close() {
        if (pps != null) {
            try {
                pps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 獲取數據庫連接
     *
     * @return
     * @throws SQLException
     */
    public Connection getConection() throws IOException {
        Connection connnection = null;

        try {
            //加載驅動
            Class.forName(driver);
            //獲取連接
            connnection = DriverManager.getConnection(
                    url,
                    user,
                    pass);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connnection;

    }
}

首先繼承RichParallelSourceFunction,實現繼承的方法,主要包括open()方法、run()方法及close方法。上述的

RichParallelSourceFunction是支持設置多並行度的,關於RichParallelSourceFunction與RichSourceFunction的區別,前者支持用戶設置多並行度,後者不支持通過setParallelism()方法設置並行度,默認的並行度爲1,否則會報如下錯誤:bashException in thread "main" java.lang.IllegalArgumentException: The maximum parallelism of non parallel operator must be 1.

另外,RichParallelSourceFunction提供了額外的open()方法與close()方法,如果定義Source時需要獲取鏈接,那麼可以在open()方法中進行初始化,然後在close()方法中關閉資源鏈接,關於Rich***Function與普通Function的區別,下文會詳細解釋,在這裏先有個印象。上述的代碼中的配置信息是通過配置文件傳遞的,由於篇幅限制,我會把本文的代碼放置在github,見文末github地址。

基本轉換

Flink提供了大量的算子操作供用戶使用,常見的算子主要包括以下幾種,注意:本文不討論關於基於時間與窗口的算子,這些內容會在《Flink基於時間與窗口的算子》中進行詳細介紹。

說明:本文的操作是基於上文自定義的MySQL Source,對應的數據解釋如下:

userId;     // 用戶ID
itemId;     // 商品ID
catId;      // 商品類目ID
merchantId; // 賣家ID
brandId;    // 品牌ID
action;     // 用戶行爲, 包括("pv", "buy", "cart", "fav")
gender;     // 性別
timestamp;  // 行爲發生的時間戳,單位秒

Map

解釋

DataStream → DataStream 的轉換,輸入一個元素,返回一個元素,如下操作:

SingleOutputStreamOperator<String> userBehaviorMap = userBehavior.map(new RichMapFunction<UserBehavior, String>() {
            @Override
            public String map(UserBehavior value) throws Exception {
                String action = "";
                switch (value.action) {
                    case "pv":
                        action = "瀏覽";
                    case "cart":
                        action = "加購";
                    case "fav":
                        action = "收藏";
                    case "buy":
                        action = "購買";
                }
                return action;
            }
        });

示意圖

將雨滴形狀轉換成相對應的圓形形狀的map操作

flatMap

解釋

DataStream → DataStream,輸入一個元素,返回零個、一個或多個元素。事實上,flatMap算子可以看做是filter與map的泛化,即它能夠實現這兩種操作。flatMap算子對應的FlatMapFunction定義了flatMap方法,可以通過向collector對象傳遞數據的方式返回0個,1個或者多個事件作爲結果。如下操作:

SingleOutputStreamOperator<UserBehavior> userBehaviorflatMap = userBehavior.flatMap(new RichFlatMapFunction<UserBehavior, UserBehavior>() {
            @Override
            public void flatMap(UserBehavior value, Collector<UserBehavior> out) throws Exception {
                if (value.gender.equals("女")) {
                    out.collect(value);
                }
            }
        });

示意圖

將黃色的雨滴過濾掉,將藍色雨滴轉爲圓形,保留綠色雨滴

Filter

解釋

DataStream → DataStream,過濾算子,對數據進行判斷,符合條件即返回true的數據會被保留,否則被過濾。如下:

  SingleOutputStreamOperator<UserBehavior> userBehaviorFilter = userBehavior.filter(new RichFilterFunction<UserBehavior>() {
            @Override
            public boolean filter(UserBehavior value) throws Exception {
                return value.action.equals("buy");//保留購買行爲的數據
            }
        });

示意圖

將紅色與綠色雨滴過濾掉,保留藍色雨滴。

keyBy

解釋

DataStream→KeyedStream,從邏輯上將流劃分爲不相交的分區。具有相同鍵的所有記錄都分配給同一分區。在內部,keyBy()是通過哈希分區實現的。
定義鍵值有3中方式:
(1)使用字段位置,如keyBy(1),此方式是針對元組數據類型,比如tuple,使用元組相應元素的位置來定義鍵值;
(2)字段表達式,用於元組、POJO以及樣例類;
(3)鍵值選擇器,即keySelector,可以從輸入事件中提取鍵值

SingleOutputStreamOperator<Tuple2<String, Integer>> userBehaviorkeyBy = userBehavior.map(new RichMapFunction<UserBehavior, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(UserBehavior value) throws Exception {
                return Tuple2.of(value.action.toString(), 1);
            }
        }).keyBy(0) // scala元組編號從1開始,java元組編號是從0開始
           .sum(1); //滾動聚合

示意圖

基於形狀對事件進行分區的keyBy操作

Reduce

解釋

KeyedStream → DataStream,對數據進行滾動聚合操作,結合當前元素和上一次Reduce返回的值進行聚合,然後返回一個新的值.將一個ReduceFunction應用在一個keyedStream上,每到來一個事件都會與當前reduce的結果進行聚合,
產生一個新的DataStream,該算子不會改變數據類型,因此輸入流與輸出流的類型永遠保持一致。

SingleOutputStreamOperator<Tuple2<String, Integer>> userBehaviorReduce = userBehavior.map(new RichMapFunction<UserBehavior, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(UserBehavior value) throws Exception {
                return Tuple2.of(value.action.toString(), 1);
            }
        }).keyBy(0) // scala元組編號從1開始,java元組編號是從0開始
          .reduce(new RichReduceFunction<Tuple2<String, Integer>>() {
              @Override
              public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                  return Tuple2.of(value1.f0,value1.f1 + value2.f1);//滾動聚合,功能與sum類似
              }
          });

示意圖

 

Aggregations(滾動聚合)

KeyedStream → DataStream,Aggregations(滾動聚合),滾動聚合轉換作用於KeyedStream流上,生成一個包含聚合結果(比如sum求和,min最小值)的DataStream,滾動聚合的轉換會爲每個流過該算子的key值保存一個聚合結果,
當有新的元素流過該算子時,會根據之前的結果值和當前的元素值,更新相應的結果值

  • sum():滾動聚合流過該算子的指定字段的和;

  • min():滾動計算流過該算子的指定字段的最小值

  • max():滾動計算流過該算子的指定字段的最大值

  • minBy():滾動計算當目前爲止流過該算子的最小值,返回該值對應的事件;

  • maxBy():滾動計算當目前爲止流過該算子的最大值,返回該值對應的事件;

union

解釋

DataStream* → DataStream,將多條流合併,新的的流會包括所有流的數據,值得注意的是,兩個流的數據類型必須一致,另外,來自兩條流的事件會以FIFO(先進先出)的方式合併,所以並不能保證兩條流的順序,此外,union算子不會對數據去重,每個輸入事件都會被髮送到下游算子。

userBehaviorkeyBy.union(userBehaviorReduce).print();//將兩條流union在一起,可以支持多條流(大於2)的union

示意圖

 

connect

解釋

DataStream,DataStream → ConnectedStreams,將兩個流的事件進行組合,返回一個ConnectedStreams對象,兩個流的數據類型可以不一致,ConnectedStreams對象提供了類似於map(),flatMap()功能的算子,如CoMapFunction與CoFlatMapFunction分別表示map()與flatMap算子,這兩個算子會分別作用於兩條流,注意:CoMapFunction 或CoFlatMapFunction被調用的時候並不能控制事件的順序只要有事件流過該算子,該算子就會被調用。

ConnectedStreams<UserBehavior, Tuple2<String, Integer>> behaviorConnectedStreams = userBehaviorFilter.connect(userBehaviorkeyBy);
        SingleOutputStreamOperator<Tuple3<String, String, Integer>> behaviorConnectedStreamsmap = behaviorConnectedStreams.map(new RichCoMapFunction<UserBehavior, Tuple2<String, Integer>, Tuple3<String, String, Integer>>() {
            @Override
            public Tuple3<String, String, Integer> map1(UserBehavior value1) throws Exception {
                return Tuple3.of("first", value1.action, 1);
            }
            @Override
            public Tuple3<String, String, Integer> map2(Tuple2<String, Integer> value2) throws Exception {
                return Tuple3.of("second", value2.f0, value2.f1);
            }
        });

split

解釋

DataStream → SplitStream,將流分割成兩條或多條流,與union相反。分割之後的流與輸入流的數據類型一致,
對於每個到來的事件可以被路由到0個、1個或多個輸出流中。可以實現過濾與複製事件的功能,DataStream.split()接收一個OutputSelector函數,用來定義分流的規則,即將滿足不同條件的流分配到用戶命名的一個輸出。

 SplitStream<UserBehavior> userBehaviorSplitStream = userBehavior.split(new OutputSelector<UserBehavior>() {
            @Override
            public Iterable<String> select(UserBehavior value) {
                ArrayList<String> userBehaviors = new ArrayList<String>();
                if (value.action.equals("buy")) {
                    userBehaviors.add("buy");
                } else {
                    userBehaviors.add("other");
                }
                return userBehaviors;
            }
        });
userBehaviorSplitStream.select("buy").print();

示意圖

Sink

Flink提供了許多內置的Sink,比如writeASText,print,HDFS,Kaka等等,下面將基於MySQL實現一個自定義的Sink,可以與自定義的MysqlSource進行對比,具體如下:

/**
 *  @Created with IntelliJ IDEA.
 *  @author : jmx
 *  @Date: 2020/4/16
 *  @Time: 22:53
 *  
 */
public class MysqlSink extends RichSinkFunction<UserBehavior> {
    PreparedStatement pps;
    public Connection conn;
    private String driver;
    private String url;
    private String user;
    private String pass;
    /**
     * 在open() 方法初始化連接
     *
     * @param parameters
     * @throws Exception
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        //初始化數據庫連接參數
        Properties properties = new Properties();
        URL fileUrl = TestProperties.class.getClassLoader().getResource("mysql.ini");
        FileInputStream inputStream = new FileInputStream(new File(fileUrl.toURI()));
        properties.load(inputStream);
        inputStream.close();
        driver = properties.getProperty("driver");
        url = properties.getProperty("url");
        user = properties.getProperty("user");
        pass = properties.getProperty("pass");
        //獲取數據連接
        conn = getConnection();
        String insertSql = "insert into user_behavior values(?, ?, ?, ?,?, ?, ?, ?);";
        pps = conn.prepareStatement(insertSql);
    }

    /**
     * 實現關閉連接
     */
    @Override
    public void close() {

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (pps != null) {
            try {
                pps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 調用invoke() 方法,進行數據插入
     *
     * @param value
     * @param context
     * @throws Exception
     */
    @Override
    public void invoke(UserBehavior value, Context context) throws Exception {
        pps.setLong(1, value.userId);
        pps.setLong(2, value.itemId);
        pps.setInt(3, value.catId);
        pps.setInt(4, value.merchantId);
        pps.setInt(5, value.brandId);
        pps.setString(6, value.action);
        pps.setString(7, value.gender);
        pps.setLong(8, value.timestamp);
        pps.executeUpdate();
    }
    /**
     * 獲取數據庫連接
     *
     * @return
     * @throws SQLException
     */
    public Connection getConnection() throws IOException {
        Connection connnection = null;

        try {
            //加載驅動
            Class.forName(driver);
            //獲取連接
            connnection = DriverManager.getConnection(
                    url,
                    user,
                    pass);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connnection;
    }
}

關於RichFunction

細心的讀者可以發現,在前文的算子操作案例中,使用的都是RichFunction,因爲在很多時候需要在函數處理數據之前先進行一些初始化操作,或者獲取函數的上下文信息,DataStream API提供了一類RichFunction,與普通的函數相比,該函數提供了許多額外的功能。

使用RichFunction的時候,可以實現兩個額外的方法:

  • open(),是初始化方法,會在每個人物首次調用轉換方法(比如map)前調用一次。通常用於進行一次的設置工作,注意Configuration參數只在DataSet API中使用,而並沒有在DataStream API中使用,因此在使用DataStream API時,可以將其忽略。

  • close(),函數的終止方法 ,會在每個任務最後一次調用轉換方法後調用一次,通常用於資源釋放等操作。

此外用戶還可以通過getRuntimeContext()方法訪問函數的上下文信息(RuntimeContext),例如函數的並行度,函數所在subtask的編號以及執行函數的任務名稱,同時也可以訪問分區狀態。

總結

本文首先實現了自定義MySQL Source,然後基於MySql 的Source進行了一系列的算子操作,並對常見的算子操作進行詳細剖析,最後實現了一個自定義MySQL Sink,並對RichFunction進行了解釋。

代碼地址:https://github.com/jiamx/study-flink

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章