概述
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. }