美文网首页程序员数据库知识点redis学习
redis数据类型及其内部数据结构

redis数据类型及其内部数据结构

作者: allen哦 | 来源:发表于2017-10-29 22:36 被阅读936次

    redis作为最常用的内存型数据库,对外提供了String, List, Hash, Set, Zset 5中数据类型,通过阅读《redis设计与实现》一书,了解了redis数据类型的内部数据结构,在此作一梳理。

    对象

    Redis作为KV式数据,使用对象来表示数据库中的键和值,每一个键值对都对应两个对象,一个是键对象,一个是值对象。比如执行SET msg "hello, redis"后,redis将创建两个对象,一个包含"msg"字符串,另一个则包含"hello, redis"。redis中的每个对象都由一个redisObject来表示,该结构中和保存数据有关 的三个属性分别是type属性、encoding属性和ptr属性:

    typedef struct redisObject { 
    
        //类型 
        unsigned type: 4; 
        
        //编码 
        unsigned encoding: 4; 
        
        //指向底层数据结构的指针 
        void *ptr; 
        
        //... 
        
    } robj;
    

    其中type的值对应5中redis数据类型的类型常量(使用TYPE命令可以获取这个值), encoding则记录了底层实现所用的数据结构(使用OBJECT ENCODING命令可以获取这个值),比如List底层可能用ziplist(压缩列表)实现,也可能用list(双向链表)实现。

    数据结构

    因为redis的每一种数据类型都由其内一种或多种数据结构来实现,因此先看其内部的数据结构,再看其在数据类型中的使用

    简单动态字符串

    redis实现了一个SDS结构(简单动态字符串)类型来替代C语言中的原生字符串, 结构如下

    struct sdshdr { 
        //记录buf中字符串的实际长度
        int len; 
        
        //记录buf数组空闲长度
        int free; 
        
        //字节数组,用于保存字符串 
        char buf[];
    };
    

    和C语言原生字符串相比, 其有一下优点:

    1. 因为存储了字符串长度,可以常数复杂度获取字符串长度
    2. 同样由于存储了字符串实际长度和buff空闲长度,字符串变化时不需要每次都重新分配内存, 同时也避免了字符串长度变化可能导致的缓冲区溢出和内存泄漏
    3. 二进制安全, SDS的设计由于根据字符串长度来判断字符串的末尾,因此中间可以存储任何数据包括空字符

    双向链表

    redis中的链表类型就是最常用的双向链表,涉及数据结构如下:

    //链表节点类型
    typedef struct listNode { 
    
        struct listNode * prev; 
        struct listNode * next;
        void * value; 
         
    }listNode;
    
    
    //链表类型
    typedef struct list { 
    
        //表头节点
        listNode * head; 
        
        //表未节点,方便逆向遍历
        listNode * tail; 
        
        //节点数目, 优化链表求长度
        unsigned long len;
        
        //**
    }list;
    

    Hash表

    redis中的字典类型也就是基本的hash表, 设计数据结构如下:

    typedef struct dictht { 
        //槽位数组
        dictEntry **table; 
        
        //槽位数组长度
        unsigned long size; 
        
        //用于计算索引的掩码 
        unsigned long sizemask;
        
        //真正存储的键值对数量
        unsigned long used; 
        
    } dictht;
    
    
    typedef struct dictEntry { 
    
        //键 
        void *key; 
        
        //值 
        union{ 
            void *val; 
            uint64_ tu64; 
            int64_ ts64; 
        } v;
        
        //指向下一个节点的指针
        struct dictEntry *next; 
        
    } dictEntry;
    

    其hash算法采用的是MurmurHash算法, 处理冲突使用链地址法。随着操作不断进行,当hash表保存的键值对过多或过少时,需要对hash表的槽位数组进行扩展或收缩以获得更好的性能和内存利用率,这个操作称为rehash。redis中的rehash就是通过创建一个临时的字典,将正在使用的字典中的所有键值对复制到临时字典中(这个过程需要重新计算hash值和索引值,因此称为rehash),最后释放老的字典,并将其指针指向临时字典。当数据量比较小的时候,这个过程可以一次性完成,但是数据量大的时候,庞大的计算量可能导致整个服务停止,因此redis采用了渐进式rehash,就是同时维护两个hash表hd[0]和hd[1],设置rehash_idx设置为0,在0< rehash_idx < 槽位数组长度时, 对hash表的任何操作除了执行正常的操作过程以外,还会将hd[0]槽位数组rehash_idx索引上的所有键值对rehash到hd[1],这个过程中的查找会同时使用两个表进行操作,同时所有的添加键值对操作只会在hd[1]上进行,保证了hd[0]只减不增。rehash完成后,hash表指针指向hd[1],释放hd[0]内存。

    跳跃表

    跳跃表是一种有序的数据结构,支持平均O(logN),平均O(N)复杂度的节点查找,其效率可以和平衡树媲美,同时实现简单,而且由于不需要reblance操作,在高并发情况下表现更加出色。redis中的跳跃表也就是基本的跳跃表实现, 涉及数据结构如下:

    //表节点
    typedef struct zskiplistNode { 
    
         //层数组,就相当于索引
         //其中包括了当前节点在每一层的前进指针以及到下一个节点的跨度
         struct zskiplistLevel { 
                //前进指针,本层中的下一个节点
                struct zskiplistNode *forward; 
                
                //跨度,用于计算节点的排位,查找节点过程中,将沿途各层的跨度累加起来,
                //就是当前节点在整个跳跃表中的排位
                unsigned int span;
                
         } level[]; 
         
         //后退指针,指向前一个节点
         struct zskiplistNode *backward; 
         
         //分值,所有节点按照分值排序
         double score; 
         
         //成员对象 
         robj *obj; 
    } zskiplistNode;
    
    
    typedef struct zskiplist { 
        //表头节点和表尾节点 
        struct zskiplistNode *header, *tail; 
        
        //表中节点的数量
        unsigned long length; 
        
        //最大层数
        int level; 
    
    } zskiplist;
    

    整数集合

    实际上就是有序无重复的数组, 可以通过二分法进行查找操作, 不再详叙。

    压缩列表

    压缩列表是Redis为了节约内存而开发的, 是由一系列特殊编码的连续内存块组成的顺序型数据结构,每个压缩列表节点可以保存一个字节数组或一个整数值。其由三部分构成,如下所示:

     ----------------------------------------
    | prev_entry_length | encoding | content |
    -----------------------------------------
    

    其中content为实际数据,encoding存储了content的具体数据类型,prev_entry_length则保存了前一个节点的占用的内存长度,通过这个值就可以定位到前一个节点。在压缩列表中为了节省内存可能会造成连锁更新,因此存在一定的性能隐患,但是由于本身出现的概率就比较小,在节点数量不多的情况下这种影响可以忽略不计。

    数据类型

    String类型

    String类型在Redis底层可以是int,raw和embstr,如果一个String对象保存的是整数值,并且可以使用long来表示,那么将会以整数形式保存。如果一个String对象保存的是字符串值,并且其长度大于32字节,那么就直接使用SDS结构来保存,其encoding标记为raw。如果一个String对象保存的字符串长度小于32字节,那么会使用embstr编码,embstr是一种短字符串的优化,其存储还是使用SDS结构,但raw编码会调用两次内存分配函数来分别创建redisObject结构和SDS结构,而embstr编码则通过调用一次内存分配函数来分配 一块连续的空间,空间中依次包含redisObject和SDS结构。

    List类型

    List类型在Redis底层可以是ziplist(压缩列表)或linkedlist(双向列表)。

    Hash类型

    Hash类型在Redis底层可以是ziplist(压缩列表)或hashtable(Hash表),ziplist编码的哈希对象每当 有新的键值对要插入时,会将保存了键和值的节点依次推入到压缩列表表尾,因此同一键值对顺序存放在一起。

    Set类型

    Set类型在Redis底层可以是intset(整数集合)或hashtable(Hash表),只有当Set中的所有元素均为整数类型时才会使用intset。

    Zset类型

    Zset类型在Redis底层可以是ziplist(压缩列表)或skiplist(跳跃表)。

    相关文章

      网友评论

        本文标题:redis数据类型及其内部数据结构

        本文链接:https://www.haomeiwen.com/subject/ttyupxtx.html