Loading...

Redis(三)——底层数据结构

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

1、底层数据结构

  • Redis由C语言编写,存储是以key-value的形式的。Redis中的key一定是字符串,value可以是string、list、hash、set、sortset这几种常用的。

    image-20210205152908010

  • Redis将key-value分别都抽取为了一个对象,使用对象来表示数据库中的键和值。在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。

  • Redis中的每个对象都由一个redisObject结构来表示:

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

    image-20210205153313746

    image-20210205154157930

  • redis对象的一些细节:

    • 服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。

      • 比如我们的数据结构是sortset,但你使用了list的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令
    • Redis的对象系统带有引用计数实现的内存回收机制

      • 对象不再被使用的时候,对象所占用的内存会释放掉
    • Redis会共享值为0到9999的字符串对象

    • 对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间。

简单字符串SDS

  • C语言中String用char[]数组表示,Redis源码中用简单动态字符串SDS(simple dynamic string)封装char[],这是是Redis存储的最小单元,一个SDS最大可以存储512M信息。

    struct sdshdr{
    
        // 字节数组,用于保存字符串
        char buf[];
    
        // 记录buf数组中已使用的字节数量,也是字符串的长度
        int len;
    
        // 记录buf数组未使用的字节数量
        int free;
    }
    

    image-20210205154142969

  • 使用SDS的好处:

    1. sdshdr数据结构中用len属性记录了字符串的长度。那么获取字符串的长度时,时间复杂度只需要O(1)
    2. SDS不会发生溢出的问题,如果修改SDS时,空间不足。先会扩展空间,再进行修改!(内部实现了动态扩展机制)。
    3. SDS可以减少内存分配的次数(空间预分配机制)。在扩展空间时,除了分配修改时所必要的空间,还会分配额外的空闲空间(free 属性)。SDS是惰性释放空间的,你free了空间,可是系统把数据记录下来下次想用时候可直接使用。不用新申请空间。
    4. SDS是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据。

双向链表List

  • Redis中的List底层使用的是无环双向链表。

  • 获取表头指针,表尾指针,链表节点长度的时间复杂度均为O(1)

  • 链表使用void *指针来保存节点值,可以保存各种不同类型的值。

  • 使用listNode结构来表示每个节点:

    typedef strcut listNode{
    
        //前置节点
        strcut listNode  *pre;
    
        //后置节点
        strcut listNode  *pre;
    
        //节点的值
        void  *value;
    
    }listNode
    
  • 使用listNode是可以组成链表了,Redis中使用list结构来持有链表

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

image-20210205155027581

哈希表Hash

  • 在Redis中,key-value的数据结构底层就是哈希表来实现的。

  • 在Redis里边,哈希表使用dictht结构来定义:

    typedef struct dictht{
    
        //哈希表数组
        dictEntry **table;  
    
        //哈希表大小
        unsigned long size;    
    
        //哈希表大小掩码,用于计算索引值
        //总是等于size-1
        unsigned long sizemark;     
    
        //哈希表已有节点数量
        unsigned long used;
    
    }dictht
    

image-20210205155200029

  • 哈希表的节点是怎么实现的dictEntry:

    typedef struct dictEntry {
    
        //键
        void *key;
    
        //值
        union {
            void *value;
            uint64_tu64;
            int64_ts64;
        }v;    
    
        //指向下个哈希节点,组成链表
        struct dictEntry *next;
    
    }dictEntry;
    
  • Redis为了更好的操作,对哈希表往上再封装了一层,使用dict结构来表示:

    typedef struct dict {
    
        //类型特定函数
        dictType *type;
    
        //私有数据
        void *privdata;
    
        //哈希表
        dictht ht[2];
    
        //rehash索引
        //当rehash不进行时,值为-1
        int rehashidx;  
    
    }dict;
    
    
    //-----------------------------------
    
    typedef struct dictType{
    
        //计算哈希值的函数
        unsigned int (*hashFunction)(const void * key);
    
        //复制键的函数
        void *(*keyDup)(void *private, const void *key);
    
        //复制值得函数
        void *(*valDup)(void *private, const void *obj);  
    
        //对比键的函数
        int (*keyCompare)(void *privdata , const void *key1, const void *key2)
    
        //销毁键的函数
        void (*keyDestructor)(void *private, void *key);
    
        //销毁值的函数
        void (*valDestructor)(void *private, void *obj);  
    
    }dictType
    

image-20210205155629244

  • 也就是说Redis中有两个哈希表

    • ht[0]:用于存放真实key-vlaue数据
    • ht[1]:用于扩容(rehash)
  • Redis中哈希算法和哈希冲突跟Java实现的差不多,它俩差异就是:Redis哈希冲突时:是将新节点添加在链表的表头

  • 为什么 dictht ht[2]是两个呢?

    • 数据量如果过大的话,一次性rehash会有庞大的计算量,这很可能导致服务器一段时间内停止服务。
    • 为了扩容的同时不影响前端的CURD,要慢慢的把数据从ht[0]转移到ht[1]中,同时rehashindex来记录转移的情况,当全部转移完成,将ht[1]改成ht[0]使用。
    • hashidx = -1说明当前没有扩容,rehashidx != -1则表示扩容到数组中的第几个了。
  • Redis具体是rehash时这么干的:

    1. 在字典中维持一个索引计数器变量rehashidx,并将设置为0,表示rehash开始。
    2. 在rehash期间每次对字典进行增加、查询、删除和更新操作时,除了执行指定命令外;还会将ht[0]中rehashidx索引上的值rehash到ht[1],操作完成后rehashidx+1。
    3. 字典操作不断执行,最终在某个时间点,所有的键值对完成rehash,这时将rehashidx设置为-1,表示rehash完成
    4. 在渐进式rehash过程中,字典会同时使用两个哈希表ht[0]和ht[1],所有的更新、删除、查找操作也会在两个哈希表进行。
    5. 例如要查找一个键的话,服务器会优先查找ht[0],如果不存在,再查找ht[1],诸如此类。此外当执行新增操作时,新的键值对一律保存到ht[1],不再对ht[0]进行任何操作,以保证ht[0]的键值对数量只减不增,直至变为空表。

跳表shiplist

  • 跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一。

  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点

  • zskiplistNode跳跃表节点的结构:

    typeof struct zskiplistNode {
        // 后退指针
        struct zskiplistNode *backward;
        // 分值
        double score;
        // 成员对象
        robj *obj;
        // 层
        struct zskiplistLevel {
            // 前进指针
            struct zskiplistNode *forward;
            // 跨度
            unsigned int span;
        } level[];
    } zskiplistNode;
    

    image-20210205161119747

    image-20210205161230317

  • zskiplist的结构如下:

    typeof struct zskiplist {
        // 表头节点,表尾节点
        struct skiplistNode *header,*tail;
        // 表中节点数量
        unsigned long length;
        // 表中最大层数
        int level;
    } zskiplist;
    

image-20210205161339370

整数集合intset

  • 整数集合是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,并且元素的数量不多时,Redis就会采用整数集合(intset)作为set(集合)的底层实现。

  • 整数集合(intset)保证了元素是不会出现重复的,并且是有序的(从小到大排序),intset的结构是这样子的:

    typeof struct intset {
        // 编码方式
        unit32_t encoding;
        // 集合包含的元素数量
        unit32_t lenght;
        // 保存元素的数组
        int8_t contents[];
    } intset;
    

    image-20210205161459495

  • 虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64。

  • 如果本来是INTSET_ENC_INT16的编码,想要存放大于INTSET_ENC_INT16编码能存放的整数值,此时就得编码升级(从16升级成32或者64,只支持升级操作,并不支持降级操作)。步骤如下:

    1. 根据新元素类型拓展整数集合底层数组的空间并为新元素分配空间。
    2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位上,需要维持底层数组的有序性质不变。
    3. 将新元素添加到底层数组。

压缩列表ziplist

  • 压缩列表(ziplist)是list和hash的底层实现之一。如果list的每个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)作为list的底层实现。

  • 压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构。

    image-20210205162024888

  • 每一个节点的结构:

    image-20210205162204597

  • 压缩列表从表尾节点倒序遍历,首先指针通过zltail偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表

2、数据类型的底层结构

字符串String

  • string类型有三种编码格式

    • int:整数值,这个整数值可以使用long类型来表示

      • 如果是浮点数,那就用embstr或者raw编码。具体用哪个就看这个数的长度了
    • embstr:字符串值,这个字符串值的长度小于32字节

    • raw:字符串值,这个字符串值的长度大于32字节

  • embstr和raw的区别

    • raw分配内存和释放内存的次数是两次,embstr是一次
    • embstr编码的数据保存在一块连续的内存里面
  • 编码之间的转换

    • int类型如果存的不再是一个整数值,则会从int转成raw
    • embstr是只读的,在修改的时候回从embstr转成raw

列表List

  • list类型有两种编码格式

    • ziplist:字符串元素的长度都小于64个字节&&总数量少于512个
    • linkedlist:字符串元素的长度大于64个字节||总数量大于512个
  • ziplist编码的列表结构:

    image-20210205162812678

  • linkedlist编码的列表结构:

    image-20210205162824082

  • 编码之间的转换:

    • 原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成linkedlist编码的。

哈希Hash

  • hash类型有两种编码格式

    • ziplist:key和value的字符串长度都小于64字节&&键值对总数量小于512
    • hashtable:key和value的字符串长度大于64字节||键值对总数量大于512
  • ziplist编码的哈希结构:

    image-20210205163032630

  • hashtable编码的哈希结构:

    image-20210205163055225

  • 编码之间的转换:

    • 原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成hashtable编码的。

集合Set

  • set类型有两种编码格式

    • intset:保存的元素全都是整数&&总数量小于512
    • hashtable:保存的元素不是整数||总数量大于512
  • intset编码的集合结构:

    image-20210205163204407

  • hashtable编码的集合结构:

    image-20210205163214480

  • 编码之间的转换:

    • 原本是intset编码的,如果保存的数据不是整数值或者元素数量大于512,会转换成hashtable编码的。

有序集合ZSet

  • zset类型有两种编码格式

    • ziplist:元素长度小于64&&总数量小于128
    • skiplist:元素长度大于64||总数量大于128
  • ziplist编码的有序集合结构:

    image-20210205163355352

  • skiplist编码的有序集合结构:

    image-20210205163411564

    • 有序集合(sortset)对象同时采用skiplist和哈希表来实现
      • skiplist能够达到插入的时间复杂度为O(logn),根据成员查分值的时间复杂度为O(1)
  • 编码之间的转换:

    • 原本是ziplist编码的,如果保存的数据长度大于64或者元素数量大于128,会转换成skiplist编码的。

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
posted @ 2021-02-05 19:24  iwehdio  阅读(287)  评论(0编辑  收藏  举报