Redis中万金油的String,为什么不好用了?( 二 )


因为 Redis 的数据类型有很多,而且 , 不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以 , Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据 。
一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的实际数据所在,例如指向 String 类型的 SDS 结构所在的内存地址,可以看一下下面的示意图 。关于 RedisObject 的具体结构细节,我会在后面的课程中详细介绍,现在你只要了解它的基本结构和元数据开销就行了 。

Redis中万金油的String,为什么不好用了?

文章插图
图片
为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计 。
一方面,当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销 。
另一方面,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片 。这种布局方式也被称为 embstr 编码方式 。
当然,当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构 。这种布局方式被称为 raw 编码模式 。
为了帮助你理解 int、embstr 和 raw 这三种编码模式,我画了一张示意图,如下所示:
Redis中万金油的String,为什么不好用了?

文章插图
图片
好了,知道了 RedisObject 所包含的额外元数据开销,现在 , 我们就可以计算 String 类型的内存使用量了 。
因为 10 位数的图片 ID 和图片存储对象 ID 是 Long 类型整数,所以可以直接用 int 编码的 RedisObject 保存 。每个 int 编码的 RedisObject 元数据部分占 8 字节 , 指针部分被直接赋值为 8 字节的整数了 。此时 , 每个 ID 会使用 16 字节 , 加起来一共是 32 字节 。但是 , 另外的 32 字节去哪儿了呢?
Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体 , 用来指向一个键值对 。dictEntry 结构中有三个 8 字节的指针 , 分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:
Redis中万金油的String,为什么不好用了?

文章插图
图片
但是,这三个指针只有 24 字节,为什么会占用了 32 字节呢?这就要提到 Redis 使用的内存分配库 jemalloc 了 。
jemalloc 在分配内存时 , 会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数 。
举个例子 。如果你申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果你申请 24 字节空间 , jemalloc 则会分配 32 字节 。所以,在我们刚刚说的场景里,dictEntry 结构就占用了 32 字节 。
好了 , 到这儿 , 你应该就能理解,为什么用 String 类型保存图片 ID 和图片存储对象 ID 时需要用 64 个字节了 。
你看,明明有效信息只有 16 字节,使用 String 类型保存时,却需要 64 字节的内存空间 , 有 48 字节都没有用于保存实际的数据 。我们来换算下,如果要保存的图片有 1 亿张,那么 1 亿条的图片 ID 记录就需要 6.4GB 内存空间 , 其中有 4.8GB 的内存空间都用来保存元数据了,额外的内存空间开销很大 。那么,有没有更加节省内存的方法呢?
用什么数据结构可以节省内存?Redis 有一种底层数据结构,叫压缩列表(ziplist),这是一种非常节省内存的结构 。
我们先回顾下压缩列表的构成 。表头有三个字段 zlbytes、zltAIl 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数 。压缩列表尾还有一个 zlend,表示列表结束 。
Redis中万金油的String,为什么不好用了?

文章插图
图片
压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据 。每个 entry 的元数据包括下面几部分 。
prev_len,表示前一个 entry 的长度 。prev_len 有两种取值情况:1 字节或 5 字节 。取值 1 字节时 , 表示上一个 entry 的长度小于 254 字节 。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束 , 其他表示长度的地方就不能再用 255 这个值了 。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节 。


推荐阅读