概述
Apache Avro是一個獨立於編程語言的數據序列化系統,獨立於語言也就意味着可以被多種語言處理。Avro起源於Hadoop,目的是解決Hadoop Writable類型的不足:缺乏語言的可移植性。
Avro是基於schema(模式),這和protobuf、thrift沒什麼區別,在schema文件中(.avsc文件)中聲明數據類型或者protocol(RPC接口),那麼avro在read、write時將依據schema對數據進行序列化。因爲有了schema,那麼Avro的讀、寫操作將可以使用不同的平臺語言。Avro的schema是JSON格式,所以編寫起來也非常簡單、可讀性很好。
Avro提供:
豐富的數據結構類型
快速可壓縮的二進制數據形式
存儲持久化數據的文件容器
RPC
簡單的和動態語言結合,無論在讀寫數據文件還是實現RPC protocol時,由arvo schema產生代碼的過程都不是必須的,code generation是一個可選的優化,在使用靜態語言時產生代碼是值得的。avro依賴於模式,動態加載相關數據的模式,Avro數據的讀寫操作很頻繁,而這些操作使用的都是模式,這樣就減少寫入每個數據文件的開銷,使得序列化快速而又輕巧。這種數據及其模式的自我描述方便了動態腳本語言的使用。當Avro數據存儲到文件中時,它的模式也隨之存儲,這樣任何程序都可以對文件進行處理。
Avro數據類型和模式
Avro的基本類型
類型名稱 | 描述模式 | 示例 |
---|---|---|
null | 空值 | “null” |
bolean | 二進制值 | “boolean” |
int | 32位帶符號整數 | “int” |
long | 64位帶符號整數 | “long” |
float | 單精度(32位)IEEE754浮點數 | “float” |
double | 雙精度(64位) | IEEE754浮點數 “double” |
bytes | 8位無符號字節序列 | “bytes” |
string | Unicode字符序列 | “string” |
Avro複雜類型
複雜類型註解
array:數組,和java中的數組沒有區別。其item屬性表示數組的item的類型。
map:跟java中的map一樣,只是key必須爲string(無法聲明key 的類型),“values”屬性聲明value的類型。
record:這個和java中的“class”有同等的意義,它支持如下屬性:
1)name:必要屬性,表示record的名稱,在java生成代碼時作爲類的名稱。
2)namespace:限定名,在java生成代碼時作爲package的名字。其中namespace + name最終構成record的全名。
3)doc:可選,文檔信息,備註信息。
4)aliases:別名
5) fields:field列表,嚴格有序。
每個fields包括,“name”、“doc”、“type”、“default”、
“order”、“aliases”幾個屬性。其中在fields列表中每個filed應該擁有不重複的name,“type”表示field的數據類型。 default”很明顯,用來表示field的默認值,當reader讀取時,當沒有此field時將會採用默認值;“order”:可選值,排序(ascending、descending、ignore),在Mapreduce集成時有用。 “aliases”別名,JSON Array,表示此field的別名列表。
enum:枚舉類型。“symbols”屬性即位enum的常量值列表。值不能重複。
fixed:表示field的值的長度爲“固定值”,“size”屬性表示值的字節長度。
union:集合,數學意義上的“集合”,集合中的數據不能重複。如果對“type”使用union類型,那麼其default值必須和union的第一個類型匹配。
Avro 的java實例
首先我們需要在pom.xml文件中增加如下配置:
1. <dependency>
2. <groupId>org.apache.avro</groupId>
3. <artifactId>avro</artifactId>
4. <version>1.7.7</version>
5. </dependency>
6.
7. <dependency>
8. <groupId>org.apache.avro</groupId>
9. <artifactId>avro-tools</artifactId>
10. <version>1.7.7</version>
11. </dependency>
avro和avro-tools兩個依賴包,是avro開發的必備的基礎包。如果需要讓maven來根據.avsc文件生成java代碼的話,還需要增加如下avro-maven-plugin依賴,否則此處是不需要的。
1. <plugin>
2. <groupId>org.apache.avro</groupId>
3. <artifactId>avro-maven-plugin</artifactId>
4. <version>1.7.7</version>
5. <executions>
6. <execution>
7. <phase>generate-sources</phase>
8. <goals>
9. <goal>schema</goal>
10. </goals>
11. <configuration>
12. <sourceDirectory>${project.basedir}/src/main/resources/avro/</sourceDirectory>
13. <outputDirectory>${project.basedir}/src/main/java/</outputDirectory>
14. </configuration>
15. </execution>
16. </executions>
17. </plugin>
18. <plugin>
19. <groupId>org.apache.maven.plugins</groupId>
20. <artifactId>maven-compiler-plugin</artifactId>
21. <configuration>
22. <encoding>utf-8</encoding>
23. </configuration>
24. </plugin>
不基於代碼生成的方式
定義模式Pair.avsc(.avsc是avro Schema文件的常用擴展名)
1. {
2. "namespace": "test.avro",
3. "type": "record",
4. "name": "Pair",
5. "doc":"A Pair of Strings.",
6. "fields": [
7. {"name": "left", "type": "string"},
8. {"name": "right", "type":"string" },
9. ]
10. }
namespace類似於包名,name類似於類名,namespace加上name是這個類的全名,fields內的內容相當於類的變量。
可以通過聲明來加載這個模式:
Schema schema =Schema.parse(file);;
file爲.avsc文件,然後可以使用下述的API來創建Avro記錄的實例:
GenericRecord datum =new GenericData.Record(schema);
datum.put("left",new Utf8("L"));
datum.put("right",new Utf8("R"));
接下來就可以執行序列化過程:
DatumWriter<GenericRecord> userDatumWriter =new SpecificDatumWriter<GenericRecord>(schema);
DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<GenericRecord>(userDatumWriter);
dataFileWriter.create(schema, storageFile);
dataFileWriter.append(datum);
dataFileWriter.close();
下面的代碼反序列化:
DatumReader<GenericRecord> datumReader =new GenericDatumReader<GenericRecord>(schema);
DataFileReader<GenericRecord> dataFileReader = new DataFileReader<GenericRecord>(storageFile,datumReader);
GenericRecord record =null;
while(dataFileReader.hasNext()){
record=dataFileReader.next();
System.out.println(record);
}
這種情況下,沒有生成JAVA API,那麼序列化過程就需要開發者預先熟悉Schema的結構,創建User的過程就像構建JSON字符串一樣,通過put操作來“賦值”。反序列化也是一樣,需要指定schema。
GenericRecord接口提供了根據“field”名稱獲取值的方法:Object get(String fieldName);不過需要聲明,這內部實現並不是基於map,而是一個數組,數組和Schema聲明的Fileds按照index對應。put操作根據field名稱找到對應的index,然後賦值;get反之。那麼在對待Schema兼容性上和“代碼生成”基本一致。
基於代碼生成的方式
定義schema,user.avsc
{"namespace":"test.avro",
"type":"record",
"name":"User",
"fields":[
{"name":"name","type":"string"},
{"name":"favourite_number","type":["int","null"]},
{"name":"favourite_color","type":["string","null"]}
]
}
“根據Avro schema生成JAVA代碼,然後根據JAVA API來進行數據操作。如果你在pom.xml配置了avro-maven-plugin插件,那麼只需要只需要執行:
maven compile
此後插件將會根據user.avsc文件在project指定的package目錄下生成java代碼。此外如果你的項目不是基於maven的,或者你不希望maven來做這件事,也可以使用如下命令生成java代碼,此後只需要將代碼copy到項目中即可:
1. java -jar /path/to/avro-tools-1.7.7.jar compile schema <schema file> <code destination>
2. java -jar avro-tools-1.7.7.jar compile schema user.avsc ./
序列化、發序列化Java代碼樣例:
//Three ways to create user
File file =new File("e:\\user.avro");
User user =new User();
user.setName("zhang");
user.setFavouriteColor("blue");
user.setFavouriteNumber(12);
User user1 =new User("shan",8,"blue");
User user2 = User.newBuilder()
.setName("lucker")
.setFavouriteColor("blcak")
.setFavouriteNumber(38)
.build();
//serialization
try {
DatumWriter<User> userDatumWriter =new SpecificDatumWriter<User>(User.class);
DataFileWriter<User> dataFileWriter = new DataFileWriter<User>(userDatumWriter);
dataFileWriter.create(user.getSchema(),file);
dataFileWriter.append(user);
dataFileWriter.append(user);
dataFileWriter.append(user1);
dataFileWriter.append(user2);
dataFileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
//deserialization
try {
DatumReader<User> userDatumReader =new SpecificDatumReader<User>(User.class);
DataFileReader<User> dataFileReader =new DataFileReader<User>(file,userDatumReader);
User u =null;
while(dataFileReader.hasNext()){
u=dataFileReader.next();
System.out.println(u);
}
} catch (IOException e) {
e.printStackTrace();
}
代碼很簡單,描述了將User通過avro schema寫入文件,我們可以通過二進制文本編輯器查看這個結果文件,會發現文件的開頭部分是schema,後續是逐個user序列化的二進制結果。代碼生成時,User的Schema信息已經作爲一個靜態常量寫入了User.java中,同時根據schema中fields的列表嚴格順序,顯式的生成Fields數組,數組的index爲schema中filed的聲明的位置。
Schema解析
Schema通過JSON對象表示。Schema定義了簡單數據類型和複雜數據類型,其中複雜數據類型包含不同屬性。通過各種數據類型用戶可以自定義豐富的數據結構。
Schema有下列JSON對象之一定義:
1. JSON字符串:命名
2. JSON對象:{“type”: “typeName” …attributes…}
3. JSON數組:Avro中Union的定義
可以選擇一個與寫入模式不同的讀取模式來讀取數據,這種方式成爲模式演化。
考慮模式Pair,如果對其添加description使其成爲一個新模式newSchema:
1. {
2. "namespace": "test.avro",
3. "type": "record",
4. "name": "Pair",
5. "doc":"A Pair of Strings.",
6. "fields": [
7. {"name": "left", "type": "string"},
8. {"name": "right", "type":"string" },
9. {"name":"description","type":"string","defualt":""}
10. ]
11. }
使用這個新的模式讀取前面序列化的數據,以爲description指定了默認值,所以Avro在讀取沒有定義該字段的記錄時就會使用這個空值。注意:如果希望將默認值設置爲null,則需要使用avro的並集指定description的type:
1. {"name":"description","type":"string","type":["null","string"],"defualt":"null"}
讀取的代碼如下,讀取兩個模式對象,讀取對象和寫入對象:
1. DatumReader<GenericRecord> reader =new GenericDatumReader<GenericRecord>(schema,newSchema);
2. Decoder decoder=DecoderFactory.defaultFactory().createBinaryDecoder(out.toByteArray(),null);
3. GenericRecord result =reader.read(null,decoder);
4. assertThat(result.get("left").toString(),is("L"));
5. assertThat(result.get("right").toString(),is("R"));
6. assertThat(result.get("description").toString(),is(""));
不同讀取模式的另一個應用是去掉記錄中的某些字段,該操作可以稱爲“投影”;
1. {
2. "namespace": "test.avro",
3. "type": "record",
4. "name": "Pair",
5. "doc":"A Pair of Strings.",
6. "fields": [
7. {"name": "left", "type": "string"},
8. ]
9. }