数据在内存中,一般是用对象、结构体、列表、Hash表、树等处理的,但在数据存储和传输时,需要对数据进行编码和解码才能实现。互联网应用是持续在更新的,应用的更新通常会伴随着数据结构的变化,一般情况下的升级模式可能会有出现以下的问题:
- 服务端:采用滚动升级的方式,新旧代码读写数据库中的同一份表结构
- 客户端:可能并不会立即跟进升级
因此,这就要求数据格式支持以下的两个特性:
- 后向兼容:新版本代码可以读取旧版本代码写入的数据
- 前向兼容:旧版本代码可以读取新版本代码写入的数据
一般后向兼容不难实现,因为新代码是已知旧代码写入的数据格式的,可以进行显式处理;前向兼容更困难一些,它需要旧代码忽略新代码写入的部分数据。
本章我们会介绍一些常见的数据编码格式,比如JSON、XML、Protocol Buffer、Thrift和Avro等,是如何处理schema变化的。然后介绍数据存储和通信的方式,比如REST、RPC和消息队列等。
JSON、XML和二进制变体
JSON和XML是被广泛使用的通用编码方式。XML具有良好的可读性,并且并不复杂;JSON被web浏览器内置支持,并且更加简单。但它们有一些其他问题:
- 数字处理的问题。XML无法确定是数字还是字符串;JSON虽然可以区分数字还是字符串,但不能区分整型和浮点型,并且不能指定浮点型的精度;
- 不支持二进制字符串。JSON和XML支持Unicode编解码,不支持用Base64等方式编码得到的二进制字符串;
- 无schema定义或者schema定义语言复杂,有些JSON/XML的schema定义方式需要在代码中进行硬编码;
- CSV由于不存在schema,每列的含义是由应用定义的,很难进行行列的变化。
尽管存在上述的问题,由于JSON、XML和CSV在一些场景,比如数据传输等,已经可以满足需求了,因此仍将是广泛流行的编码方式。
二进制编码
JSON有很多支持二进制编码的衍生编码格式,比如MessagePack、BSON、BJSON、UBSJON、BISON等,编码的原理并不复杂,这里不做详细介绍了,下面贴出一个MessagePack的编码实例图:
Thrift和Protocol Buffers
Thrift和Protobuf是基于相同的原理的二进制编码方式,Thrift是Facebook提出的,Protobuf是Google提出的,它们都是由一种叫接口定义语言(IDL,interface define language)确定的数据结构:
Thrift和Protobuf都有对应的代码生成工具,可以生成多种语言支持的类代码。
编码方式
Thrift有两种编码方式,分别为BinaryProtocol和CompactProtocol,先来看下BinaryProtocol的编码结果:
BinaryProtocol的编码特点:
- 每个字段都有类型和长度标识,数据中的字符串使用ASCII或UTF-8编码;
- 用字段编号替换了字段名称,压缩了名称需要的空间。
再来看下CompactProtocol的编码结果:
CompactProtocol的编码特点:
- 使用变长字节进行数字编码,用sign标识编码是否结束,减少数字的编码结果使用的空间。
最后看下Protobuf的编码结果:
Protobuf支持某字段可选的语法optional,但不影响编码的过程,区别在于required的字段在运行时检查是否存在。
schema变化
对于Thrift格式,每个字段都有相应的数字tag,因此当需要添加新的字段时,只需要按照顺序,为新的字段添加新的tag即可,这里需要注意的点是:
- 不能修改已有tag的字段,这样会造成格式混乱;
- 新的字段要声明成为optional的,保证旧代码写入不会报错;
- 删除已有字段,也只能删除声明为optional的,并且字段的tag不能被再次使用。
如果要进行数据类型的变化,可能会导致精度的丢失或者超精度结果被丢弃,比如32位整形和64位整形的转换。Protobuf和Thrift在处理列表类型时有一些区别:
- Protobuf支持列表和非列表类型的转换。在Protobuf中,列表是通过关键字repeated定义的,当一个字段从非repeated修改为repeated时,新的代码读取的历史数据是0或者1个长度的数组,旧代码读取的新数据是列表的最后一个元素;
- Thrift自带数组类型,因此不允许像Protobuf这样可以进行数组和非数组类型的转换。
Avro
Avro主要是针对Hadoop的处理场景产生的编码格式,Avro的格式和数据样例如下:
Avro的编码结果如下所示:
编码方式
Avro将读schema和写schema分离,分别用于解码和编码。当数据解码时,Avro会对比读schema和写schema,并完成数据从写schema到读schema的转换。这里对两份schema中字段的顺序是不关心的。
schema变化
添加或者删除字段,必须要该字段支持以null作为默认值,这样才可以保证后向兼容和前向兼容,null为默认值在Avro的schema中必须进行显式指定。
修改字段的数据类型比较容易,因为Avro会自动根据schema进行类型转换。修改字段名称会麻烦一点,读schema支持为字段设置别名,可以设置为修改前的写schema的字段名。因此修改字段支持后向兼容,但不支持前向兼容。
如何获取写schema
在进行数据解码时,如何获取写数据时使用的schema呢?这里有几种方式:
- 文件:对于Hadoop的场景,可能是一个大的文件,包含很多记录,那是可以把写schema存储在整个文件的开头。
- 数据库:写入到数据库中的每条记录使用的写schema可能不一样,因此在每条记录的开头保存写schema的版本号,然后在数据库中保存版本号和schema的对应关系。
- 网络传输:在连接建立时先将写schema传输过来,然后在整个连接过程都使用这个写schema。
动态生成schema
Avro和前两个格式相比,由于避免了必须显式的指定每个字段的标签顺序,因此可以很方便的由代码动态生成schema,在schema变化时是很方便的。
Schema的好处
在学习完JSON、XML和Thrift、Protobuf和Avro这些格式后,我们看到后者的schema定义要比前者更简单和直接,并且包含更加丰富的校验信息。
类似JSON这种,schemaless或者schema-on-read的格式灵活性是更好的,但二进制数据格式的schema可以带来以下的好处:
- 压缩比:摒弃了编码后数据中的字段名称;
- 可读性:schema是可读的,可以很直接的看到schema是否是最新的;
- 兼容性检查:将所有的schema保存在数据库中,可以直观的进行后向兼容和前向兼容的判断;
- 代码生成:为静态语言生成类代码,可在编译时进行代码检查。