Redis底层数据结构

字符串(简单动态字符串 simple dynamic string)

SDS 数据结构
每个 sds.h/sdshdr 结构表示一个 SDS 值:

struct sdshdr {
    // 记录 buf 数组中已使用字节的数量
    // 等于 SDS 所保存字符串的长度
    int len;
    // 记录 buf 数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};

SDS与C字符串的区别

  1. 常数复杂度获取字符串长度

    因为 C 字符串并不记录自身的长度信息, 所以为了获取一个 C 字符串的长度, 程序必须遍历整个字符串, 对遇到的每个字符进行计数, 直到遇到代表字符串结尾的空字符为止, 这个操作的复杂度为 O(N)
    和 C 字符串不同, 因为 SDS 在 len 属性中记录了 SDS 本身的长度, 所以获取一个 SDS 长度的复杂度仅为O(1)

  2. 杜绝缓冲区溢出

    因为C字符串不记录本身长度,所以进行字符串拼接的时候若没有分配足够的内存空间,新的字符串会溢出到其他内存空间,导致其他内容被修改;而当SDS API对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需大小,然后再执行修改操作。

  3. 二进制安全

    C字符串除了字符串末尾有空格外,字符串中不能出现空字符串,否者会被程序任务是字符串结尾 – 这些限制了C字符串只能保存文本数据,不能保存图片,视频,音频,压缩文件这样的二进制数据;所有SDS API 都是以处理二进制的方式来处理SDS存放字buff数组中的数据,不会做限制和过滤

· SDS 空间预分配

优化SDS字符串增长的操作,避免频繁的内存分配。程序不仅会分配修改必要的空间外,还会为SDS分配额外的未使用的空间。
分配公式:小于1M 程序会分配和len属性同样大小的未使用空间,这是len属性的值和free相相同;大于1M 程序会分配1M的未使用空间。

· 惰性空间释放
优化SDS字符串缩短的操作。当SDS API 需要缩短SDS保存的字符串时,程序不会立即使用内存分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。(与此同时,SDS也提供了API来真正释放SDS里面未使用的空间,不会造成内存浪费)

链表

  • 链表被广泛用于实现 Redis 的各种功能, 比如列表键, 发布与订阅, 慢查询, 监视器, 等等。
  • 每个链表节点由一个 listNode 结构来表示, 每个节点都有一个指向前置节点和后置节点的指针, 所以 Redis
    的链表实现是双端链表。
  • 每个链表使用一个 list 结构来表示, 这个结构带有表头节点指针、表尾节点指针、以及链表长度等信息。
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向 NULL , 所以 Redis 的链表实现是无环链表。
  • 通过为链表设置不同的类型特定函数, Redis 的链表可以用于保存各种不同类型的值。

字典

字典是哈希键的底层实现之一,当一个哈希键包含的键值对比较多的时候,又或者键值对中的元素都是比较长的字符串时 ,redis就会使用字典作为哈希键的底层实现。

哈希

使用MurmurHash2算法
rehash
rehashid:它记录了rehash当前的进度,如果目前没有进行rehash,值为-1。
什么时候进行rehash?(负载因子 = 哈希表已保存节点数量 ÷ 哈希表大小) (load_factor = ht[0].used / ht[0].size)

  • 服务器目前没有执行BGSAVE(background save)或BGREWRITEAOF命令时,并且哈希表的负载因子大于等于1;

  • 服务器目前正在执行BGSAVE或BGREWRITEAOF命令时,并且负载因子大于等于5;

当redis进行BGSAVE或BGREWRITEAOF命令的过程中,redis需要创建当前服务器进程的子进程,大多数操作系统都是采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程(BGSAVE或BGREWRITEAOF)存在期间,服务器会提高执行扩展操作所需的负载因子,避免在子进程(BGSAVE或BGREWRITEAOF)存在的期间进行哈希表的扩展操作,这可以避免不必要的内存写入操作,最大程度的节约内存。

如何rehash?

  1. 为字典的ht[1]哈希表分配空间,这个表的空间大小取决于执行的操作(扩展/收缩),以及ht[0]包含的键值对数量(ht[0].uesd)

    扩展操作,ht[1]的大小为:第一个大于等于ht[0].uesd * 2的 2的n次方;
    收缩操作,ht[1]的大小为:第一个大于等于ht[0].uesd 的2的n次方;

  2. 将ht[0]上所有的键值对rehash到ht[1]上。这里的rehash指的是重新计算键的哈希值和索引值,然后将键值对放置在ht[1]指定的位置上。
  3. 当ht[0]上所有的键值对都迁移到ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]上新建一个空白哈希表,为下次rehash做准备。
    渐进式rehash
    rehash动作并不是一次集中完成的,而是分多次、渐进式完成的。rehash期间字典的urd操作会在ht[0] ht[1]两个表上同时进行(添加操作会在ht[1])。

跳跃表(skiplist)

有序的数据结构,通过在每个节点维持多个指向其他节点的指针,达到快速查找的目的。
查找的时间复杂度O(logN) 最坏O(N)。
大部分情况下,跳跃表的效率可以和平衡树相媲美,而且实现比平衡树简单。
redis使用跳跃表最为有序集合的底层实现之一:
● 如果一个有序集合包含的元素比较多
● 有序集合中元素的成员是比较长的字符串时。
·redis 的跳跃表由zskiplist和zskipnode两个结构实现,zskiplist保存跳跃表信息(比如:表头节点,表尾节点,长度);
·每个跳跃表的层高都是1到32之间的;
·再同一个跳跃表中,多个节点可以包含相同的分值,但每个几点的对象必须唯一;
·跳跃表的节点按分值大小进行排序,当分值相同时,节点按照成员对象大小进行排序;

整数集合

整数集合(intset)是集合键的底层实现之一:当一个集合只包含整数元素,并且这个集合的元素数量不多的时候。
升级
每当我们将一个新的新元素添加到整数集合里面时,并且新元素的类型比现有整数集合所有元素的类型要长的时候
升级需要三步:
1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并且为新元素分配空间;
2. 将底层数组的所有元素都转换成与新元素相同的类型,将转换后的元素放置到正确的位置上,而且放置的过程中,需要继续维持底层数组的有序性质不变;
3. 将新元素添加到底层数组里面。
升级的好处:

  1. 提高灵活性

    因为c语言是静态类型,为了避免类型错误,我们通常不会将不同的类型值放在同一个结构中。但是因为整数集合可以自动升级底层数组来适应新元素,不用担心类型错误

  2. 节约内存

当然,如果让一个数组同时保存int16、int32、int64,简单的做法就是直接使用int64最为底层数组的实现。

参考文献:《redis设计与实现》

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页