之前的分享内容都是相对零散的知识点,不成体系。以后的每周分享,我会尽量将每篇文章串连起来,于是我决定做一个专栏,名字就叫《学习分享》。这是该系列的第一篇。
《学习分享》每周一或周二发表,这些内容大多来自我平时学习过程中的笔记,笔记仓库在 Github:studeyang/technotes。其中我认为有深度、对工作有帮助的内容,就会以文章的形式发表在该专栏,内容会首发在我的公众号、掘金和今日头条,也会维护在 Github:studeyang/leanrning-share。
上篇文章《Redis 的 String 类型,原来这么占内存》中,我们使用 String 类型存储了图片 ID 和图片存储对象 ID,结果发现两个 Long 类型的 ID 竟然占了 68 字节内存。具体验证过程,我还是贴一下方便你回顾。
1、查看 Redis 的初始内存使用情况。
127.0.0.1:6379> info memory
# Memory
used_memory:871840
2、接着插入 10 条数据。
10.118.32.170:0> set 1101000060 3302000080
10.118.32.170:0> set 1101000061 3302000081
10.118.32.170:0> set 1101000062 3302000082
10.118.32.170:0> set 1101000063 3302000083
10.118.32.170:0> set 1101000064 3302000084
10.118.32.170:0> set 1101000065 3302000085
10.118.32.170:0> set 1101000066 3302000086
10.118.32.170:0> set 1101000067 3302000087
10.118.32.170:0> set 1101000068 3302000088
10.118.32.170:0> set 1101000069 3302000089
3、再次查看内存。
127.0.0.1:6379> info memory
# Memory
used_memory:872528
可以看到,存储 10 个图片,内存使用了 688 个字节。一个图片 ID 和图片存储对象 ID 的记录平均用了 68 字节。
这是上次我们讲述的场景。
并且还留下了一道思考题:既然 String 类型这么占内存,那么你有好的方案来节省内存吗?
今天呢,我们就来具体谈一谈。
Redis 提供了一种非常节省内存的数据结构,叫压缩列表(ziplist)。它是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。
压缩列表各个部分含义如下。
0xFF
(十进制 255),用于标记压缩列表的末端。举个例子,压缩列表 zlbytes
值为 0x50
(十进制是 80),表示该压缩列表占用 80 字节;zltail
值为 0x3c
(十进制是 60),表示如果有一个指向压缩列表起始地址的指针 p
,那么只要用指针 p
加上偏移量 60,就可以计算出表尾节点 entry3
的地址;zllen
值为 0x3
(十进制是 3),表示压缩列表有三个节点。
压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。每个 entry 的元数据包括下面几部分。
由于 ziplist 节省内存的特性,哈希键(Hash)、列表键(List)和有序集合键(Sorted Set)初始化的底层实现皆采用 ziplist。
我们先看一下能不能使用 Sorted Set 类型来进行保存。
首先,使用 Sorted Set 类型保存数据,面临的第一个问题就是:在一个键对应一个值的情况下,我们该怎么用集合类型来保存这种单值键值对呢?
我们知道 Sorted Set 的元素有 member 值和 score 值,可以把图片 ID 拆成两部分进行保存。具体做法是,把图片 ID 的前 7 位作为 Sorted Set 的 key,把图片 ID 的后 3 位作为 member 值,图片存储对象 ID 作为 score 值。
Sorted Set 中元素较少时,Redis 会使用压缩列表进行存储,可以节省内存空间。但是,在插入数据时,Sorted Set 需要按 score 值的大小进行排序,它的性能就差了。
所以,Sorted Set 类型虽然可以用来保存图片 ID 和图片存储对象 ID,但并不是最优选项。
那 List 类型呢?
List 类型对于存储图片 ID 和图片存储对象 ID 这种一对一的场景不是很适合。我们可以使用 Hash 类型。
还是用上面拆成两部分保存的方法,把图片 ID 的前 7 位 Hash 集合的 key,把图片 ID 的后 3 位作为 Hash 集合的 value。
对于数据 060,会选择对应的编码 11000000;同样,数据 3302000080 对应的编码是 11100000。
为什么对应的编码是这个?这里不是很清楚?没关系,这不影响你理解本文内容,如果你感兴趣,可以自行查看一下源码。
其中有的 entry 保存一个图片 ID 的后 3 位(4 字节),有的 entry 保存存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只需要 1 个字节就行,因为每个 entry 的前一个 entry 长度都小于 254 字节。这样一来,一个图片 ID 后 3 位所占用的内存大小是 8 字节(1+1+4+4);一个存储对象 ID 所占用的内存大小是 14 字节(1+1+4+8=14),实际分配 16 字节。
10 个图片所占用的内存就是:ziplist 4(zlbytes) + 4(zltail) + 2(zllen) + 8*10(entry) + 16*10(entry) + 1(zlend) = 251 字节。
结合全局哈希表,内存各部分占用如下:
10 个图片占 32(dictEntry) + 8(key) + 16(redisObject) + 251 = 307 字节。
这比 String 的类型的存储结果 688 节约了一倍的内存。
我们也通过下面的实战来验证一下。
127.0.0.1:6379> info memory
# Memory
used_memory:871872
127.0.0.1:6379> hset 1101000 060 3302000080 061 3302000081 ...
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:872152
实际使用了 280 字节。
不过,这里你可能会问了,图片 ID 1101000060 一定要折成 7+3,即 1101000+060 的方式吗?拆成 5+5,即 11010+00060 行不行?
答案是肯定的。
Redis Hash 类型的两种底层数据结构,一种是压缩列表,另一种是哈希表。Hash 类型设置了压缩列表保存数据的阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。
如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries
(默认 512 个),或者写入的单个元素大小超过了 hash-max-ziplist-value
(默认 64 字节),Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表。在节省内存方面,哈希表就没有压缩列表那么高效了。
为了能使用压缩列表来节省内存,我们一般要控制保存在 Hash 集合中的元素个数。所以,我们只用图片 ID 的后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000,同时,我们把 hash-max-ziplist-entries
设置为 1000,这样一来,Hash 集合就可以一直使用压缩列表来节省内存空间了。
也许你对下面文章也感兴趣。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。