Redis底层数据结构

SDS

SDS全称是Simple Dynamic String,具有如下显著的特点:

  1. 常数复杂度获取字符串长度:C语言获取一个字符串的长度需要遍历整个字符串时间复杂度为O(N),而SDS在属性len中记录了字符串长度,获取字符串长度的时间复杂度为O(1)。
  2. 杜绝缓冲区溢出:C字符串在执行拼接字符串时,如果长度不够会产生缓冲区溢出的问题,而SDS在拼接字符串时,会先检查空间是否足够,如果不满足会将空间自动扩展至执行修改所需的大小,然后再执行操作。
  3. 减少修改字符串时带来的内存重分配次数:C字符串的长度和底层数组的长度之间存在着关联性,每次增加或缩小一个C字符串,程序都需要对C字符串的数据进行内存重分配操作,为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组间的关系,通过未使用空间SDS实现了空间预分配惰性空间释放两种优化策略。
    1. 空间预分配:SDS内部为当前字符串实际分配的空间,一般要高于实际字符串的长度,当字符串的长度小于1M时,扩容都是扩一倍,如果超过1M,扩容是最多扩1M的空间,字符串的最大长度是512M。
    2. 惰性空间释放:用于优化SDS字符串的缩短操作,当需要缩短SDS的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录下来,并等待将来使用。
  4. 二进制安全:SDS的API都是二进制安全的,都会以处理二进制的方式处理SDS存放在buf数组里的数据,写入什么样,读取就是什么样,redis不是用buf属性来保存字符,而是保存一系列的二进制数据。
  5. 兼容部分C字符串函数。

dict

dict是一个基于哈希表的数据结构,在不要求数据有序存储,且能保持较低的哈希值冲突概率的前提下,查询的时间复杂度接近O(1)。它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。Redis的dict实现最显著的一个特点,就在于它的重哈希。它采用了一种称为渐进式哈希的方法,在需要扩展内存时避免一次性对所有key进行重哈希,而是将重哈希操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。dict之所以这样设计,是为了避免重哈希期间单个请求的响应时间剧烈增加,这与前面提到的“快速响应时间”的设计原则是相符的。

当装载因子大于 1 的时候,Redis 会触发扩容,将hash扩大为原来大小的 2 倍左右;当装载因子小于 0.1 的时候,Redis 就会触发缩容,hash缩小为原来的一半左右

ziplist

ziplist的特点如下:

  1. ziplist使用一整块连续的内存,将表中每一项存放在前后连续的地址空间内,类似于一个数组。Ziplist内的集合元素按score从小到大排序,score较小的排在表头位置。
  2. ziplist对于值的存储采用了变长的编码方式,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。

quicklist

quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,比如,一个包含3个节点的quicklist,如果每个节点的ziplist又包含4个数据项,那么对外表现上,这个list就总共包含12个数据项

quicklist的结构为什么这样设计呢?总结起来,大概又是一个空间和时间的折中

  1. 双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片
  2. ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。

不过,这也带来了一个新问题:到底一个quicklist节点包含多长的ziplist合适呢?比如,同样是存储12个数据项,既可以是一个quicklist包含3个节点,而每个节点的ziplist又包含4个数据项,也可以是一个quicklist包含6个节点,而每个节点的ziplist又包含2个数据项。

这又是一个需要找平衡点的难题。我们只从存储效率上分析一下:

  1. 每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
  2. 每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。

可见,一个quicklist节点上的ziplist要保持一个合理的长度。那到底多长合理呢?这可能取决于具体应用场景。实际上,Redis提供了一个配置参数list-max-ziplist-size,就是为了让使用者可以来根据自己的情况进行调整。

list-max-ziplist-size -2

这个参数可以取正值,也可以取负值。

当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。

当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每个值含义如下:

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

intset

intset是一个由整数组成的有序集合,从而便于进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与ziplist有些类似,是连续的一整块内存空间。

intset的定义如下:

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
 
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

各个字段含义如下:

  • encoding: 数据编码,表示intset中的每个数据元素用几个字节来存储。它有三种可能的取值:INTSET_ENC_INT16表示每个元素用2个字节存储,INTSET_ENC_INT32表示每个元素用4个字节存储,INTSET_ENC_INT64表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。
  • length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。
  • contents: 是一个柔性数组(flexible array member),表示intset的header后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding *length。柔性数组在Redis的很多数据结构的定义中都出现过(例如sds, quicklist, skiplist),用于表达一个偏移量。
  • contents需要单独为其分配空间,这部分内存不包含在intset结构当中。

其中需要注意的是,intset可能会随着数据的添加而改变它的数据编码:

  1. 最开始,新创建的intset使用占内存最小的INTSET_ENC_INT16(值为2)作为数据编码。
  2. 每添加一个新元素,则根据元素大小决定是否对数据编码进行升级。

intset与ziplist的比较:

  1. ziplist可以存储任意二进制串,而intset只能存储整数。
  2. ziplist可以对每个数据项进行不同的变长编码(每个数据项前面都有数据长度字段len),而intset只能整体使用一个统一的编码(encoding)。

skiplist

跳表是一种可以进行二分查找的有序链表,采用空间换时间的设计思路,跳表在原有的有序链表上面增加了多级索引(例如每两个节点就提取一个节点到上一级),通过索引来实现快速查找。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都为O(logn),空间复杂度为 O(n)。跳表非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。

跳表具有以下两个特点:

  1. 跳表的删除操作除了要删除原始链表中的节点,还要删除索引中的节点。
  2. 插入元素后,索引的动态更新,不停的往跳表里面插入数据,如果不更新索引,就有可能出现某两个索引节点之间的数据非常多的情况,甚至退化成单链表。针对这种情况,我们在添加元素的时候,通过一个随机函数,同时选择将这个数据插入到部分索引层。比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第K级的索引中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值