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

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数据类型及其内部数据结构》 allen独白

    原文作者:allen哦
    原文地址: https://www.jianshu.com/p/f8ccf8806095
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞