redis 中的数据结构(1)|青训营

92 阅读5分钟

redis 中的数据结构(1)

sds

Redis兼容传统的C语言字符串类型,但没有直接使用C语言的传统的字符串(以’\0’结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的对象.sds的数据结构如下


typedef char *sds;  
//sds兼容传统C风格字符串,所以起了个别名叫sds,并且可以存放sdshdr结构buf成员的地址

struct sdshdr {
    int len;        //buf中已占用空间的长度
    int free;       //buf中剩余可用空间的长度
    char buf[];     //初始化sds分配的数据空间,而且是柔性数组(Flexible array member)
};

SDS本质上就是字符数组,因为有了表头sdshdr结构的存在,所以SDS比传统C字符串在某些方面更加优秀,并且能够兼容传统C字符串。同时据有了len,free结构存在,能够更加轻易的获取字符串长度(复杂度为o(1)),和防止缓存区溢出(因为SDS表头的free成员记录着buf字符数组中未使用空间的字节数,所以,在进行APPEND命令向字符串后追加字符串时,如果不够用会先进行内存扩展,在进行追加)。

sds的空间分配策略

空间预分配策略用于优化SDS的字符串增长操作。 如果对SDS进行修改后,SDS表头的len成员小于1MB,那么就会分配和len长度相同的未使用空间。free和len成员大小相等。 如果对SDS进行修改后,SDS的长度大于等于1MB,那么就会分配1MB的未使用空间。 通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数

list

列表类型(list)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的某一个片段,因为其内部实现使用双向链表(double linked list)实现的。与散列类型相同,列表类型最多能容纳2^32 -1 个元素。在redis中的链表结构被实现成为双向链表,因此,在头部和尾部进行的操作就会非常快。实现代码如下

typedef struct listNode {
    struct listNode *prev; //前驱节点,如果是list的头结点,则prev指向NULL
    struct listNode *next;//后继节点,如果是list尾部结点,则next指向NULL
    void *value;            //万能指针,能够存放任何信息
} listNode;

IMG_1.png

同时list列表还实现了,dup 、 free 和 match特定函数,具体地,dup 函数用于复制链表节点所保存的值;free 函数用于释放链表节点所保存的值;match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。 同时redis中还实现了列表迭代器,以便于链表的各种操作


typedef struct listIter {
    listNode *next;     //迭代器当前指向的节点(名字叫next有点迷惑)
    int direction;      //迭代方向,可以取以下两个值:AL_START_HEAD和AL_START_TAIL
} listIter

#define AL_START_HEAD 0 //正向迭代:从表头向表尾进行迭代
#define AL_START_TAIL 1 //反向迭代:从表尾到表头进行迭代

hash

字典又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。例如:redis中的所有key到value的映射,就是通过字典结构维护,还有hash类型的键值. redis 中hash表的定义如下


typedef struct dictht { //哈希表
    dictEntry **table;      //存放一个数组的地址,数组存放着哈希表节点dictEntry的地址。
    unsigned long size;     //哈希表table的大小,初始化大小为4
    unsigned long sizemask; //用于将哈希值映射到table的位置索引。它的值总是等于(size-1)。
    unsigned long used;     //记录哈希表已有的节点(键值对)数量。
} dictht;

rehash

哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩,其扩展策略如下,在此之前我们可以先看一下dict的结构:


typedef struct dict {
    dictType *type;     //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。
    void *privdata;     //私有数据,保存着dictType结构中函数的参数。
    dictht ht[2];       //两张哈希表。
    long rehashidx;     //rehash的标记,rehashidx==-1,表示没在进行rehash
    int iterators;      //正在迭代的迭代器数量
} dict;

redis采用渐进式rehash,Redis对哈希表的rehash操作步骤如下: 扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n 收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n 将所有的ht[0]上的节点rehash到ht[1]上。 释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。 渐进式rehash:字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。 在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。 当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。