Flink之數據類型詳解

一、數據類型支持

Flink支持非常完善的數據類型,數據類型的描述信息都是由TypeInformation定義,比較常用的TypeInformation有BasicTypeInfo、TupleTypeInfo、CaseClassTypeInfo、PojoTypeInfo類等。TypeInformation主要作用是爲了在Flink系統內有效地對數據結構類型進行管理,能夠在分佈式計算過程中對數據的類型進行管理和推斷。同時基於對數據的類型信息管理,Flink內部對數據存儲也進行了相應的性能優化。

Flink能夠支持任意的Java或Scala的數據類型,不用像Hadoop中的org.apache.hadoop.io.Writable而實現特定的序列化和反序列化接口,從而讓用戶能夠更加容易使用已有的數據結構類型。另外使用TypeInformation管理數據類型信息,能夠在數據處理之前將數據類型推斷出來,而不是真正在觸發計算後才識別出,這樣能夠及時有效地避免用戶在使用Flink編寫應用的過程中的數據類型問題。

1.1、 原生數據類型

Flink通過實現BasicTypeInfo數據類型,能夠支持任意Java 原生基本類型(裝箱)或 String 類型,例如Integer、String、Double等,如以下代碼所示,通過從給定的元素集中創建DataStream數據集。

image.png

Flink實現另外一種TypeInfomation是BasicArrayTypeInfo,對應的是Java基本類型數組(裝箱)或 String對象的數組,如下代碼通過使用Array數組和List集合創建DataStream數據集。

image.png

1.2、Java Tuples類型

通過定義TupleTypeInfo來描述Tuple類型數據,Flink在Java接口中定義了元祖類(Tuple)供用戶使用。Flink Tuples是固定長度固定類型的Java Tuple實現,不支持空值存儲。目前支持任意的Flink Java Tuple類型字段數量上限爲25如果字段數量超過上限,可以通過繼承Tuple類的方式進行拓展。如下代碼所示,創建Tuple數據類型數據集。

image.png

1.3、Scala Case Class類型

Flink通過實現CaseClassTypeInfo支持任意的Scala Case Class,包括Scala tuples類型,支持的字段數量上限爲22,支持通過字段名稱和位置索引獲取指標,不支持存儲空值。如下代碼實例所示,定義WordCount Case Class數據類型,然後通過fromElements方法創建input數據集,調用keyBy()方法對數據集根據word字段重新分區。

image.png

通過使用Scala Tuple創建DataStream數據集,其他的使用方式和Case Class相似。需要注意的是,如果根據名稱獲取字段,可以使用Tuple中的默認字段名稱。

image.png

注:不同於Scala,Java中的元組是從“0”開始計數。 

Tuple2<String, Integer> tuple = new Tuple2("Flink", 1);
System.out.println(tuple.getField(0).toString());
System.out.println(tuple.getField(1).toString());
System.out.println("-----------------------------");

tuple.setField("Scala",0);
// 類型擦除,可以設置非Integer類型的數據
tuple.setField("java",1);
// 推薦獲取元組值的方式
System.out.println(tuple.f0);
System.out.println(tuple.f1);       

1.4、POJOs類型

POJOs類可以完成複雜數據結構的定義,Flink通過實現PojoTypeInfo來描述任意的POJOs,包括Java 和Scala類。在Flink中使用POJOs類可以通過字段名稱獲取字段,例如dataStream.join(otherStream).where("name").equalTo("personName"),對於用戶做數據處理則非常透明和簡單,如代碼清單3-2所示。如果在Flink中使用POJOs數據類型,需要遵循以下要求:

  • POJOs 類必須是Public修飾且必須獨立定義,不能是內部類;
  • POJOs類中必須含有默認空構造器;
  • POJOs類中所有的Fields必須是Public或者具有Public修飾的getter和setter方法;
  • POJOs類中的字段類型必須是Flink支持的。

image.png

定義好POJOs Class後,就可以在Flink環境中使用了,如下代碼所示,使用fromElements接口構建Person類的數據集。POJOs類僅支持字段名稱指定字段,如代碼中通過Person name來指定Keyby字段

image.png

Scala POJOs數據結構定義如下,使用方式與Java POJOs相同。

image.png

1.5、Flink Value類型

Value數據類型實現了org.apache.flink.types.Value,其中包括read()和write()兩個方法完成序列化和反序列化操作,相對於通用的序列化工具會有着比較高效的性能。目前Flink提供了內建的Value類型有IntValue、DoubleValue以及StringValue等,用戶可以結合原生數據類型和Value類型使用。

1.6、 特殊數據類型

在Flink中也支持一些比較特殊的數據數據類型,例如Scala中的List、Map、Either、Option、Try數據類型,以及Java中Either數據類型,還有Hadoop的Writable數據類型。如下代碼所示,創建Map和List類型數據集。這種數據類型使用場景不是特別廣泛,主要原因是數據中的操作相對不像POJOs類那樣方便和透明,用戶無法根據字段位置或者名稱獲取字段信息,同時要藉助Types Hint幫助Flink推斷數據類型信息,關於Tyeps Hmt介紹可以參考下一小節。

image.png

二、TypeInformation信息獲取

通常情況下Flink都能正常進行數據類型推斷,並選擇合適的serializers以及comparators。但在某些情況下卻無法直接做到,例如定義函數時如果使用到了泛型,JVM就會出現類型擦除的問題,使得Flink並不能很容易地獲取到數據集中的數據類型信息。同時在Scala API和Java API中,Flink分別使用了不同的方式重構了數據類型信息。

2.1、 Scala API類型信息

Scala API通過使用Manifest和類標籤,在編譯器運行時獲取類型信息,即使是在函數定義中使用了泛型,也不會像Java API出現類型擦除的問題,這使得Scala API具有非常精密的類型管理機制。同時在Flink中使用到Scala Macros框架,在編譯代碼的過程中推斷函數輸入參數和返回值的類型信息,同時在Flink中註冊成TypeInformation以支持上層計算算子使用。

當使用Scala API開發Flink應用,如果使用到Flink已經通過TypeInformation定義的數據類型,TypeInformation類不會自動創建,而是使用隱式參數的方式引入,代碼不會直接拋出編碼異常,但是當啓動Flink應用程序時就會報”could not find implicit value for evidence parameter of type TypeInformation”的錯誤。這時需要將TypeInformation類隱式參數引入到當前程序環境中,

代碼實例如下:

import org.apache.flink.api.scala._

2.2、Java API類型信息

由於Java的泛型會出現類型擦除問題,Flink通過Java反射機制儘可能重構類型信息,例如使用函數簽名以及子類的信息等。同時類型推斷在當輸出類型依賴於輸入參數類型時相對比較容易做到,但是如果函數的輸出類型不依賴於輸入參數的類型信息,這個時候就需要藉助於類型提示(Ctype Himts)來告訴系統函數中傳入的參數類型信息和輸出參數信息。如代碼清單3-3通過在returns方法中傳入TypeHint實例指定輸出參數類型,幫助Flink系統對輸出類型進行數據類型參數的推斷和收集。

image.png

在使用Java API定義POJOs類型數據時,PojoTypeInformation爲POJOs類中的所有字段創建序列化器,對於標準的類型,例如Integer、String、Long等類型是通過Flink自帶的序列化器進行數據序列化,對於其他類型數據都是直接調用Kryo序列化工具來進行序列化。

通常情況下,如果Kryo序列化工具無法對POJOs類序列化時,可以使用Avro對POJOs類進行序列化,如下代碼通過在ExecutionConfig中調用enableForceAvro()來開啓Avro序列化

image.png

如果用戶想使用Kryo序列化工具來序列化POJOs所有字段,則在ExecutionConfig中調用enableForceKryo()來開啓Kryo序列化。

image.png

如果默認的Kryo序列化類不能序列化POJOs對象,通過調用ExecutionConfig的addDefault-KryoSerializer()方法向Kryo中添加自定義的序列化器。

image.png

2.3、自定義TypeInformation

除了使用已有的TypeInformation所定義的數據格式類型之外,用戶也可以自定義實現TypeInformation,來滿足的不同的數據類型定義需求。Flink提供了可插拔的Type Information Factory讓用戶將自定義的TypeInformation註冊到Flink類型系統中。如下代碼所示只需要通過實現org.apache.flink.api.common.typeinfo.TypeInfoFactory接口,返回相應的類型信息。

通過@TypeInfo註解創建數據類型,定義CustomTuple數據類型。

image.png

然後定義CustomTypeInfoFactory類繼承於TypeInfoFactory,參數類型指定CustomTuple。最後重寫createTypeInfo方法,創建的CustomTupleTypeInfo就是CustomTuple數據類型TypeInformation。

image.png

三、FAQ

3.1、flink使用map算子返回Tuple3時,如果不指定returns則會報錯

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env);
Properties kafkaProp = new Properties();
FlinkKafkaConsumer010<String> myConsumer = new FlinkKafkaConsumer010<String>("test", new SimpleStringSchema(), kafkaProp);

DataStream<Tuple3<Integer, String, Integer>> dataStream = env
                .addSource(myConsumer)
                .map(record -> {
                    JSONObject jsonObject = JSON.parseObject(record);
                    return new Tuple3<>(jsonObject.getInteger("id"), jsonObject.getString("name"), jsonObject.getInteger("age"));
                });
env.execute();

運行上述代碼,錯誤信息如下:

Exception in thread "main" org.apache.flink.api.common.functions.InvalidTypesException: The return type of function 'main(TestFlinkTable.java:43)' could not be determined automatically, due to type erasure. You can give type information hints by using the returns(...) method on the result of the transformation call, or by letting your function implement the 'ResultTypeQueryable' interface.
	at org.apache.flink.streaming.api.transformations.StreamTransformation.getOutputType(StreamTransformation.java:420)
	at org.apache.flink.streaming.api.datastream.DataStream.getType(DataStream.java:175)
	at org.apache.flink.streaming.api.datastream.DataStream.union(DataStream.java:217)
	at com.miaoke.sync.test.TestFlinkTable.main(TestFlinkTable.java:50)
Caused by: org.apache.flink.api.common.functions.InvalidTypesException: The generic type parameters of 'Tuple3' are missing. In many cases lambda methods don't provide enough information for automatic type extraction when Java generics are involved. An easy workaround is to use an (anonymous) class instead that implements the 'org.apache.flink.api.common.functions.MapFunction' interface. Otherwise the type has to be specified explicitly using type information.
	at org.apache.flink.api.java.typeutils.TypeExtractionUtils.validateLambdaType(TypeExtractionUtils.java:350)
	at org.apache.flink.api.java.typeutils.TypeExtractor.getUnaryOperatorReturnType(TypeExtractor.java:579)
	at org.apache.flink.api.java.typeutils.TypeExtractor.getMapReturnTypes(TypeExtractor.java:175)
	at org.apache.flink.streaming.api.datastream.DataStream.map(DataStream.java:585)
	at com.miaoke.sync.test.TestFlinkTable.main(TestFlinkTable.java:43)

根據錯誤提示,加上returns(),則正常通過

DataStream<Tuple3<Integer, String, Integer>> dataStream = env
                .addSource(myConsumer)
                .map(record -> {
                    JSONObject jsonObject = JSON.parseObject(record);
                    return new Tuple3<>(jsonObject.getInteger("id"), jsonObject.getString("name"), jsonObject.getInteger("age"));
                }).returns(Types.TUPLE(Types.INT, Types.STRING, Types.INT));

常見的 returns 的使用:

.returns(Types.TUPLE(Types.INT, TypeInformation.of(Alarm.class)))
.returns(Types.TUPLE(Types.INT,Types.INT))

.returns(Types.STRING)
.returns(TypeInformation.of(String.class))

.returns(new TypeHint<Tuple2<String, String>>(){})
.returns(TypeInformation.of(new TypeHint<Tuple2<ConsumerRecord, String>>() {}))
.returns(SomeType.class)

推薦文章:https://blog.csdn.net/QcloudCommunity/article/details/82623022

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