redis学习(十) 数据结构

字符串结构

struct sdshdr{
    int len;
    int free;
    char buf[];
}

简单字符串结构中,buf存储的字符数组也是使用'\0'作为字符数组的结尾,但是在使用上对用户是透明的,这个设计能重用C的字符数组函数。

另一个好处是,redis使用了一个常数len记录字符串的长度,在函数自动更新,不像C那样查询一个字符数组长度需要遍历字符数组,strlen命令获取字符串长度的时间复杂度有N降低到了1.

C字符串不记录自身长度带来的另一个问题就是容易造成缓冲区溢出,即在C语言的语境中合并字符串,是假设字符数组已经有足够的空间,容纳另一个字符数组,但实际上C并不能保证这点。而redis的字符串结构,会在进行相关字符串操作前,使用free检查空间是否足够,不够那么扩容。

减少修改字符串时带来的内存重新分配,即字符串数组不够时,重新分配内存空间,而SDS结构中,可以进行预分配,使得free>len,buf的实际长度不是等于len,而是free+len的长度。

而SDS不会主动删除多余的字符,而是会让free记录不需要的字符长度,这是惰性删除。

二进制安全,C字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符串,会被程序误认为是字符串结尾。这些限制使得C字符串只能保存文本数据。

SDS API都是二进制安全的,因为SDS是使用len判断字符串结尾,而不是使用'\0'字符。

链表

每个链表使用一个adlist.h/listNode结构体表示

typedef struct listNode{
    struct listNode *prev;
    struct listNode *next;
    void *value; //节点的值
}

typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned long len;
    void *(*dup)(void *ptr); //节点值复制函数
    
    void (*free)(void *ptr); //节点值释放函数
    
    int (*match)(void *ptr,void *key); //节点值对比函数
    
}list;

redis的链表有以下特性:

每个节点有双端节点

无环,head的前端节点指向null,tail的后端节点指向null。这点和常规构成环的链表不同。

可以记录链表的长度,len

多态,拥有三个指向不同类型的函数。

字典

redis的字典使用哈希作为底层实现

typedef struct dictht{
    dictEntry **table; //哈希表数组,数组中的每一个元素都是一个指向dictEntry结构的指针
    unsigned long size; //哈希表大小
    unsigned long sizemask; //哈希表大小掩码,用于计算索引值,总是等于size-1
    unsigned long used; //该哈希表已有节点的数量
}dictht;

C里面没有二维数组,所以用指针的指针实现二维数组。

typedef struct dictEntry{
    void *key; //键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    }v; //值,可以是一个指针,也可以是uint64_t整数,int64_t整数
    struct dictEntry *next;//指向下一个哈希表节点,形成链表 ,用于解决哈希冲突的问题
}dictEntry;

图片示例:

redis1

typedef struct dict{
    //特定类型函数
    dictType *type;
    void *private;//私有数据
    dictht ht[2];//哈希表
    int trehashidx; //rehash所以,当rehash不进行时,值为-1
}dict ;

typedef struct dictType{
    //计算哈希值的函数
    unsigned int (*hashFunction)(const void* key);
    //复制键函数
    void* (*keyDup)(void *privdata,const void* key);
    //复制值函数
    void* (*valDup)(void *privdata,const void* obj);
    //比较键函数
    void (*keyCompare)(void *privdata,const void *key1,const void *key2);
    //销毁键或值函数
    void (*keyDestructor)(void *privdata,const void *key);
    void (*valDestructor)(void *privdata,const void *obj);
}

ht属性是一个包含两项的数组,数组中的每一个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只进行rehash(再哈希)时使用。

题外话,在C中函数名可以说是一个指针

比如int fun(int x, int y)int *func(int x,int y)是一样的,他们的调用方式是等价的,func(2,3)=*func(2,3)

在 C 语言中,void 被翻译为"无类型",相应的void *"无类型指针"

一个没有进行 再哈希的字典

redis2

哈希算法:

redis计算哈希值和索引值的方法如下:

hash=dict->type->hashFunction(key)

index=hash&dict->ht[x].sizemask //使用哈希表的sizemask属性和哈希值,计算索引值

与sizemask进行逻辑且运算是让最后得出的索引值不会超过哈希表的大小。

redis采用的是Murmurhash算法

redis解决键冲突,即键被分配在同一个索引上,方法是采用链地址法,相同索引值的键值对将联成一条链表。

rehash

随着哈希表保存的键值对增加或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要的哈希表进行相应的扩张或者收缩。

步骤大致为三步:

第一步:为ht[1]分配空间

第二步:将ht[0]的键值对迁移到ht[1]中

第三步:将ht[1]改为ht[0],ht[0]改为ht[1]

如果执行的操作是扩展,那么ht[1]的大小为 第一个大于等于 ht[0].used*2=2^n。

如果执行的操作是收缩,那么ht[1]的大小为 第一个大于等于ht[0].used=2^n。

负载因子=ht[0].used/ht[0].size

渐进式rehash

为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]的键值对全部rehash到ht[1]中,而是分多次,渐进式地将ht[0]的键值对慢慢地rehash到ht[1]。

步骤1,为ht[1]分配空间,让字典同时持有ht[0],ht[1]两个哈希表

步骤2 在字典中维持一个索引计数器变量rehashidx,设置值0,表示再哈希正式开始

步骤3,在再哈希期间,每次对字典执行添加,删除,查询,更新操作是,程序除执行指定的操作外,还顺带将ht[0]哈希表在rehashidx索引上的索引键值对Rehash到ht[1]。当rehash工作完成后,程序将rehash属性的值增一。

步骤4,某个时间点,rehashidx值等于最开始的th[0].used,那么代表再哈希工作完成,rehashidx设置值为-1。

步骤5,释放th[0],th[1]代替工作。

在渐进式哈希期间,字典同时使用两个哈希表,如果ht[0]没有找到相应的键值对,去ht[1]上查找。

ht[0]迁移的键值对会空间被释放,新增的键值对在ht[1]

跳跃表

是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。

跳跃表支持平均 O(logN) ,最坏O(N)的复杂度的节点查询,还可以通过顺序性操作来批处理节点。

跳跃表的效率可以和平衡树媲美,而且实现更加简单。

redis采用跳跃表作为有序集合的底层实现之一。

redis在另一个地方用到跳跃表,实在集群节点中用作内部数据结构。

typedef struct zskiplistNode{
    struct zskiplistLevel{
        struct zskiplistNode *forward; //前进指针
        
        unsigned int span; //跨度
    }level[];
    
    struct zskiplistNode *backward;//后退指针
    
    double score;
    
    robj* obj;
}zskiplistNode;


跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层快速访问其他节点。

跨度

层的跨度属性用于记录两个节点之间的距离,指向NULL节点的前进指针跨度都是0.

遍历操作只用到前进指针,跨度是用来计算排位的。

ss

typedef struct zskiplist{
    struct skiplistNode *header,*tail;
    unsigned long length;
    int level;
}zskiplist;

ws

头节点并不包括在层级和长度里面。

整数集合

整数集合是redis用于保存整数值的集合抽象数据结构,它可以保存类型int16_t,int32_t,int64_t的整数。

typedef struct intset{
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
}intset;

cotents数组是整数集合的底层实现。

压缩列表

压缩列表是redis为了节约内存空间而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。

一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

d

s

对象

posted @ 2021-02-26 17:28  lfcom  阅读(64)  评论(0编辑  收藏  举报