2. 数据类型基础回顾:基本数据类型(int、string、float、bool)、复合类型(list、dict、struct)的底层存储与序列化差异

做字段级加密,说白了就是在数据离开内存之前,先把某些字段“封住”。但封住之前,你得先搞清楚这些字段在内存里长什么样,序列化之后又变了什么样子。我刚开始接触加密 SDK 时,就踩过一个坑:把一个 int 按大端序列化之后加密,服务端却用小端去解,出来一堆乱码。所以这一章,我们老老实实把数据类型的底牌翻一遍。

基本数据类型的底层存储

每种基本类型在内存中的布局,其实是由 CPU、编译器、语言运行时共同决定的。我习惯把它们分成两类:定长类型和变长类型。

int(整数)

常见的 int32、int64 都是定长。比如 int32 固定 4 字节。但有个细节很容易忽略——字节序。x86 用 little-endian,网络协议用 big-endian。我几年前做过一个跨语言加密方案,Go 端序列化用了小端,Java 端默认大端,结果整整 debug 了一下午。后来我强制所有加密前统一转成 big-endian,再没出过问题。

还有一点:有些加密 SDK 为了压缩空间,会把小整数用 varint(变长编码)表示。比如 Protobuf 的 int32 实际存储是 varint,位数越少的数字占越少字节。但这个变长会导致密文长度不确定,你如果要做字段级固定长度对齐,那就得小心了。

注意:加密前一定要明确字节序。建议统一使用 big-endian(网络字节序)作为加密输入标准,避免跨平台、跨语言时出现乱码。

string(字符串)

字符串在内存里是一段连续的字节,但编码方式直接影响字节数。UTF-8 是字节流,每个字符 1-4 字节不等;UTF-16 是双字节流,有 BOM 头的问题。我建议字段级加密时,统一使用 UTF-8 编码,并且额外记录字符串的字节长度——不是字符长度。为什么?因为加密是对字节操作的,你不知道字符边界,解密后根本恢复不了原字符串。

序列化时,通常有两种方式:长度前缀法(先写 4 字节长度,再写内容)或 空字符结尾(C 风格)。加密 SDK 大多推荐长度前缀,因为空字符可能出现在内容中,截断数据。我曾经在一个遗留系统里看到用 '\0' 结尾的加密方案,结果用户昵称里带了个 '\0'……嗯,后面的事情大家都猜到了。

float / double(浮点数)

IEEE 754 标准是主流,float 4 字节,double 8 字节。但这玩意在序列化时有个大坑:精度丢失。同样的浮点值,在不同的编译器优化级别下,最后两个 bit 可能不同。字段级加密要求加密前后完全一致,否则解密出来对不上。所以我个人习惯的做法是:加密之前先把浮点数转成 定点数(比如乘以 10^6 后取整),或者直接用字符串保存。你想想看,乘法算出的结果和除法算回去,误差有时候就出现在尾数上,加密一旦掺进去,怎么对都对不回来。

经验:如果业务允许,把 float 转成 int 再加密。如果必须保留浮点类型,建议用 decimal 库先格式化到固定精度,再按字符串序列化。

bool(布尔值)

看起来最简单,但其实也有坑。C++ 里 bool 占 1 字节,Python 的 bool 是 int 的子类,序列化后变成 1 或 0。但有些系统会把 true 序列化成 1,有些序列化成 0xFF。字段级加密必须统一序列化协议,否则密文长度和内容都不一样。我建议把 bool 统一映射成单字节 0x00 和 0x01,别用多字节表示。

复合类型:底层存储与序列化差异

list(列表 / 数组)

底层存储最常见的是连续内存(像 C 数组)或链表。序列化时,我们要考虑:

  • 元素个数:必须提前或一起写入。
  • 元素类型:如果是异构 list,每个元素得带上类型标记。
  • 变长元素:比如 list 里的字符串,每个字符串需要独立长度前缀。

我经历过一个性能问题:加密一个包含 10 万条小整数的 list,如果每加密一个元素都重新计算长度、申请内存,时间翻了好几倍。后来改成先批量计算总序列化长度,一次性分配 buffer,再逐个写入加密,速度提升了近 80%。

dict / map(字典)

底层是哈希表或平衡树。序列化时最大的差异在于 key 的排序。有些序列化协议(比如 JSON)不保证 key 顺序,但加密要求确定性——同样的数据必须产生同样的密文。如果不排序,每次序列化出来的字节流不同,加密结果也会不同。

我建议:无论是 encrypt 还是 decrypt,都要先对 dict 的 key 做字典序排序。如果 key 是非字符串类型,还得约定一个通用的序列化规则(比如 int 统一转成 big-endian 再按字节比较)。

序列化协议key 排序规则对加密的影响
JSON通常不排序每次结果可能不同 → 不可加密
Protobuf按 field number确定性,可直接加密
MsgPack取决于实现需要额外排序步骤

struct(结构体 / 对象)

底层是内存对齐后的连续字段。不同的语言、编译器可能会在字段间插入填充字节(padding)。比如 C:

struct Example {
    char a;    // 1 字节
    int b;     // 4 字节(实际从偏移 4 开始,因为 4 字节对齐)
};  // 总共 8 字节,而不是 5 字节

这种 padding 在不同架构下可能不同。如果把整个 struct 视为一个整体直接加密,换一台机器或换个编译器,解密出来的 padding 位置不一样,数据就坏了。所以字段级加密的最佳实践是:不要直接加密内存结构体,而是将每个字段提取出来,按显式序列化规范(比如 TLV:Type-Length-Value)逐一加密。

另外,struct 里的字段顺序也很关键。我见过一个案例:A 系统把 struct 的字段顺序改了一下,结果加密后的密文长度变了,下游解析全乱。所以一定要把字段顺序固化成 schema,用代码管理起来。

核心结论:复合类型的序列化,必须保证:① 字段顺序确定;② 元素类型确定;③ 长度明确;④ 无歧义的填充规则。加密之前,先把复合类型拍平成确定的字节序列。

一张图看懂数据类型适配要点

下面这张 SVG 图,我把基本类型和复合类型的关键存储特征、序列化注意事项、以及对应的加密适配建议画出来了。你可以用它快速对照检查。

数据类型底层存储与序列化差异 · 加密适配要点 基本类型 int 定长4/8字节 注意大小端 → 统一 big-endian string 变长,UTF-8 bytelen 记录字节长度+内容 float/double IEEE 754 精度 建议转定点数或字符串 bool 1字节 0x00/0x01 统一不允许多字节表示 加密适配层:统一序列化规范(字节长度 + 字节序 + 编码) 每个字段单独提取 → 显式 TLV 序列化 → 字段级加密 复合类型 list / 数组 元素个数 + 每个元素序列化 变长元素加长度前缀 同构类型可批量加密 dict / map key 必须排序 value 序列化规则同字段 避免不确定性 struct / 对象 字段顺序固定 消除 padding 每个字段单独加密 加密适配原则:确定性 + 可逆 + 跨平台 对字节操作,不对语言语义操作

这张图其实把上面说的内容串起来了:基本类型各自有各自的坑,复合类型又叠加了顺序、长度、填充的问题。但只要你抓住“确定性”这根弦,在加密之前先拍成统一的字节序列,后面的适配就稳了。

一个小总结

数据类型这件事,看起来基础,但却是字段级加密最容易翻车的地方。我自己的经验是:写代码之前,先画一张每个字段的序列化描述表,标注好字节序、长度、编码、对齐规则。然后固化到 SDK 的配置文件中。别指望不同语言、不同版本的编译器自动帮你做对——它们不会。