Redis String、Hash 内存优化

Categories: 编程

问题

💡 如果存储上亿条数据,如何进行内存优化?是使用 String 存储还是 Hash 存储?

假设存储的数据是用户信息,即 userId 以及 userData。 假设两者的长度都是 8 字节

String 格式写入

1 条数据理论上应该占用 16 字节,即 8 字节 userId + 8 字节 userData。但是实际上内存占用比这个更大。

为了实现从键到值的快速访问,Redis 使用了一个全局哈希表来保存所有键值对。其中 Key 用 SDS 格式存储,Value 则用 RedisObject 存储,RedisObject 再指向具体的底层数据结构。

String 底层实现

Redis 没有使用 C 语言传统的字符串(以空字符为结尾的字符数组),而是自己创建了一种名为 SDS(简单动态字符串)的抽象类型,用作 Redis 默认的字符串。SDS 的定义如下:

struct sdshdr {
    int len;         // 记录 buf 数组中已使用字节的数量
    int free;        // 记录 buf 数组中未使用字节的数量
    char buf[];      // 字节数组,用于保存实际字符串
}

一个简单的 set 命令最终会产生 4 个消耗内存的结构:

  • 一个 dictEntry 结构,24 字节,负责保存具体的键值对
  • 一个 redisObject 结构,16 字节,负责保存 value 对象
  • 一个 SDS 结构,(sizeof(int) * 2 + 1 个结束符 + key 长度),用作 key 字符串
  • 一个 SDS 结构,(sizeof(int) * 2 + 1 个结束符 + value 长度),用作 value 字符串

当 key 个数逐渐增多,redis 还会以 rehash 的方式扩展哈希表节点数组,即增大哈希表的 bucket 个数,每个 bucket 元素都是个指针(dictEntry*),占 8 字节,bucket 个数是超过 key 个数向上求整的 2 的 n 次方。

由此可见,在 String 格式中,Redis 自身的数据结构占了绝大部分的内存。通过查阅相关资料,我们可以使用 Hash 格式的压缩表来压缩存储。

Hash 格式写入

Hash 底层实现

Hash 底层是 ziplist 与 hashtable 结构之一。

当一个哈希对象可以满足以下两个条件中的任意一个,哈希对象会选择使用 ziplist 来进行存储:

  1. 哈希对象中的所有键值对总长度 (包括键和值) 小于 64 字节(这个阈值可以通过参数 hash-max-ziplist-value 来进行控制)。
  2. 哈希对象中的键值对数量小于 512 个(这个阈值可以通过参数 hash-max-ziplist-entries 来进行控制)。

一旦不满足这两个条件中的任意一个,哈希对象就会选择使用 hashtable 来存储。

压缩表实际上类似一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表的结束。Entry 中保存了 field 和 Value 数据。

Hash 在使用压缩表的数据结构时只使用了少量 Redis 自身的对象,可以极大的节约内存。

如何存储

KV 结构的数据如何才能存入 key/field/value 格式呢?我们可以采用基数排序的思想,将数据均匀的分散在不同的桶中,将每个桶中的数据放入对应的 key 中。

为保持分桶均匀,我们使用 Crc64 算法对用户 ID 作 hash 计算。将计算后的 Long 值做拆分,取前 19 个比特位(524287 个分桶)计算 key,取后 45 个比特位作为 filed,用户数据作为 data。最终 6.5 亿数据拆分的分桶数为 524287,每个分桶中有 1240 个数据。

Read More

Spring Bean 的 Scope、生命周期和循环依赖分析

【2024-02-02】