简介:本文系统梳理主流内存键值数据库五大数据结构的底层实现思路,重点拆解字符串对象 int、embstr、raw 三种内部编码的触发条件、内存布局与转换规则,并结合共享整数对象池、简单动态字符串等关键设计,帮助读者掌握内存效率优化的底层逻辑,为缓存选型、容量评估与性能调优提供扎实依据。
内存数据库将全部数据驻留在内存之中,内存既是性能优势的来源,也是成本的主要构成。同样的业务数据,采用不同的内部表示,内存占用可能相差数倍,读写路径的缓存局部性也会有明显差异。
因此,主流内存键值数据库普遍在“对外统一的数据类型接口”与“对内各异的具体表示”之间建立了一层编码(encoding)机制:高层看是字符串、哈希、列表、集合、有序集合五大数据结构;底层则依据值的类型、长度与元素特征,自动挑选最节省内存、访问效率最高的编码。理解这层机制,是做容量评估、性能调优与内存异常排查的基本功——为什么一个看似很小的键可能占用远超直觉的内存?为什么两个内容相似的键编码却不同?答案都藏在编码规则之中。
五大数据结构各自对应一组候选编码,典型的对应关系如下:
这套设计的共同思想可以概括为一句话:小数据用紧凑、连续、低开销的表示;大数据用支持高效增删与查找的常规结构。阈值通常由“元素数量”与“单个元素长度”两个维度共同决定。下面以最基础、使用最广泛的字符串对象为例,完整拆解这一机制。
当字符串对象保存的值本身是一个整数,且该整数能被 64 位有符号长整型(long)表示时,对象采用 int 编码。此时系统不会为字符串内容分配任何额外空间,而是直接把整数值存入对象结构中的指针字段(把原本指向数据的指针转换为长整型数值),并将编码标记设置为 int。一个 long 占用 8 字节,可表示的数值上界约为 2^63−1。
这里有一个容易被忽略的约束:把这个值当作字符串看待时,其长度不能超过 20。原因并不神秘——long 的最大值是 9223372036854775807,共 19 位数字;考虑负号,字符串形式最长为 20 个字符。一旦超过这个长度,数值必然超出 long 的表示范围,自然无法走 int 编码。这个看似奇怪的校验,本质上是“能否安全转换”的精确边界。
int 编码的收益不止于省掉字符串数据的内存,还引出一项更激进的优化——共享整数对象池。系统默认会预先创建 10000 个整数值对象(对应 0 到 9999),所有满足条件的键直接引用这些共享对象。这意味着,如果有多个不同的键,其值落在这一区间内,它们事实上全部指向同一个对象,通过引用计数统一管理生命周期。对于计数器、状态位、等级、评分等大量使用小整数的业务场景,这一设计能把“每个键一份对象”的开销压缩为“全局一份、多处引用”,节省效果非常可观。
示意性的验证方式如下(不同实现的命令语法略有差异):
# 写入一个整数值> SET number "1234"OK# 查看键 number 的内部编码> OBJECT ENCODING number"int"
由于 “1234” 是可用 long 表示的整数,编码结果为 int。
当字符串长度不超过 44 字节(早期版本阈值为 39 字节)时,采用 embstr 编码。它的核心特征是:对象头(包含类型、编码、引用计数、数据指针等元信息)与字符串数据头被放进同一块连续内存,通过一次内存分配完成。
相比下面要讲的 raw,embstr 有三个明显优势:
关于 44 这个阈值,业界一种被广泛接受的解释是内存分配器按固定档位供给内存:对象头占 16 字节,短字符串的数据头占 3 字节,结尾符占 1 字节,合计 20 字节,44 = 64 − 20,恰好凑满 64 字节的分配档位,一个字节都不浪费;早期版本的数据头为 8 字节,故阈值为 64 − 16 − 8 − 1 = 39。
还需注意 embstr 的一个重要特性:它是只读的。对 embstr 编码的字符串执行任何修改操作,都会使其整体转换为 raw 编码,而非原地修改。因为重新分配一整块连续内存并搬移数据的代价,通常高于直接切换编码。这也提示我们:会被反复修改的短字符串,首次修改后就固定为 raw,不必期待它再回到 embstr。
当字符串长度超过 44 字节时,采用 raw 编码:对象结构与字符串数据分别进行两次内存分配,二者在内存中并不保证相邻。
表面看 raw 更“浪费”,但它换来了灵活性:数据部分可以独立扩容、缩容,而不牵动对象头;配合下述动态字符串的预分配策略,长字符串的追加写入无需频繁整体搬移。存放长文本、序列化后的结构化数据、图片字节流等大对象时,raw 是必然且合理的选择。
无论 embstr 还是 raw,字符串数据的实际载体都是“简单动态字符串”。相比 C 语言原生以 ‘\0’ 结尾的字符数组,它增加了一个记录已用长度、已分配长度与类型标记的头部,带来四项关键收益:
更进一步,头部结构本身也按字符串长度分档:极短的字符串使用 1 字节级别的长度字段,更长的才启用更宽的字段,在元数据开销上继续“斤斤计较”。
从 int 的“指针复用”,到共享整数池的“以引用换空间”,再到 embstr 的“一次分配、连续布局”,五大数据结构的底层实现处处体现着内存数据库“每一字节都要精打细算”的工程哲学。无论是自建部署,还是依托百度智能云等云平台的计算与存储资源构建内存服务,掌握这些编码规则,都能帮助我们在选型、容量评估与性能调优中做出有依据的判断——这正是从“会用”走向“精通”的分水岭。