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

    对象

  • 相关阅读:
    创建Variant数组
    ASP与存储过程(Stored Procedures)
    FileSystemObject对象成员概要
    Kotlin 朱涛9 委托 代理 懒加载 Delegate
    Kotlin 朱涛 思维4 空安全思维 平台类型 非空断言
    Kotlin 朱涛7 高阶函数 函数类型 Lambda SAM
    Kotlin 朱涛16 协程 生命周期 Job 结构化并发
    Proxy 代理模式 动态代理 cglib MD
    RxJava 设计理念 观察者模式 Observable lambdas MD
    动态图片 Movie androidgifdrawable GifView
  • 原文地址:https://www.cnblogs.com/lin7155/p/14402027.html
Copyright © 2011-2022 走看看