Redis核心技术及实战(十.String类型开销大?如何替代?)

十:String类型内存开销大?如何替代?

原文:《11丨“万金油”的String,为什么不好用了?》

例子:存储一个10位数的图片ID和其对应的10位数的图片存储ID,一共存储1亿个这样一个单键值对,我们选用String类型存储时,发现居然用了6.4GB的内存,也就是说平均一个键值对就用了64字节内存,但是,10位数可以用一个8字节的Long型表示,那么一个键值对也就16字节,多出来的48字节去哪了呢? => **除了记录实际数据,String还需要额外的内存空间记录数据长度、空间使用情况等元数据,当实际数据比较小时,元数据的占比就相当大了。**下面我们看看String类型的实际内存使用情况。

1. String类型的内存使用情况

保存的时64位有符号整数时,String类型会把它保存位一个8字节的Long类型整数(int编码方式);但是当保存的数据中含有字符时,就会用简单动态字符串(Simple Dynamic String, SDS)结构来保存,它有三部分:

  • buf:字节数组,保存实际数据,在数据的最后,会自动加一个“\0”,代表数组结束,占用1字节的开销;
  • len:buf的已用长度,4个字节
  • alloc:buf的实际分配长度,4个字节

除了SDS外,String类型还有一个来自RedisObject结构体的开销。因为Redis的数据类型有很多,而且不同的数据类型都有一些相同的元数据要记录(比如最后一次访问时间、引用次数等),RedisObject的基本结构如下:

  • 元数据:用来记录各个数据类型的一些信息,占8个字节
  • 指针:指向存有String类型数据的SDS,占8个字节

为了节省内存空间,Redis还对Long型和SDS的内存布局做了专门的设计

  • 保存Long型数据时,RedisObject的指针就直接赋值为整数数据了;
  • 保存字符串数据时
    • 如果字符串小于44字节,RedisObject中的元数据、指针和SDS为一块连续的区域,可以避免内存碎片;
    • 如果字符串大于44字节,SDS和RedisObject就不再连续。

image-20220512212342793

至此,我们已经了解了String类型的内存使用情况了,但是2个10位的ID,也就用了(8+8)*2=32字节,剩下的32字节呢? => 第二节中讲过,Redis会用一个全局哈希表来保存所有键值对,哈希表的每一项都是一个dictEntry结构体,该结构体有3个8字节的指针,分别指向key、value以及链表的下一个dictEntry,所以,一个键值对在这里会使用24字节的内存。但是,jemalloc在分配内存的时候,如果我们申请n字节的内存,它会给我们分配大于n且最接近n的2次幂字节数作为分配的空间,所以24字节就会分配32字节。

至此,我们就知道了为什么用String类型来保存这两个ID会占用64字节了。

**小结:当使用String类型来保存小数据量的键值对时,是非常浪费空间的!**那么,有没有更加节省内存的方法呢?

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

压缩列表ziplist。

image-20220512214431760

它是用一系列连续的entry保存数据,每个entry包含下面四部分:

  • prev_len:前一个entry的长度,有两种取值情况,如果前一个entry的长度小于等于254字节时,prev_len占用1字节,否则5字节;
  • encoding:编码方式,1字节;
  • len:当前entry长度,4字节;
  • content:实际数据。

如果一个entry保存一个ID(8字节),那么这个entry就是1+1+4+8=14字节,分配16字节。

Redis基于压缩列表实现的List、Hash、Sorted Set这样的集合类型,最大的好处就是,节省了全局哈希表的dictEntry的开销!当用String类型时,每一个键值对就有一个32字节的dictEntry,但是用集合类型时,一个key就对应一个集合的数据,也就是说仅用一个dictEntry就能保存很多数据,大大节省了内存。

但是,集合类型,一个键对应一个集合,而我们的需求如果是单键值对(一个图片ID对应一个图片存储ID)时,该怎么用集合类型呢?

3. 用集合类型保存单键值对

首先,因为数据是键值对,我们使用Hash类型,由第二节可知,Hash类型底层有两种数据结构:哈希表和压缩列表,在节省内存方面,哈希表没有压缩列表那么高效,所以,我们要使用压缩列表,那么,Hash类型什么时候会使用压缩列表呢?其实,Hash类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,就会转用哈希表存储数据

  • hash-max-ziplist-entries:使用压缩列表存储的最大元素个数,Hash集合中的元素个数超过该值,就转为哈希表结构
  • hash-max-ziplist-value:使用压缩列表存储的单个元素的最大长度,集合中任一元素大小超过了该值,就转为哈希表结构

为了能充分利用压缩列表的内存性能,我们要控制保存在Hash集合中元素的个数。比如,我们把hash-max-ziplist-entries设置为1000(也就是说一个哈希集合最多只能存储1000个元素才能保证使用的是压缩列表)。

设计:

采用二级编码方式,把一个单值的键值对数据拆分成两部分,前一部分作为Hash类型的key,后一部分作为Hash类型的value,以图片ID 1101000060 和图片存储ID 3302000080为例,我们把图片ID的前7位(1101000)作为Hash类型的key,图片ID的最后3位(060)和图片存储ID分别作为Hash集合中的key和value。存储后发现,增加一条记录,内存占用仅增长了16字节!极大节省了空间!

image-20220512223234507

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值