16.1.1 模式声明
模式声明主要是定义数据的类型,Avro中的模式可以使用JSON通过以下方式表示。
1)JSON字符串,指定已定义的类型。
2)JSON对象,其形式为:
{"type":"typeName"……attributes……}
其中,typeName可以是原生的或衍生的类型名称,本章没有定义的属性可以视为元数据,但是其不能影响序列化数据的格式。
3)JSON数组,表示嵌入类型的联合。
声明的类型必须是Avro所支持的数据类型,其中包括原始类型(Primitive Types)和复杂类型(Complex Types),下面分别介绍它们。
原始类型名称包括以下几部分。
null:没有值;
boolean:二进制值;
int:32位有符号整数;
long:64位有符号整数;
float:单精度(32位)IEEE 754浮点数;
double:双精度(64位)IEEE 754浮点数;
bytes:8位无符号字节序列;
string:unicode字符序列。
原始类型没有特定的属性,其名称可以通过类型来定义,如模式"string"相当于:
{"type":"string"}
Avro支持六种复杂类型:记录(records)、枚举(enums)、数组(arrays)、映射(maps)、联合(unions)和固定型(fixed),下面一一介绍。
(1)记录(records)
记录使用类型名称“record”并且支持以下属性:
name:提供记录名称的JSON字符串(必须)。
namespace:限定名称的JSON字符串。
doc:向模式使用者提供说明的JSON字符串(可选)。
aliases:字符串的JSON数组,为记录提供代替名称(可选)。
field:一个JSON数组,用来列出字段(必须)。每个字段就是一个JSON对象且拥有以下属性。
·name:提供记录名称的JSON字符串(必须)。
·doc:为使用者提供字段说明的JSON字符串(可选)。
·type:定义模式的JSON对象,或者记录定义的JSON字符串(必须)。
·default:该字段的默认值用于读取缺少该字段的实例(可选)。如表16-1所示,允许的值依赖于字段的模式类型。联合字段的默认值对应于联合中的第一个模式。字节和固定字段的默认值是JSON字符,这里0~255的Unicode映射到0~255的8位无符号字节。
·order:指定该字段如何影响记录的排序(可选)。有效的值有“ascending”(默认)、“descending”或“ignore”。
·aliases:字符串的JSON数组,为该字段提供可选的名称(可选)。
例如,一个64位的链表可以定义为:
{
"type":"record",
"name":"LongList",
"aliases":["LinkedLongs"],//别名
"fields":[
{"name":"value","type":"long"},//每个元素都含有长整型
{"name":"next","type":["LongList","null"]}
//下一元素
]
}
(2)枚举(enums)
枚举使用类型名称“enum”并且支持以下几种类型。
name:提供实例名称的JSON字符串(必须)。
namespace:限定名称的JSON字符串。
aliases:字符串的JSON数组,为枚举提供替代名称(可选)。
doc:对模式使用者提供说明的JSON字符串(可选)。
symbols:列出标记的JSON数组(必须)。枚举中的所有标记必须是唯一的,不允许有重复的标记。
例如,纸牌游戏可以定义为:
{"type":"enum",
"name":"Suit",
"symbols":["SPADES","HEARTS","DIAMONDS","CLUBS"]
}
(3)数组(arrays)
数组使用类型名称“array”并且支持一个属性。
items:数组项目的模式。
例如,字符串数组可以定义为:
{"type":"array","items":"string"}
(4)映射(maps)
映射使用类型名称“map”并且支持一个属性。
values:映射值的模式。
映射值默认为字符串,例如,从字符串到长整型的映射可以声明为:
{"type":"map","values":"long"}
(5)联合(unions)
联合主要使用JSON数组表示,例如可以用["string","null"]声明一个是字符串或者null的模式。除了指定的记录、固定型(fixed)和枚举外,对于相同的类型,联合只能包含一个模式。例如,联合中不允许包含两个数组类型或两个映射类型,但是允许包含不同名称的两种类型。联合中不能直接包含其他的联合。
(6)固定型(fixed)
固定型使用类型名称“fixed”并且支持以下属性。
name:固定型名称的字符串(必须)。
namespace:限定名称的字符串。
aliases:提供替代名称的字符串的JSON数组(可选)。
size:说明每个值的字节数的整型(可选)。
例如,16字节大小的固定型可以声明为:
{"type":"fixed","size":16,"name":"md5"}
记录、枚举和固定型都是指定的类型,其全名由两部分组成:名称和命名空间。全名为由点分开的名字序列,其名称部分和记录字段名字必须:
以字母A~Z或a~z或_开头;
后面应只含有A~Z、a~z、0~9或_。
在记录、枚举和固定型的定义中,全名可以通过以下几种方式定义。
指定名称和命名空间。如使用名称"name":"X"和命名空间"namespace":"org.foo"来表示全名org.foo.X。
指定全名。如果指定的名称中包含点,则可以使用名称作为全名,并且任何指定的命名空间都将被忽略。如使用名称"name":"org.foo.X"来表示全名org.foo.X。
只指定名称,且名称中不包含点。这种情况下命名空间取自外层的模式或协议,比如指定名称"name":"X",其所在记录定义的字段则为org.foo.Y,即全名为org.foo.X。
总结以上后两种情况可得:如果名称包含点,则是全名;如果不包含点,则命名空间默认为外层定义的命名空间。原始类型没有命名空间并且它们的名称也不能定义为任何命名空间。
命名的类型和字段可以拥有别名。为方便模式的发展和处理不同的数据集,在实现中可以选择使用别名将作者的模式(writer’s schema)映射成读者的模式(reader’s schema)。使用别名可以改变作者的模式,例如,如果作者的模式命名为"Foo",而读者的模式命名为"bar"且别名为"Foo",那么在读取时即使"Foo"称作"Bar"也能实现。同理,如果数据曾经写成字段名为"x"的记录,那么即使是字段名为'"y"别名为"x"的记录也能读取,尽管"x"写成了"y"。
16.1.2 数据序列化
模式声明后就可以根据模式写入数据了。当数据存储或传输时需要对其序列化,需要注意的是,Avro数据和其模式会一起被序列化。基于Avro的RPC系统必须保证远程的数据接收器拥有写入数据的模式副本,因为读取数据时写入数据的模式是知道的,所以Avro数据本身不需要标记类型信息。
通常,序列化和还原序列化过程(见图16-1)可以看成是对模式深度优先、从左到右的遍历过程,并在遍历过程中序列化或还原序列化遇到的原始类型。
Avro指定两种序列化编码:二进制和JSON。在这两种序列化编码中,因为二进制编码速度快且生成的数据量小,所以大多数的应用程序使用二进制编码。但是基于调试和网络的应用程序有时使用JSON编码比较合适。下面先介绍各种类型的二进制编码。
原始类型的二进制编码有如下几种。
null编码成零字节。
boolean编码成单字节,值为0(false)或1(true)。
int和long使用可变长度的ZigZag编码[1],如表16-2所示。
float占4字节,使用类似于Java中floatToIntBits的方法可以将浮点数转化成32位的整型,然后编码成低字节序的格式。
double占8字节,使用类似于Java中doubleToLongBits的方法可以将双精度数转化
为64位的整型,然后编码成低字节序的格式。
bytes根据数据的字节编码成长整型。
string根据UTF-8字符集编码成长整型。
如果UTF-8字符集中'f'、'o'、'o'的十六进制分别为66 6f 6f,并且字符串"foo"含有三(编码成十六进制06)个字符,那么"foo"编码为06 66 6f 6f。
复杂类型的二进制编码方法有如下几种。
(1)记录(records)
通过对声明的每个字段值按顺序编码来对记录进行编码。换句话说,记录的编码由每个字段的编码串联而成。例如,记录模式的代码如下:
{
"type":"record",
"name":"test",
"fields":[
{"name":"a","type":"long"},
{"name":"b","type":"string"}
]
}
以上代码中,假设a字段的值为27(十六进制为36),b字段的值为"foo"(十六进制为06 66 6f 6f),那么记录的编码仅仅是这些编码的串联,即十六进制序列36 06 66 6f 6f。
(2)枚举(enums)
枚举按整型编码,其中整型代表每个标志在模式中的位置(从0开始)。例如,枚举模式的代码如下:
{"type":"enum","name":"Foo","symbols":["A","B","C","D"]}
上面的例子序列化时将被编码成整型0~3,其中0代表"A",3代表"D"。
(3)数组(arrays)
数组编码成一系列块,每个块包含一个长整型的数值,长整形的数值组成为数组项,其中最后一块为0,表示是数组的结尾,每个数组项的模式都会编码。如果块中的值为负数,则取绝对值,紧跟数值后面的块的大小为长整型,表示块中的字节数。如果只映射记录部分字段,则利用块大小可以跳过部分数据。例如,数组模式为:
{"type":"array","items":"long"}
对于包含项3和27的数组,其数组包含两个长整型值,其中数组个数“2”使用ZigZag编码成十六进制为04,而3和27编码成十六进制分别为06和36,最后以0结尾,其数组编码成:04 06 36 00。
这种块表示方法允许读/写大小超过内存缓冲的数组,因为在不知道数组长度的情况下就可以开始写入。
(4)映射(maps)
映射的编码和数组相似,也是编码成一系列块,每个块包含一个长整型值,然后是键值对,值为0的块表示映射的结尾。如果块的值为负数,则取其绝对值。紧跟数值后面的块的大小为长整型,表示块中的字节数。如果只映射记录的部分字段,则利用块大小可以跳过部分数据。
同数组一样,在不知道映射长度的情况下就可以写入,因此这种块的表示方法也允许读/写大小超过内存缓冲的映射。
(5)联合(unions)
联合编码时先写入一个长整型值表示联合中每个模式值的位置(从0开始),再对联合中的值编码。例如,联合模式["string","null"]应如此编码:null为整数1(联合中null的索引,使用ZigZag编码成十六进制02);字符串"a"为整数0(联合中"string"的索引);随后为序列化的字符串61,所以最后这个联合编码应为00 02 61。
(6)固定型(fixed)
固定型实例编码可使用模式中声明的字节数。对于编码成JSON类型,除了联合之外,还可以参照表16-1中JSON类型与Avro类型的对应关系进行编码。联合的JSON编码如下所示:
如果值为null,那么按照JSON null来编码;
否则,按照带有名称/值的JSON对象进行编码,其中名称为类型名称,值为递归编码值。对于Avro的命名类型(记录、固定性和枚举)将使用用户指定的名称,对于其他类型将使用类型名。
例如,对于联合模式["null","string","Foo"],其中Foo是记录名称,应如此编码:null作为null编码;
字符串"a"按照{"string":"a"}编码;
Foo实例按照{"Foo":{……}}编码,这里{……}表示一个Foo实例的JSON编码。
需要注意的是,正确处理JSON编码数据仍需要模式,因为JSON编码并不区分int和long、float和double、记录(records)和映射(maps)、枚举(enums)和字符串(strings)等。
[1]ZigZag编码原使用于Protocol Buffers,是一种将有符号数映射成无符号数的一种编码方式。