编码数据的格式

程序通常(至少)使用两种形式的数据:

  1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
  2. 如果要将数据写入文件,或通过网络发送,则必须将其 编码(encode) 为某种自包含的字节序列(例如,JSON文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同^i

所以,需要在两种表示之间进行某种类型的翻译。 从内存中表示到字节序列的转换称为 编码(Encoding) (也称为序列化(serialization)编组(marshalling)),反过来称为解码(Decoding)[^ii](解析(Parsing)反序列化(deserialization)反编组( unmarshalling)^译i

[^ii]: 请注意,编码(encode)加密(encryption) 无关。 本书不讨论加密。

术语冲突

不幸的是,在第七章事务(Transaction) 的上下文里,序列化(Serialization) 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 编码(Encoding) 表达此含义。

这是一个常见的问题,因而有许多库和编码格式可供选择。 首先让我们概览一下。

语言特定的格式

许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java有java.io.Serializable 【1】,Ruby有Marshal【2】,Python有pickle【3】等等。许多第三方库也存在,例如Kryo for Java 【4】。

这些编码库非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有一些深层次的问题:

  • 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据。如果以这类编码存储或传输数据,那你就和这门语言绑死在一起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
  • 为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这通常是安全问题的一个来源【5】:如果攻击者可以让应用程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执行任意代码【6,7】。
  • 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
  • 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的。 例如,Java的内置序列化由于其糟糕的性能和臃肿的编码而臭名昭着【8】。

因此,除非临时使用,采用语言内置编码通常是一个坏主意。

JSON,XML和二进制变体

谈到可以被许多编程语言编写和读取的标准化编码,JSON和XML是显眼的竞争者。它们广为人知,广受支持,也“广受憎恶”。 XML经常被批评为过于冗长和不必要的复杂【9】。 JSON倍受欢迎,主要由于它在Web浏览器中的内置支持(通过成为JavaScript的一个子集)以及相对于XML的简单性。 CSV是另一种流行的与语言无关的格式,尽管功能较弱。

JSON,XML和CSV是文本格式,因此具有人类可读性(尽管语法是一个热门辩题)。除了表面的语法问题之外,它们也有一些微妙的问题:

  • 数字的编码多有歧义之处。XML和CSV不能区分数字和字符串(除非引用外部模式)。 JSON虽然区分字符串和数字,但不区分整数和浮点数,而且不能指定精度。
  • 当处理大量数据时,这个问题更严重了。例如,大于$2^{53}$的整数不能在IEEE 754双精度浮点数中精确表示,因此在使用浮点数(例如JavaScript)的语言进行分析时,这些数字会变得不准确。 Twitter上有一个大于$2^{53}$的数字的例子,它使用一个64位的数字来标识每条推文。 Twitter API返回的JSON包含了两种推特ID,一个JSON数字,另一个是十进制字符串,以此避免JavaScript程序无法正确解析数字的问题【10】。
  • JSON和XML对Unicode字符串(即人类可读的文本)有很好的支持,但是它们不支持二进制数据(不带字符编码(character encoding)的字节序列)。二进制串是很实用的功能,所以人们通过使用Base64将二进制数据编码为文本来绕开这个限制。模式然后用于表示该值应该被解释为Base64编码。这个工作,但它有点hacky,并增加了33%的数据大小。 XML 【11】和JSON 【12】都有可选的模式支持。这些模式语言相当强大,所以学习和实现起来相当复杂。 XML模式的使用相当普遍,但许多基于JSON的工具嫌麻烦才不会使用模式。由于数据的正确解释(例如数字和二进制字符串)取决于模式中的信息,因此不使用XML/JSON模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。
  • CSV没有任何模式,因此应用程序需要定义每行和每列的含义。如果应用程序更改添加新的行或列,则必须手动处理该变更。 CSV也是一个相当模糊的格式(如果一个值包含逗号或换行符,会发生什么?)。尽管其转义规则已经被正式指定【13】,但并不是所有的解析器都正确的实现了标准。

尽管存在这些缺陷,但JSON,XML和CSV已经足够用于很多目的。特别是作为数据交换格式(即将数据从一个组织发送到另一个组织),它们很可能仍然很受欢迎。这种情况下,只要人们对格式是什么意见一致,格式多么美观或者高效就没有关系。让不同的组织达成一致的难度超过了其他大多数问题。

二进制编码

对于仅在组织内部使用的数据,使用最小公分母编码格式的压力较小。例如,可以选择更紧凑或更快的解析格式。虽然对小数据集来说,收益可以忽略不计,但一旦达到TB级别,数据格式的选择就会产生巨大的影响。

JSON比XML简洁,但与二进制格式一比,还是太占地方。这一事实导致大量二进制编码版本JSON & XML的出现,JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)(例如WBXML和Fast Infoset)。这些格式已经被各种各样的领域所采用,但是没有一个像JSON和XML的文本版本那样被广泛采用。

这些格式中的一些扩展了一组数据类型(例如,区分整数和浮点数,或者增加对二进制字符串的支持),另一方面,它们没有盖面JSON / XML的数据模型。特别是由于它们没有规定模式,所以它们需要在编码数据中包含所有的对象字段名称。也就是说,在例4-1中的JSON文档的二进制编码中,需要在某处包含字符串userNamefavoriteNumberinterest

例4-1 本章中用于展示二进制编码的示例记录

  1. {
  2. "userName": "Martin",
  3. "favoriteNumber": 1337,
  4. "interests": ["daydreaming", "hacking"]
  5. }

我们来看一个MessagePack的例子,它是一个JSON的二进制编码。图4-1显示了如果使用MessagePack 【14】对例4-1中的JSON文档进行编码,则得到的字节序列。前几个字节如下:

  1. 第一个字节0x83表示接下来是3个字段(低四位= 0x03)的对象 object(高四位= 0x80)。 (如果想知道如果一个对象有15个以上的字段会发生什么情况,字段的数量塞不进4个bit里,那么它会用另一个不同的类型标识符,字段的数量被编码两个或四个字节)。
  2. 第二个字节0xa8表示接下来是8字节长的字符串(最高四位= 0x08)。
  3. 接下来八个字节是ASCII字符串形式的字段名称userName。由于之前已经指明长度,不需要任何标记来标识字符串的结束位置(或者任何转义)。
  4. 接下来的七个字节对前缀为0xa6的六个字母的字符串值Martin进行编码,依此类推。

二进制编码长度为66个字节,仅略小于文本JSON编码所取的81个字节(删除了空白)。所有的JSON的二进制编码在这方面是相似的。空间节省了一丁点(以及解析加速)是否能弥补可读性的损失,谁也说不准。

在下面的章节中,能达到比这好得多的结果,只用32个字节对相同的记录进行编码。

编码数据的格式 - 图1

图4-1 使用MessagePack编码的记录(例4-1)

Thrift与Protocol Buffers

Apache Thrift 【15】和Protocol Buffers(protobuf)【16】是基于相同原理的二进制编码库。 Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007~2008年都是开源的【17】。Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift的例4-1中对数据进行编码,可以使用Thrift 接口定义语言(IDL) 来描述模式,如下所示:

  1. struct Person {
  2. 1: required string userName,
  3. 2: optional i64 favoriteNumber,
  4. 3: optional list<string> interests
  5. }

Protocol Buffers的等效模式定义看起来非常相似:

  1. message Person {
  2. required string user_name = 1;
  3. optional int64 favorite_number = 2;
  4. repeated string interests = 3;
  5. }

Thrift和Protocol Buffers每一个都带有一个代码生成工具,它采用了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类【18】。您的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。用这个模式编码的数据是什么样的?令人困惑的是,Thrift有两种不同的二进制编码格式[^iii],分别称为BinaryProtocol和CompactProtocol。先来看看BinaryProtocol。使用这种格式的编码来编码例4-1中的消息只需要59个字节,如图4-2所示【19】。

编码数据的格式 - 图2

图4-2 使用Thrift二进制协议编码的记录

[^iii]: 实际上,Thrift有三种二进制协议:CompactProtocol和DenseProtocol,尽管DenseProtocol只支持C ++实现,所以不算作跨语言[18]。 除此之外,它还有两种不同的基于JSON的编码格式【19】。 真逗!

图4-1类似,每个字段都有一个类型注释(用于指示它是一个字符串,整数,列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串(“Martin”, “daydreaming”, “hacking”)也被编码为ASCII(或者说,UTF-8),与之前类似。

图4-1相比,最大的区别是没有字段名(userName, favoriteNumber, interest)。相反,编码数据包含字段标签,它们是数字(1, 2和3)。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。

Thrift CompactProtocol编码在语义上等同于BinaryProtocol,但是如图4-3所示,它只将相同的信息打包成只有34个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字1337不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节来。这意味着-64到63之间的数字被编码为一个字节,-8192和8191之间的数字以两个字节编码,等等。较大的数字使用更多的字节。

编码数据的格式 - 图3

图4-3 使用Thrift压缩协议编码的记录

最后,Protocol Buffers(只有一种二进制编码格式)对相同的数据进行编码,如图4-4所示。 它的打包方式稍有不同,但与Thrift的CompactProtocol非常相似。 Protobuf将同样的记录塞进了33个字节中。

编码数据的格式 - 图4

图4-4 使用Protobuf编码的记录

需要注意的一个细节:在前面所示的模式中,每个字段被标记为必需或可选,但是这对字段如何编码没有任何影响(二进制数据中没有任何字段指示是否需要字段)。所不同的是,如果未设置该字段,则所需的运行时检查将失败,这对于捕获错误非常有用。

字段标签和模式演变

我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift和Protocol Buffers如何处理模式更改,同时保持向后兼容性?

从示例中可以看出,编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字1,2,3)标识,并用数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。您可以更改架构中字段的名称,因为编码的数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。

您可以添加新的字段到架构,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。

向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的领域,你不能要求。如果您要添加一个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后 添加的每个字段必须是可选的或具有默认值

删除一个字段就像添加一个字段,倒退和向前兼容性问题相反。这意味着您只能删除一个可选的字段(必填字段永远不能删除),而且您不能再次使用相同的标签号码(因为您可能仍然有数据写在包含旧标签号码的地方,而该字段必须被新代码忽略)。

数据类型和模式演变

如何改变字段的数据类型?这可能是可能的——检查文件的细节——但是有一个风险,值将失去精度或被扼杀。例如,假设你将一个32位的整数变成一个64位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。

Protobuf的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(这是第三个选项旁边必要和可选)。如图4-4所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。

Thrift有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许Protocol Buffers所做的从单值到多值的相同演变,但是它具有支持嵌套列表的优点。

Avro

Apache Avro 【20】是另一种二进制编码格式,与Protocol Buffers和Thrift有趣的不同。 它是作为Hadoop的一个子项目在2009年开始的,因为Thrift不适合Hadoop的用例【21】。

Avro也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON),更易于机器读取。

我们用Avro IDL编写的示例模式可能如下所示:

  1. record Person {
  2. string userName;
  3. union { null, long } favoriteNumber = null;
  4. array<string> interests;
  5. }

等价的JSON表示:

  1. {
  2. "type": "record",
  3. "name": "Person",
  4. "fields": [
  5. {"name": "userName", "type": "string"},
  6. {"name": "favoriteNumber", "type": ["null", "long"], "default": null},
  7. {"name": "interests", "type": {"type": "array", "items": "string"}
  8. ]
  9. }

首先,请注意架构中没有标签号码。 如果我们使用这个模式编码我们的例子记录(例4-1),Avro二进制编码只有32个字节长,这是我们所见过的所有编码中最紧凑的。 编码字节序列的分解如图4-5所示。

如果您检查字节序列,您可以看到没有什么可以识别字段或其数据类型。 编码只是由连在一起的值组成。 一个字符串只是一个长度前缀,后跟UTF-8字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。 它可以是一个整数,也可以是其他的整数。 整数使用可变长度编码(与Thrift的CompactProtocol相同)进行编码。

编码数据的格式 - 图5

图4-5 使用Avro编码的记录

为了解析二进制数据,您按照它们出现在架构中的顺序遍历这些字段,并使用架构来告诉您每个字段的数据类型。这意味着如果读取数据的代码使用与写入数据的代码完全相同的模式,则只能正确解码二进制数据。阅读器和作者之间的模式不匹配意味着错误地解码数据。

那么,Avro如何支持模式演变呢?

作者模式与读者模式

有了Avro,当应用程序想要编码一些数据(将其写入文件或数据库,通过网络发送等)时,它使用它知道的任何版本的模式编码数据,例如,架构可能被编译到应用程序中。这被称为作者的模式。

当一个应用程序想要解码一些数据(从一个文件或数据库读取数据,从网络接收数据等)时,它希望数据在某个模式中,这就是读者的模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能是从该模式生成的。

Avro的关键思想是作者的模式和读者的模式不必是相同的 - 他们只需要兼容。当数据解码(读取)时,Avro库通过并排查看作者的模式和读者的模式并将数据从作者的模式转换到读者的模式来解决差异。 Avro规范【20】确切地定义了这种解析的工作原理,如图4-6所示。

例如,如果作者的模式和读者的模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在作者模式中但不在读者模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是作者的模式不包含该名称的字段,则使用在读者模式中声明的默认值填充。

编码数据的格式 - 图6

图4-6 一个Avro Reader解决读写模式的差异

模式演变规则

使用Avro,向前兼容性意味着您可以将新版本的架构作为编写器,并将旧版本的架构作为读者。相反,向后兼容意味着你可以有一个作为读者的新版本的模式和作为作者的旧版本。

为了保持兼容性,您只能添加或删除具有默认值的字段。 (我们的Avro模式中的字段favourNumber的默认值为null)。例如,假设您添加一个默认值的字段,所以这个新的字段存在于新的模式中,而不是旧的。当使用新模式的阅读器读取使用旧模式写入的记录时,将为缺少的字段填充默认值。

如果你要添加一个没有默认值的字段,新的阅读器将无法读取旧作者写的数据,所以你会破坏向后兼容性。如果您要删除没有默认值的字段,旧的阅读器将无法读取新作者写入的数据,因此您会打破兼容性。在一些编程语言中,null是任何变量可以接受的默认值,但在Avro中并不是这样:如果要允许一个字段为null,则必须使用联合类型。例如,union {null,long,string}字段;表示该字段可以是数字或字符串,也可以是null。如果它是union的分支之一,那么只能使用null作为默认值^iv。这比默认情况下可以为null是更加冗长的,但是通过明确什么可以和不可以是什么,有助于防止错误的null 【22】。

因此,Avro没有像Protocol Buffers和Thrift那样的optionalrequired标记(它有联合类型和默认值)。

只要Avro可以转换类型,就可以改变字段的数据类型。更改字段的名称是可能的,但有点棘手:读者的模式可以包含字段名称的别名,所以它可以匹配旧作家的模式字段名称与别名。这意味着更改字段名称是向后兼容的,但不能向前兼容。同样,向联合类型添加分支也是向后兼容的,但不能向前兼容。

但作者模式到底是什么?

到目前为止,我们已经讨论了一个重要的问题:读者如何知道作者的模式是哪一部分数据被编码的?我们不能只将整个模式包括在每个记录中,因为模式可能比编码的数据大得多,从而使二进制编码节省的所有空间都是徒劳的。答案取决于Avro使用的上下文。举几个例子:

  • 有很多记录的大文件

    Avro的一个常见用途 - 尤其是在Hadoop环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。 (我们将在第10章讨论这种情况。)在这种情况下,该文件的作者可以在文件的开头只包含一次作者的模式。 Avro指定一个文件格式(对象容器文件)来做到这一点。

  • 支持独立写入的记录的数据库

    在一个数据库中,不同的记录可能会在不同的时间点使用不同的作者的模式编写 - 你不能假定所有的记录都有相同的模式。最简单的解决方案是在每个编码记录的开始处包含一个版本号,并在数据库中保留一个模式版本列表。读者可以获取记录,提取版本号,然后从数据库中获取该版本号的作者模式。使用该作者的模式,它可以解码记录的其余部分。 (例如Espresso 【23】就是这样工作的。)

  • 通过网络连接发送记录

    当两个进程通过双向网络连接进行通信时,他们可以在连接设置上协商模式版本,然后在连接的生命周期中使用该模式。 Avro RPC协议(参阅“通过服务的数据流:REST和RPC”)如此工作。

具有模式版本的数据库在任何情况下都是非常有用的,因为它充当文档并为您提供了检查模式兼容性的机会【24】。作为版本号,你可以使用一个简单的递增整数,或者你可以使用模式的散列。

动态生成的模式

与Protocol Buffers和Thrift相比,Avro方法的一个优点是架构不包含任何标签号码。但为什么这很重要?在模式中保留一些数字有什么问题?

不同之处在于Avro对动态生成的模式更友善。例如,假如你有一个关系数据库,你想要把它的内容转储到一个文件中,并且你想使用二进制格式来避免前面提到的文本格式(JSON,CSV,SQL)的问题。如果你使用Avro,你可以很容易地从关系模式生成一个Avro模式(在我们之前看到的JSON表示中),并使用该模式对数据库内容进行编码,并将其全部转储到Avro对象容器文件【25】中。您为每个数据库表生成一个记录模式,每个列成为该记录中的一个字段。数据库中的列名称映射到Avro中的字段名称。

现在,如果数据库模式发生变化(例如,一个表中添加了一列,删除了一列),则可以从更新的数据库模式生成新的Avro模式,并在新的Avro模式中导出数据。数据导出过程不需要注意模式的改变 - 每次运行时都可以简单地进行模式转换。任何读取新数据文件的人都会看到记录的字段已经改变,但是由于字段是通过名字来标识的,所以更新的作者的模式仍然可以与旧的读者模式匹配。

相比之下,如果您为此使用Thrift或Protocol Buffers,则字段标记可能必须手动分配:每次数据库模式更改时,管理员都必须手动更新从数据库列名到字段标签。 (这可能会自动化,但模式生成器必须非常小心,不要分配以前使用的字段标记。)这种动态生成的模式根本不是Thrift或Protocol Buffers的设计目标,而是为Avro。

代码生成和动态类型的语言

Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用您选择的编程语言生成实现此模式的代码。这在Java,C ++或C#等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。

在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了明确的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。

Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。如果你有一个对象容器文件(它嵌入了作者的模式),你可以简单地使用Avro库打开它,并以与查看JSON文件相同的方式查看数据。该文件是自描述的,因为它包含所有必要的元数据。

这个属性特别适用于动态类型的数据处理语言如Apache Pig 【26】。在Pig中,您可以打开一些Avro文件,开始分析它们,并编写派生数据集以Avro格式输出文件,而无需考虑模式。

模式的优点

正如我们所看到的,Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式。他们的模式语言比XML模式或者JSON模式简单得多,它支持更详细的验证规则(例如,“该字段的字符串值必须与该正则表达式匹配”或“该字段的整数值必须在0和100之间“)。由于Protocol Buffers,Thrift和Avro实现起来更简单,使用起来也更简单,所以它们已经发展到支持相当广泛的编程语言。

这些编码所基于的想法绝不是新的。例如,它们与ASN.1有很多相似之处,它是1984年首次被标准化的模式定义语言【27】。它被用来定义各种网络协议,其二进制编码(DER)仍然被用于编码SSL证书(X.509),例如【28】。 ASN.1支持使用标签号码的模式演进,类似于Protocol Buf-fers和Thrift 【29】。然而,这也是非常复杂和严重的文件记录,所以ASN.1可能不是新应用程序的好选择。

许多数据系统也为其数据实现某种专有的二进制编码。例如,大多数关系数据库都有一个网络协议,您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定的数据库,并且数据库供应商提供将来自数据库的网络协议的响应解码为内存数据结构的驱动程序(例如使用ODBC或JDBC API)。

所以,我们可以看到,尽管JSON,XML和CSV等文本数据格式非常普遍,但基于模式的二进制编码也是一个可行的选择。他们有一些很好的属性:

  • 它们可以比各种“二进制JSON”变体更紧凑,因为它们可以省略编码数据中的字段名称。
  • 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)。
  • 保留模式数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性。
  • 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。

总而言之,模式进化允许与JSON数据库提供的无模式/模式读取相同的灵活性(请参阅第39页的“文档模型中的模式灵活性”),同时还可以更好地保证数据和更好的工具。