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


len:表示自身长度,4 字节;
encoding:表示编码方式,1 字节;
content:保存实际数据 。
这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间 。
我们以保存图片存储对象 ID 为例 , 来分析一下压缩列表是如何节省内存空间的 。
每个 entry 保存一个图片存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只需要 1 个字节就行,因为每个 entry 的前一个 entry 长度都只有 8 字节,小于 254 字节 。这样一来 , 一个图片的存储对象 ID 所占用的内存大小是 14 字节(1+4+1+8=14),实际分配 16 字节 。
Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型,这样做的最大好处就是节省了 dictEntry 的开销 。当你用 String 类型时,一个键值对就有一个 dictEntry,要用 32 字节空间 。但采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了很多,但也只用了一个 dictEntry,这样就节省了内存 。
这个方案听起来很好 , 但还存在一个问题:在用集合类型保存键值对时,一个键对应了一个集合的数据,但是在我们的场景中,一个图片 ID 只对应一个图片的存储对象 ID,我们该怎么用集合类型呢?换句话说,在一个键对应一个值(也就是单值键值对)的情况下,我们该怎么用集合类型来保存这种单值键值对呢?
如何用集合类型保存单值的键值对?在保存单值的键值对时 , 可以采用基于 Hash 类型的二级编码方法 。这里说的二级编码,就是把一个单值的数据拆分成两部分 , 前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了 。
以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前 7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象 ID 分别作为 Hash 类型值中的 key 和 value 。
按照这种设计方法,我在 Redis 中插入了一组图片 ID 及其存储对象 ID 的记录,并且用 info 命令查看了内存开销,我发现,增加一条记录后 , 内存占用只增加了 16 字节,如下所示:
127.0.0.1:6379> info memory# Memoryused_memory:1039120127.0.0.1:6379> hset 1101000 060 3302000080(integer) 1127.0.0.1:6379> info memory# Memoryused_memory:1039136在使用 String 类型时,每个记录需要消耗 64 字节,这种方式却只用了 16 字节,所使用的内存空间是原来的 1/4,满足了我们节省内存空间的需求 。
不过,你可能也会有疑惑:“二级编码一定要把图片 ID 的前 7 位作为 Hash 类型的键,把最后 3 位作为 Hash 类型值中的 key 吗?”其实,二级编码方法中采用的 ID 长度是有讲究的 。
Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表 。
那么,Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?其实 , Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了 。
这两个阈值分别对应以下两个配置项:
hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数 。
hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度 。
如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表 。
一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了 。在节省内存空间方面,哈希表就没有压缩列表那么高效了 。
为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数 。所以,在刚才的二级编码中,我们只用图片 ID 最后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000,同时 , 我们把 hash-max-ziplist-entries 设置为 1000,这样一来,Hash 集合就可以一直使用压缩列表来节省内存空间了 。
小结在这篇文章中,我们将颠覆以往对 String 数据类型的传统认知 。以前 , String 被视为一种“万金油”,在各种场合都被广泛使用 。然而,当存储的键值对数据本身占用的内存空间较小时,String 类型的元数据开销占据了主导地位 。这些开销包括 RedisObject 结构、SDS 结构以及dictEntry 结构的内存消耗 。


推荐阅读