[Redis_01]String类型的值保存到Redis中时所需的内存开销

697 阅读4分钟

需求

先跟你分享一个我曾经遇到的需求。

当时,我们要开发一个图片存储系统,要求这个系统能快速地记录图片 ID 和图片在存储系统中保存时的 ID(可以直接叫作图片存储对象 ID)。同时,还要能够根据图片 ID 快速查找到图片存储对象 ID。

因为图片数量巨大,所以我们就用 10 位数来表示图片 ID 和图片存储对象 ID,例如,图片 ID 为 1101000051,它在存储系统中对应的 ID 号是 3301000051。

photo_id: 1101000051

photo_obj_id: 3301000051

可以看到,图片 ID 和图片存储对象 ID 正好一一对应,是典型的“键 - 单值”模式。所谓的“单值”,就是指键值对中的值就是一个值,而不是一个集合,这和 String 类型提供的“一个键对应一个值的数据”的保存形式刚好契合。而且,String 类型可以保存二进制字节流,就像“万金油”一样,只要把数据转成二进制字节数组,就可以保存了。

Redis使用String数据类型来保存图片ID和图片存储对象ID的关系时,存储一亿个<photo_id:photo_obj_id>键值对消耗了6.4GB内存,平均一张图片就需要64byte的内存来存储<photo_id:photo_obj_id>键值对。但是因为我们的photo_id和photo_obj_id是10位数,每个id顶天用8byte(2^64)来保存就够了,一共也才16byte,这样算下来,多出的48byte的内存是哪里用掉的呢?

String类型键值对保存时需要多少内存?

String类型键值对保存的时候,不是直接往内存里面塞上这对键值对就行了。我们都知道,Redis有提供键过期、基于LRU算法的键删除功能,上两个功能的实现需要有键的创建时间以及键创建后被访问的次数,所以键值对还需要额外保存其他的信息,这些信息我们称之为元数据.

同时为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。一方面,当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。另一方面,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。当然,当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。

所以String类型的值在各个情况下的数据结构如下图所示,这种数据结构再redis中被称之为RedisObject:

String类型KV的底层存储结构

综上,因为我们的photo_id和photo_obj_id都是整数类型,所以加上元数据,一共需要32字节。

那多出的32字节的内存被哪里使用了呢?请问下文。

Redis会创建一个全局的dict(Hash表),保存所有的键值对。Hash表的每一个项是一个dictEntry。每个dictEntry持有三个指针,分别指向key、value和下一个dictEntry,每个指针占用8字节。一共是24个字节。又因为,Redis使用的内存分配库是jemalloc,再分配内存的时候,会根据申请的内存大小N,去分配比N大且最接近N的2的幂次数大小的内存空间。所以dictEntry一共占用了32个字节的内存。加上key、value 的两个分别16字节大小的RedisObject,一共就是64字节,如下图所示:

image-20200930210853080

如何节省内存的使用

将phote_id进行二级编码,使用Redis中的Hash类型数据结构进行保存,且保证Hash类型底层是使用zipList(压缩列表)作为数据结构,这样就能减少Redis全局dict中dictEntry的个数。缺点是查询某个key的时候,需要遍历一级编码对应的key的zipList,是一种时间换空间的方案。

Hash类型底层的数据结构有两种,分别是zipList和hashTable。如果我们往 Hash 集合中写入的元素个数超过了hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由zipList转为hashTable。所以在我们这个场景中为了节省内存开销,我们要设计好phote_id的一二级编码,并且在redis.conf中给hash-max-ziplist-entries和hash-max-ziplist-value配置好合适的值。