应用程序需要不断迭代,迭代意味着需要改变存储的数据(模式)。对于关系数据库,尽管可以更改模式,但在任意时刻有且仅有一个正确的模式;而读时模式数据库,不会强制一个模式,因此新老数据格式的可以同时存在。
数据模式变化时,代码也需要相应的变化,大型应用更改代码通常:
- 对于服务端,需要滚动升级(rolling upgrade) (也称为 阶段发布(staged rollout) )
- 对于客户端,看用户的心情
此时,新旧版本的代码、新旧版本的数据格式可能会同时存在。因此,系统想要正常工作就需要保持双向兼容性:
-
向后兼容(backward compability): 新代码可以读取旧数据
- 保留旧代码即可读取旧数据
-
向前兼容(forward compability):旧代码可以读取旧数据
- 旧版程序需要忽略新版数据格式中新增的部分
编码的数据的格式
程序中数据的两种格式:
- 在内存中,数据保存在对象、结构体、数组等中。这些数据结构对CPU操作进行了优化(通常使用指针)。
- 将数据写入文件,或通过网络发送,则必须将其**编码(encode)**为某种自包含的字节序列(如,JSON文档)。
因此,需要在两种格式之间进行翻译。编码(Encoding):从内存中表示到字节序列;解码(Deconding):从字节序列到内存表示。
语言特定的格式
很多变成语言内置了将内存对象编码为字节序列的支持。
优点:
- 方便
- 实现简单
缺点:
- 编码与特定的编程语言深度绑定,其他语言很难读取这种数据。不同系统之间难以集成。
- 解码过程需要实例化任意类的能力,存在安全问题
- 未考虑前后兼容性问题
- 未考虑效率问题
JSON、XML和二进制变体
可以被多种语言读写的标准编码
优点:
- 文本格式,具有可读性
XML:
- 冗长、负载
JSON:
- 简单
- Web浏览器内置支持
CSV:
- 功能较弱
其他问题:
- 数值(numbers)编码多有歧义之处。XML和CSV不能区分数字和字符串。JSON不区分整数和浮点数,且不能指定精度
- JSON和XML对Unicode字符串支持很好,但它们不支持二进制数据。
- XML和JSON都有可选的模式支持。但实现复杂。XML模式使用普遍,但JSON一般不会折腾模式。
- CSV没有任何模式。
二进制编码
JSON比XML简单,但与二进制格式相比还是太占空间。因此有一些二进制格式版本的JSON和XML出现。但都不出JSON和XML那样使用广泛。
因为它们没有改变JSON/XML的数据模型。特别是它们没有规定模式,所以需要在编码数据中包含所有的对象字段名称。
二进制编码不需要存储字段名称,可以少占一点空间。
Thrift与Proto Buffers
Apache Thrift 与 Proto Buffers(protobuf)是基于相同原理的二进制编码库。它们都需要一个模式来编码数据。Thrift使用**接口定义语言(IDL)**来描述模式,Proto Buffer使用 protobuf (也是IDL的思想)来描述模式。
它们都有配套的代码生成工具,可以根据定义的模式生成对应语言的实现模式类。
Thrift有两种编码格式:
-
BinaryProtocol
-
CompactProtocol
-
DenseProtocol
- 只支持C++
protobuf只有一种二进制编码格式。
protobuf编码示例
字段标签和模式演变
💡 Thrift和Protocol Buffers如何处理模式更改,同时保持向后兼容性?编码记录就是其编码字段的拼接。每个字段由其标签号码标识,并用数据类型注释。如果没有设置字段值,则简单地从编码记录中省略。字段标签对编码数据的含义至关重要。可以更改字段的名称,因为编码数据永远不会引用字段名称,但不能更改字段的标记,因为这会使所有现有的编码数据无效。
编码字段数据 = (标签,类型,数据)
添加新的字段?:
- 为该字段设置一个新的标签
如何保持向后兼容性?
如果旧代码读取到新标签,会忽略该字段。数据类型可以让解析器跳过指定的字节数。这保持了向前兼容性。
如何保证向后兼容性?
只要每个字段都有一个唯一的标签,新代码总是可以读取旧的数据,因为标签号仍然具有相同的含义。要注意的是,如果添加一个新的字段,该字段不能设置为required,否则会检查失败,因为旧代码不会新添加的字段。因此,为了保持向后兼容性,在模式的初始部署之后 添加的每个字段必须是可选的或具有默认值。
如何删除字段?
考虑到向前兼容性,只能删除可选的字段(必需字段永远不能删除),而且不能再次使用相同的标签号。
数据类型和模式的演变
如何改变字段的数据类型?看具体的文档吧。
风险:值将失去精度或被截断。
模式的优点
与XML或JSON相比:
- 简单
- 更紧凑:因为可以省略编码数据中的字段名称。
- “模式”即文档:它是解码所必需的,所以可以确定它是最新的。
- 维护前后兼容性
- 编译时类型检查
数据流的类型
兼容性是编码数据的一个进程和解码它的另一个进程之间的一种关系。
数据可以以多种方式从一个进程流向另一个进程。谁编码数据了,谁解码?
数据库中的数据流
编码:写入数据库
解码:读取数据库
兼容性:
- 向后兼容性是必需的,否则无法解码以前写的东西。
- 新旧版本的代码可能会同时存在,就版本代码的进程可能会读取由新版本代码进程写入的数据,因此,数据库也需要向前兼容
问题:
- 数据丢失问题:新代码将新字段写入数据库,旧代码读记录,更新记录并写回,未知字段在翻译过程中可能会丢失
服务中的数据流:REST与RPC
常见的通信方式是客户端与服务端之间进行通信。
客户端可以是:
- 浏览器
- 移动设备
- 应用程序
- …
服务端可以是:
-
服务器
-
另一个服务的客户端
- 面向服务的体系结构(SOA)(也称为 微服务架构)
客户端和服务端使用HTTP作为传输协议,顶层实现的API是特定于应用程序的,客户端和服务端需要就API的细节达成一致,它们使用的数据编码必须在不同版本的服务API之间兼容。
Web服务
两种流行的Web服务方法:REST和SOAP
REST
一个基于HTTP原则的设计哲学:
- 强调简单的数据格式
- 使用URL来标识资源
- 使用HTTP功能进行缓存控制、身份验证和内容协商
SOAP
用于制作网络API请求的基于XML的协议。
RPC的问题
RPC模型视图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同,但网络请求与本地函数调用非常不同:
- 可预测性。本地函数调用可预测,而网络调用不可预测。
- 超时。网络请求可能会超时,而没有返回结果。此时服务器不会响应,你压根不知道发生了什么。
- 幂等性。如果使用了重试机制,需要考虑幂等问题。
- 稳定性。本地调用每次的耗时基本相同,而网络调用有可能因为延迟,导致耗时差异巨大。
- 数据传输。本地调用可以传指针,网络需要编码、解码,对较大对象是个问题。
- 编程语言不同。客户端和服务端可以采用不同的编程语言,不是所有的语言都具有相同的类型。
消息传递中的数据流
与RPC类似,客户端请求以低延迟传送到另一个进程;
与数据库类似,不是通过直接的网络连接发送消息,而是通过消息代理(消息队列或消息中间件)来临时存储消息;
消息代理相对于RPC的优点:
- 缓冲。如果收件人不可用或过载,可以充当缓冲区,从而提高系统的可靠性。
- 防丢失。它可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失。
- 重复消费。它允许将一条消息发送给多个收件人。
- 解耦。将发件人与收件人逻辑分离(发件人只是发布邮件,不关心使用者)
与RPC的区别:
- 单向传递。消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。
- 异步。发送者不会等待消息被传递,而只是发送它,然后忘记它。
消息代理
通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给那个队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上,或者发送给原始消息的发送者使用的回复队列
消息代理通常不会执行任何特定的数据模型 —— 消息只是包含一些元数据的字节序列,因此你可以使用任何编码格式。如果编码是向后和向前兼容的,你可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。