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