redis

redis(七:数据结构-String)

2022-11-24  本文已影响0人  supremecsp

String 类型可以保存二进制字节流,就像“万金油”一样,只要把数据转成二进制字节数组,就可以保存了。但String 类型并不是适用于所有场合的,它有一个明显的短板,就是它保存数据时所消耗的内存空间较多。

一组图片 ID 及其存储对象 ID 的记录,只需要 16 字节就可以了。但是存为String 类型却要64 字节,为什么 String 类型内存开销大?
其实,除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。当实际保存的数据较小时,元数据的空间开销就显得比较大了,有点“喧宾夺主”的意思。

SDS
当保存的数据中包含字符时,String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存,如下图所示:


buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
len:占 4 个字节,表示 buf 的已用长度。
alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。
在 SDS 中,buf 保存实际数据,而 len 和 alloc 本身其实是 SDS 结构体的额外开销。

RedisObject
除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销。
因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。


一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的实际数据所在
为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。一方面,当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。

编码
1,当保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式。
2,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。
3,当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。

dictEntry
Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。
dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节

jemalloc
jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,使得内存对齐,减少内存碎片化。
如果你申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果你申请 24 字节空间,jemalloc 则会分配 32 字节。所以,在我们刚刚说的场景里,dictEntry 结构就占用了 32 字节。

现在可以回答16 字节的数据,存为String类型需要64字节了:ID是Long 类型整数,可以直接用 int 编码的 RedisObject 保存RedisObject 元数据部分占 8 字节,指针部分被直接赋值也为 8 字节;dictEntry三个指针24字节,由于jemalloc会申请32字节。所以为 16(key) + 16 (value) + 32 (dictEntry)的空间存了分别为8字节的key和value。

为什么dictEntry要有个next指针,有什么意义,又怎么样才能通过key找到该dictEntry?


不同的key做完哈希后通过dictiht找到dictEntry链表再遍历找到dictEntry。
redis中dictiht的数据结构(上图标黄的部分)的字段table指向了一个dictEntry数组(数组的初始大小为4),而dictEntry就会按照一定的规则被存放在table中。不同的key做完哈希后,有可能出现哈希值相同的情况,因此可能出现冲突的情况。redis的解决方式是,将哈希值相同的key所在的dictEntry用指针的方式链接起来。

看起来dictht已经实现了存储key-value的所有功能,那为什么还需要dict呢?
dict的出现,主要是为了解决dictiht扩容的问题,当dictiht需要扩容时,会先创建一个2倍大小的新的dictiht,然后逐渐将数据迁移过来,迁移完成后将老的dictiht释放。即为rehash了。而前面讲的hash桶就是dictht。rehash渐进式的将dictiht[0]的数据迁移到dictiht[1]。再释放dictht[0]。redis数据结构-dict

上一篇下一篇

猜你喜欢

热点阅读