zoukankan      html  css  js  c++  java
  • redis学习(十) 数据结构

    字符串结构

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

    简单字符串结构中,buf存储的字符数组也是使用''作为字符数组的结尾,但是在使用上对用户是透明的,这个设计能重用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判断字符串结尾,而不是使用''字符。

    链表

    每个链表使用一个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

    对象

  • 相关阅读:
    kill process
    USB development guide
    MMC device
    memtester
    printf()格式化输出详解
    C语言动态内存分配
    归并排序C语言
    c 文件操作
    数据包分析
    C语言文件操作函数大全
  • 原文地址:https://www.cnblogs.com/lin7155/p/14402027.html
Copyright © 2011-2022 走看看