【DDIA】编码和演进

Capture IV – 编码和演进

上一章聊的是存储引擎,本章继续下钻,探讨编码相关问题。

所有涉及跨进程通信的地方,都需要对数据进行编码(Encoding),或者说序列化(Serialization)。因为持久化存储和网络传输都是面向字节流的。编码本质上是一种“降维”操作,将内存中高维的数据结构降维成单维的字节流,于是底层硬件和相关协议,只需要处理一维信息即可。而解码(Decoding),或者说反序列化(Deserialization),便是反向地将字节流还原为各种数据结构。

在这里插入图片描述

在进行编码时,往往需要考虑如下两方面问题:

  • 如何编码能够节省空间、提高性能?
  • 如何编码以适应数据的演化和兼容?

为了避免出现歧义,我们在此规定:

  • 向后兼容 (backward compatibility):当前代码可以读取历史版本代码接受的数据。
  • 向前兼容 (forward compatibility):当前代码可以读取未来版本代码接受的数据。

数据编码的格式

编程语言内置

这一节,我们聊聊几种常见的编码工具(JSON,XML,Protocol Buffers 和 Avro),他们是如何进行编码、如何进行多版本兼容。

相信大家在日常写代码的过程中会发现,很多编程语言内置了一些缺省的编码方法:

  • Java 有 java.io.Serializable
  • Ruby 有 Marshal
  • Python 有 pickle

如果你确定你的数据只会被某种特定的语言所读取,那么直接用内置的编码方法即可。但这些编程语言内置的编码格式有以下缺点:

  • 和特定语言绑定
  • 安全问题
  • 兼容性支持不够
  • 效率不高

文本编码

JSON,XML 和 CSV 属于常用的文本编码格式,其好处在于肉眼可读,坏处在于不够紧凑,占空间较多。

  • JSON 最初由 JavaScript 引入,因此在 Web Service 中用的较多,当然随着 Web 的火热,现在成为了比较通用的编码格式,比如很多日志格式就是 JSON 的。
  • XML 比较古老了,比 JSON 冗余度还高,现在基本上是在配置文件用到,但总体而言用的越来越少了。
  • CSV(以逗号、TAB、换行符分割)还算紧凑,但是表达能力有限。数据库表导出有时会用。

除了不够紧凑外,文本编码(text encoding) 还有以下缺点:

  • 对数值类型支持不够。CSV 和 XML 直接不支持,万物皆字符串。JSON 虽区分字符串和数值,但是不进一步区分细分数值类型。可以理解,毕竟是文本编码,主要还是面向字符串。
  • 对二进制数据支持不够。支持 Unicode,但是对二进制串支持不够,可能会显示为乱码。虽然可以通过 Base64 编码来绕过,但有点做无用功的感觉。
  • XML 和 JSON 支持额外的模式。模式会描述数据的类型,告诉你如何理解数据。配合这些模式语言,虽然可以让 XML 和 JSON 变得强大,但是大大增加了复杂度。
  • CSV 没有任何模式。

很多场景下需要数据具备一定的可读性,并且不关心编码效率,那么这几种编码格式就够用了。

二进制编码

如果数据只被单一程序读取,不需要进行交换,不需要考虑易读性等问题,则可以用二进制编码,在数据量达到一定程度后,二进制编码所带来的空间节省、效率提升都很可观。

因此,JSON 有很多二进制变种:MessagePack、BSON、BJSON、UBJSON、BISON 和 Smile 等。我们以 MessagePack 为例,他在对性能有较高要求的场景中,是 JSON 的一个热门的高效替代品。那么他是如何进行编码的呢?

对于下面例子,

{
  "userName": "Martin",
  "favoriteNumber": 1337,
  "interests": ["daydreaming", "hacking"]
}

如果用 MessagePack 来编码,则为:

83                     // 3个元素的map
a8 757365724e616d65  // "userName" (8字节)
a6 4d617274696e      // "Martin" (6字节)
ae 6661766f726974654e756d626572  // "favoriteNumber" (14字节)
cd 0539              // uint16 1337 (0x0539 = 1337)
a9 696e74657265737473 // "interests" (9字节)
92                    // 2个元素的数组
ab 646179647265616d696e67  // "daydreaming" (11字节)
a7 6861636b696e67         // "hacking" (7字节)

在这里插入图片描述

可以看出其基本编码策略为:使用类型,长度,bit 串,顺序编码,去除无用的冒号、引号、花括号。
从而将 JSON 编码的 81 字节缩小到了 66 字节,微有提高。

Thrift & Protocol Buffers

Thrift 最初由 Facebook,ProtoBuf 由 Google 在 07~08 年左右开源。他们都有对应的 RPC 框架和编解码工具。表达能力类似,语法也类似,在编码前都需要由接口定义语言(IDL)来描述模式。

什么是 IDL 呢?IDL 是编程语言无关的,这体现了微服务架构中非常重要的“契约先行”开发模式,利用相关代码生成工具,可以将上述 IDL 翻译为指定语言的代码。也就是说,集成这些生成的代码,无论什么样的语言,都可以使用同样的格式编解码。这也是使用不同编码语言的服务能够互相通信的基础。

还是这个例子,

{
  "userName": "Martin",
  "favoriteNumber": 1337,
  "interests": ["daydreaming", "hacking"]
}

Thrift IDL 结构如下:

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

有了上面的 IDL 后,Thrift 就可以直接对数据内容进行编码,其支持多种不同的编码格式,常用的有:Binary、Compact、JSON。可以让用户自行在:编码速度、占用空间、可读性方便进行取舍。

Binary 编码结果如下:

0B        // 字段1的类型:STRING (11)
00 01     // 字段1的ID:1

00 00 00 06 // 字符串长度:6 (Martin 占6个字节)
4D 61 72 74 69 6E // "Martin" 的 ASCII 码 (M a r t i n)

// --- 字段2 ---
0A        // 字段2的类型:I64 (10)
00 02     // 字段2的ID:2

00 00 00 00 00 00 05 39 // 数字 1337 的大端字节序(Big-Endian)表示 (0x539)

// --- 字段3 ---
0F        // 字段3的类型:LIST (15)
00 03     // 字段3的ID:3

0B        // 列表中元素的类型:STRING (11)
00 00 00 02 // 列表长度:2 (有两个字符串)

// 第一个字符串 "daydreaming"
00 00 00 0B // 长度:11
64 61 79 64 72 65 61 6D 69 6E 67 // "daydreaming"

// 第二个字符串 "hacking"
00 00 00 07 // 长度:7
68 61 63 6B 69 6E 67 // "hacking"

00        // 停止标记:STOP (0)

在这里插入图片描述

可以看出其特点:

  • 使用 field tag 编码。field tag 其实蕴含了协议中的字段类型和名字。
  • 使用类型、tag、长度、bit 数组的顺序编码。

再看看 Compact 编码:
在这里插入图片描述

相比 Binary Protocol,Compact Protocol 由以下优化:

  • filed tag 只记录增量 delta。如果字段ID是1,2,3,只需要存 1,1,1 而不是 1,2,3,以便节省空间,从而将 field tag 和 type 压缩到一个字节中。
  • 对数字使用变长编码和 Zigzag 编码。

ProtoBuf 与 Thrift Compact Protocol 编码方式很类似,也用了变长编码和 Zigzag 编码。但 ProtoBuf 对于数组的处理与 Thrift 显著不同,使用了 repeated 前缀而非真数组,其好处在于兼容数组类型的同时,支持将可选(optional)单值字段,修改为多值字段。修改后,旧代码在看到新的多值字段时,只会使用最后一个元素。

ProtoBuf IDL 结构如下:

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

在这里插入图片描述

在聊完了编码方式后,我们回到全文一开始提出的第二个问题:这些编码如何适应数据的演化和兼容?

随着时间的推移,业务总会发生变化,我们也不可避免的增删字段,修改字段类型,即模式演变。在模式发生改变后,需要:

  • 向后兼容:新的代码,在处理新的增量数据格式的同时,也得处理旧的存量数据。
  • 向前兼容:当前代码,需要以后可拓展。

Thrift 和 ProtoBuf 是怎么解决这两个问题的呢?

  • 字段标号 + 限定符(optional、required)保证向后兼容:新加的字段需为 optional。这样在解析旧数据时,由于新字段是可选的,就不会出现字段缺失的情况。
  • 向前兼容:字段标号不能修改,只能追加。这样旧代码在看到不认识的标号时,省略即可。

数据流模型

数据可以以很多种形式从一个系统流向另一个系统,但不变的是,流动时都需要编码与解码。在数据流动时,会涉及编解码双方模式匹配问题,上一节已经讨论。本小节主要探讨几种进程间典型的数据流方式:

  • 通过数据库
  • 通过服务调用
  • 通过异步消息传递

经由数据库的数据流

访问数据库的程序,可能:

  • 只由同一个进程访问。
  • 由多个进程访问。则多个进程可能有的是旧版本,有的是新版本,此时数据库需要考虑向前和向后兼容的问题。

对于应用程序,可能很短时间就可以由旧版本替换为新版本。但是对于数据,旧版本的代码写入的数据量,经年累月可能很多。在变更了模式之后,由于这些旧模式的数据量很大,全部更新对齐到新版本的代价很高。这种情况我们称之为:数据的生命周期超过了其对应代码的生命周期。

在读取时,数据库一般会对缺少对应列的旧数据

  • 填充新版本字段的默认值(default value)
  • 如果没有默认值则填充空值(nullable)

后返回给用户。一般来说,在更改模式时(比如 alter table),数据库不允许增加既没有默认值、也不允许为空的列。

经由服务的数据流

通过网络通信时,通常涉及两种角色:服务端(server)和客户端(client)。通常来说,暴露于公网的多为 HTTP 服务,而 RPC 服务常在内部使用。

服务端和客户端只是一个相对的概念。服务端也可以同时是客户端:

  • 作为客户端访问数据库。
  • 作为客户端访问其他服务。我们常把一个大的服务拆成一组功能独立、相对解耦的服务,这就是 面向服务的架构(service-oriented architecture,SOA),或者微服务架构(micro-services architecture)。

基于二进制编码的 RPC 通常比基于 HTTP 服务效率更高。但 HTTP 服务,或者更具体一点,Restful API 的好处在于,生态好、有大量的工具支持。而 RPC 的 API 通常和 RPC 框架生成的代码高度相关,因此很难在不同组织中无痛交换和升级。因此,这也是为什么“暴露于公网的多为 HTTP 服务,而 RPC 服务常在内部使用”。

经由消息传递的数据流

采用介于两个服务之间的异步消息系统:消息队列。与 RPC 相比,使用消息队列的优点:

  • 如果消费者暂时不可用,可以充当暂存系统。
  • 当消费者宕机重启后,自动地重新发送消息。
  • 生产者不必知道消费者 IP 和端口。
  • 能将一条消息发送给多个消费者。
  • 将生产者和消费者解耦。

近年来,消息队列越来越流行,可以适应不同场景,如 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 等等。

消息队列的送达保证因实现和配置而异,包括:

  • 最少一次(at-least-once):同一条数据可能会送达多次给消费者。
  • 最多一次(at-most-once):同一条数据最多会送达一次给消费者,有可能丢失。
  • 严格一次(exactly-once):同一条数据保证会送达一次,且最多一次给消费者。

消息队列的逻辑抽象叫做 Queue 或者 Topic,常用的消费方式两种:

  • 多个消费者互斥消费一个 Topic
  • 每个消费者独占一个 Topic

我们有时会区分这两个概念:将点对点的互斥消费称为 Queue,多点对多点的发布订阅称为 Topic,但这并不通用,或者说没有形成共识。

一个 Topic 提供一个单向数据流,但可以组合多个 Topic,形成复杂的数据流拓扑。消息队列通常是面向字节数组的,因此你可以将消息按任意格式进行编码。如果编码是前后向兼容的,同一个主题的消息格式,便可以进行灵活演进。

最后,我们再来聊聊 Actor 模型。Actor 模型可以理解为"微观"的消息队列,Actor 模型是一种基于消息传递的并发编程模型,把消息传递的理念下沉到了编程模型层面;而消息队列是"宏观"的 Actor 系统,把 Actor 的理念放大到了系统架构层面。

Actor 通常是由状态(State)、行为(Behavior)和信箱(MailBox,可以认为是一个消息队列)三部分组成:

  • 状态:Actor 中包含的状态信息。
  • 行为:Actor 中对状态的计算逻辑。
  • 信箱:Actor 接受到的消息缓存地。
打开链接下载源码: https://pan.quark.cn/s/a4b39357ea24 QT框架是由Qt公司设计的一种跨平台C++图形用户界面应用程序开发工具包,该框架被广泛地应用于桌面电脑、移动设备以及嵌入式系统等领域。QTableView作为QT框架中的一个核心组件,其主要功能是用于展示表格形式的数据,并且常常与QAbstractItemModel或QSqlTableModel等模型类协同工作。在QTableView中嵌入自定义组件,例如按钮,能够实现更加多样化的用户交互功能。 在QT框架环境下,若想在QTableView的一列中嵌入两个按钮,我们需要掌握以下几个关键的技术要点: 1. **QTableView**:QTableView是QTableView类的一个实例,它提供了一个二维的表格视图界面,可以用来展示编辑模型中的数据。QTableView能够显示由QAbstractItemModel子类所提供的数据,例如QStandardItemModel或QAbstractTableModel等。 2. **QTableWidgetItem**:在QTableView中,QTableWidgetItem是构成表格单元格的基本对象,它用于表示表格中每一行每一列的数据。在默认情况下,QTableView仅能展示文本信息,但通过继承QTableWidgetItem并重新绘制,我们可以实现自定义的内容,比如嵌入按钮。 3. **自定义视图项**:若要在单元格内部嵌入两个按钮,我们需要开发一个自定义的QTableWidgetItem子类,该子类中包含两个QPushButton。这个子类需要重写paintEvent()方法以绘制按钮,并且实现必要的信号槽机制来处理按...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值