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

开篇

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

《学习分享》每周一或周二发表,这些内容大多来自我平时学习过程中的笔记,笔记仓库在 Github:studeyang/technotes[1]。其中我认为有深度、对工作有帮助的内容,就会以文章的形式发表在该专栏,内容会首发在我的公众号[2]今日头条[3],也会维护在 Github:studeyang/leanrning-share[4]

回顾

上篇文章《Redis 的 String 类型,原来这么占内存》中,我们使用 String 类型存储了图片 ID 和图片存储对象 ID,结果发现两个 Long 类型的 ID 竟然占了 68 字节内存。具体验证过程,我还是贴一下方便你回顾。

1、查看 Redis 的初始内存使用情况。

127.0.0.1:6379> info memory# Memoryused_memory:871840

2、接着插入 10 条数据。

10.118.32.170:0> set 1101000060 330200008010.118.32.170:0> set 1101000061 330200008110.118.32.170:0> set 1101000062 330200008210.118.32.170:0> set 1101000063 330200008310.118.32.170:0> set 1101000064 330200008410.118.32.170:0> set 1101000065 330200008510.118.32.170:0> set 1101000066 330200008610.118.32.170:0> set 1101000067 330200008710.118.32.170:0> set 1101000068 330200008810.118.32.170:0> set 1101000069 3302000089

3、再次查看内存。

127.0.0.1:6379> info memory# Memoryused_memory:872528

可以看到,存储 10 个图片,内存使用了 688 个字节。一个图片 ID 和图片存储对象 ID 的记录平均用了 68 字节。

这是上次我们讲述的场景。

并且还留下了一道思考题:既然 String 类型这么占内存,那么你有好的方案来节省内存吗?

今天呢,我们就来具体谈一谈。

用什么数据结构可以节省内存?

Redis 提供了一种非常节省内存的数据结构,叫压缩列表(ziplist)。它是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。

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

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

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

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

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

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

学习分享(第1期)之Redis:巧用Hash类型节省内存
  • prevlen,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。如果上一个 entry 的长度小于 254 字节,取值 1 字节;否则,就取值为 5 字节;

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

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

  • data:保存实际数据。

由于 ziplist 节省内存的特性,哈希键(Hash)、列表键(List)和有序集合键(Sorted Set)初始化的底层实现皆采用 ziplist。

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

我们先看一下能不能使用 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。

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

对于数据 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 字节。

结合全局哈希表,内存各部分占用如下:

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

10 个图片占 32(dictEntry) + 8(key) + 16(redisObject) + 251 = 307 字节。

这比 String 的类型的存储结果 688 节约了一倍的内存。

我们也通过下面的实战来验证一下。

127.0.0.1:6379> info memory# Memoryused_memory:871872127.0.0.1:6379> hset 1101000 060 3302000080 061 3302000081 ...(integer) 1127.0.0.1:6379> info memory# Memoryused_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 集合就可以一直使用压缩列表来节省内存空间了。

参考资料

  • 文中的一些命令,参考菜鸟教程:https://www.runoob.com/redis/redis-tutorial.html
  • 极客时间《Redis 核心技术与实战》
  • 书籍《Redis 设计与实现》
  • 压缩列表:https://redisbook.readthedocs.io/en/latest/compress-datastruct/ziplist.html
  • 哈希表:http://redisbook.com/preview/object/hash.html

[1]

studeyang/technotes: https://github.com/studeyang/technotes

[2]

公众号: https://mp.weixin.qq.com/s/TWRVaQPhrQf9oPxsAsuIKQ

[3]

今日头条: https://www.toutiao.com/c/user/token/MS4wLjABAAAArFlpgpSvRI74ttxw76bAENUnFIFcYTJQnZYS77fZmNQ/?source=mp_msg&tab=article

[4]

studeyang/leanrning-share: https://github.com/studeyang/learning-share

相关文章

也许你对下面文章也感兴趣。

原文始发于微信公众号(杨同学technotes):学习分享(第1期)之Redis:巧用Hash类型节省内存

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98183.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!