再谈Redis的数据类型String
1.String不是万能的
在我们日常使用Redis的过程中,String这个数据是最常用的,甚至我们会费尽心思的把一些比较别扭的数据,强制转换成String来存储在Redis中。为什么这么做呢?其实我觉得就是因为我们操作String简单、明了,还有就是,对Redis中的String不了解,只会用,别人这么用,我也这么用,反正也没见到出什么问题。那么我们下面考虑一个问题:假如有上亿个整数型的键值对需要存储,如(1111111111,2222222222)等等,我们应该怎么存储呢?可能不加思索,就直接是把1111111111作为key,把2222222222作为value来存储 ,即set 1111111111 2222222222 。这似乎没有问题,但是随着我们数据量的急剧上升,我们会发现,Redis内存占用也会变得很大。按理说,上面的这一个键值对占用的内存为(8+8)=16字节(Byte),可实际上它却占用了64个字节(Byte),直接扩大了3倍(48个字节)。天哪,我存了这么点数据,竟然占用了这么多的内存空间,真是不看不知道,一看吓一跳。那么令人奇怪的是,其他那48个字节被谁占用了呢?客官,我们往下继续谈。
2.简单动态字符串(Simple Dynamic String—SDS)
通过前面的几篇博客我们知道,Redis底层存储字符串是采用的一种叫做简单动态字符串的数据结构,以下我们简称为SDS。
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
从结构体中我们可以看到,除了真实存储数据的buf外,还有两个元素len和free,由于每个都是int类型,所以占用了(4+4)=8个字节。一般的,我们把String类型中记录数据长度等这些信息的数据,称为元数据。所以当保存的数据比较小的时候,元数据的开销占比就比较大了,有点主次不分的感觉。
除了SDS中元数据造成的内存开销,RedisObject的结构体也会有额外的开销。一个RedisObject包含一个8个字节元数据和一个8个字节的指针,以String类型为例:
nter)
但是幸运的是,Redis对整数和SDS内存布局做了特殊的处理,主要体现在以下三点;
- 当保存的是整数类型时,RedisObject中的指针就直接赋值为整数数据了,这样就不需要额外的指针指向具体的整数值了,减少了内存开销;
- 如果保存的是字符串,当字符串小于等于44个字节(Byte)时,RedisObject中的元数据、指针、和SDS都存储在同一块连续的内存上,这样做的好处是在数据量小的时候,避免内存碎片的产生,这种方式也被称为embstr编码方式;
- 如果保存的是字符串,当字符串大小44个字节(Byte)时,SDS的数据量就大了,RedisObject会给SDSf分配独立的空间,并用指针指向SDS结构体,这种方式被称为raw编码方式。
4.
根据上面我们分析,我们计算一下第一部分说的那个键值对(1111111111,222222222)占用多少空间。因为是int编码,所以RedisObject中的元数据占用8Byte,每个整数占用8Byte,所以每个RedisObject一共占用(8+8)=16Byte的内存空间,又由于key和value共两个RedisObject结构体,所以共占用了162=32Byte的内存空间。到了这里,好像离我们最初得出的64Byte的内存空间还差32Byte,那32Byte去哪里了呢?
我们知道,Redis是使用一个全局哈希表来保存所有的键值对,哈希表的每一个每一项都是一个dictEntry(字典项),用来指向键值对。dictEntry共有3个8字节(Byte)的指针,分别指向key、value、以及下一个dictEntry,即dictEntry共有38=24Byte。
dictEntry的三个指针实际占了24Byte,但为什么却是占用了32个Byte呢?原来是因为Redis使用了内存分配库jemalloc,它在分配内存的时候,会根据我们申请的字节数M,找一个比M大并且最接近M的2的幂次数作为分配空间,这样减少频繁分配的次数。
如果你申请 10 字节空间,jemalloc 实际会分配 816字节空间;如果你申请 24 字节空间,jemalloc 则会分配 32 字节。所以,dictEntry 结构就占用了 32 字节。
从上面的分析,我我们看到,键值对(1111111111,222222222)明明仅需要16 Byte的空间就足够了,但是作为String类型时,却需要使用64Byte的内存空间,其中48个Byte都没有真正用来保存实际的数据,额外的内存空间开销就很大了。
3.小结
从上面的分析我们看到,String这个数据是最常用的数据类型,却产生了极大的空间浪费,有些时候我们可以考虑一下使用其他类型的数据来处理,至于用什么类型来替换,我们可以根据不同的场景来,具体就不再进行阐述了。