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

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: 本文主要会介绍压缩列表的底层结构。

开篇

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

《学习分享》每周一或周二发表,这些内容大多来自我平时学习过程中的笔记,笔记仓库在 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)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。

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

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

参考资料

相关文章

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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5天前
|
存储 NoSQL Redis
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
13 1
|
5天前
|
NoSQL Java Redis
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
14 0
|
5天前
|
存储 消息中间件 缓存
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
12 0
|
5天前
|
存储 NoSQL 安全
Redis系列学习文章分享---第十五篇(Redis最佳实践--设计优雅的key+合适的数据结构+持久化如何配置+慢查询问题解决)
Redis系列学习文章分享---第十五篇(Redis最佳实践--设计优雅的key+合适的数据结构+持久化如何配置+慢查询问题解决)
13 1
|
5天前
|
缓存 负载均衡 NoSQL
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
14 1
|
5天前
|
存储 缓存 NoSQL
Redis系列学习文章分享---第十三篇(Redis多级缓存--JVM进程缓存+Lua语法)
Redis系列学习文章分享---第十三篇(Redis多级缓存--JVM进程缓存+Lua语法)
16 1
|
存储 NoSQL Redis
Redis命令——哈希(Hash)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。 Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
1471 0
|
存储 NoSQL Redis
redis必杀命令:哈希(Hash)
题记: Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。 Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
1027 0
|
16天前
|
存储 运维 NoSQL
Redis Cluster集群模式部署
Redis Cluster集群模式部署
42 4
|
19天前
|
监控 NoSQL 算法
手把手教你如何搭建redis集群(二)
手把手教你如何搭建redis集群(二)
36 1