学习分享(第 1 期)之 Redis:巧用 Hash 类型节省内存

2023/2/8 4:24:07

本文主要是介绍学习分享(第 1 期)之 Redis:巧用 Hash 类型节省内存,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

开篇

之前的分享内容都是相对零散的知识点,不成体系。以后的每周分享,我会尽量将每篇文章串连起来,于是我决定做一个专栏,名字就叫《学习分享》。这是该系列的第一篇。

《学习分享》每周一或周二发表,这些内容大多来自我平时学习过程中的笔记,笔记仓库在 Github:studeyang/technotes。

回顾

上篇文章《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)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。

压缩列表各个部分含义如下。

  • zlbytes:表示压缩列表占用的内存字节数。
  • zltail:表示压缩列表表尾节点距离起始地址有多少字节。
  • zllen:表示压缩列表包含的节点数量。
  • entry:压缩列表的各个节点。
  • zlend:特殊值 0xFF (十进制 255),用于标记压缩列表的末端。

举个例子,压缩列表 zlbytes 值为 0x50 (十进制是 80),表示该压缩列表占用 80 字节;zltail 值为 0x3c (十进制是 60),表示如果有一个指向压缩列表起始地址的指针 p,那么只要用指针 p 加上偏移量 60,就可以计算出表尾节点 entry3 的地址;zllen 值为 0x3 (十进制是 3),表示压缩列表有三个节点。

压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。每个 entry 的元数据包括下面几部分。

  • prevlen,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。如果上一个 entry 的长度小于 254 字节,取值 1 字节;否则,就取值为 5 字节;

  • encoding:表示编码方式,1 字节;

  • len:表示自身长度,4 字节;

  • data:保存实际数据。

由于 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 类型。

使用 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 行不行?

一定要 7+3 的方式存储 key 吗?

答案是肯定的。

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 集合就可以一直使用压缩列表来节省内存空间了。



这篇关于学习分享(第 1 期)之 Redis:巧用 Hash 类型节省内存的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程