【Redis】Redis数据类型底层结构

参考:《Redis设计与实现》

Redis服务器

Redis服务器的16个库由redisServer结构体来存储:

struct redisServer{
    //...
    redisDb *db;
}

Redis客户端

Redis客户端,通过修改指向的Redis服务器的db指针,来切换数据库;

Redis键空间

Redis中的一个库下所有k-v全都保存在一个字典内部:

一个库一个键空间:

typedef struct redisDb{
    //...
    // 键空间
    dict *dict;
    // 过期字典
    dict *expires;
    //...
} redisDb;
  • dict:存储了所有的key-value;

  • expires:

    存储了设置了过期时间的key,value为计算出的过期时间点Unix时间戳;

    value是long类型;

    时间戳,精确到毫秒;

    (1)添加过期时间:将key添加进过期字典;value设置为当前时间戳;

    (2)删除过期时间:将key从过期字典中删除;

    (3)判断过期:value>当前时间戳,则没有过期;小于,则说明已经过期;

RedisObject

Redis五种数据结构底层都是一个C语言下的redisObject结构体:

typedef struct redisObject{
     //类型
     unsigned type:4;
     //编码
     unsigned encoding:4;
     //指向底层数据结构的指针
     void *ptr;
     //引用计数
     int refcount;
     //记录最后一次被程序访问的时间
     unsigned lru:22;
 
}robj

Redis中创建一个key-value,至少会创建两个redisObject:

键对象:一定是一个REDIS_STRING类型;

值对象;

  • type:表明redisObject对象类型:

    • REDIS_STRING:字符串对象
    • REDIS_LIST:列表对象;
    • REDIS_HASH:哈希对象
    • REDIS_SET:集合
    • REDIS_ZSET:有序集合
  • ptr指针:指向具体的数据;

    • 如果是key,指向value(redisObject)
    • 如果是value,指向value的真正的值;
  • refcount:引用计数;

    用于内存回收;

    • 当创建一个对象,引用计数初始化为1;
    • 当对象被一使用一次,计数+1;

String

Redis的字符串底层使用SDS(动态字符串),而不是C语言的字符串;

// SDS的定义
strcut sdshdr{
    // 记录字符串长度
    int len;
    // 记录未使用的buf;
    int free;
    // 字符数组,存放实际的字符串
    char buf[];
}

为什么不使用C的字符串?

C字符串 SDS
获取字符串长度复杂度:O(N) 获取字符串长度复杂度:O(1)
不安全,可能缓冲区溢出 安全,不会溢出,不会泄露
修改字符串,必须执行N次内存的重新分配 修改字符串,除非是全部改需要N次内存分配

List

底层双向链表

typedef strcut list{
    // 头节点
    listNode *head;
    // 尾节点
    listNode *tail;
    // 节点数量
    unsigned long len;
    // 节点复制函数
    void *(*dup)(void *ptr);
    // 节点释放函数
    void *(*free)(void *ptr);
    
}
typedef strcut listNode{
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点值
	void *value;
}

链表特点:

  • 双向链表;
  • 带有头尾指针;
  • 记录链表长度;
  • 多态:同样是listNode,但是可以通过void*指针,保存不同类型的值;(泛型的感觉)

set

集合对象,有两种编码:intsethashtable

当集合对象,可以同时满足下面两条,则使用intset编码:

  • 所有元素都为整数数值;
  • 元素数量不超过512;

否则:使用hashtable编码;

两种编码的存储方式:

Zset跳跃表

是一种有序数据结构,通过每个节点中维持多个指向其他节点指针,达到快速访问;

复杂度:增删改查O(logn),最坏O(n)

  • 通过节点的前进指针的个数(层),以及每个前进指针的跨度,来实现跳跃表;

  • 层:每个节点的多个指针,是一个指针集合List;同层之前指针关联;
  • 前进指针:每一层的从头指向后面的指针;
  • 跨度:记录两个节点间的距离;
  • 后退指针:每个节点,只有一个后退指针,也就是全部后退指针相当于一个单链表;
  • 分值Score:跳跃表,按照Score大小排序;
  • 成员:也就是此节点的value;

Zset插入节点,跳跃表的结构:

(1)每插入一个节点,随机为当前节点设置一个层数;

结构源码:

// zset结构(跳跃表外观)
typedef struct zskiplist{
    // 头节点,尾节点
    structz skiplisNode *header , *tail;
    // 节点数量
    unsigned long length;
    // 最大的节点层数;
    int level;
}

// 跳跃表的结构
typedef struct skiplisNode{
	// 层:是一个节点集合
    struct zskiplistLevel{
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
	// 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;

}

按照上面两个结构,形成的总体为:

  • zskiplist:元数据;
  • zskiplistNode:每一个节点;

Hash表

RedisObject中的ptr指针,指向的就是哈希对象:

typedef struct dict{
    // 指向对哈希表操作的函数
    dictType *type;
    // 私有数据
    void *privdata;
    // ht[1]指向真正的哈希表结构,ht[2]用于备用扩容,指向正在扩容的哈希表
    dictht ht[2];
    // 是否在rehash:如果不在rehash,此值为-1;
    // 当rehash开始,用trehashidx来记录索引
    int trehashidx;
}

哈希表:

typedef struct dictht{
    // 哈希数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 记录尾部:size-1
    unsigned long sizemask;
    // 已使用的大小
    unsigned long used;
}

哈希节点对象:

typedef struct dictEntry{
    // key
    void *key;
    // value
    union{
        void *val;
        unit64_tu64;
        int64_ts64;
    } v;
    // next指针
    struct dictEntry *next;
} dictEntry;

  • 上面的整个哈希表,出现了哈希冲突,使用链地址法解决哈希冲突;

哈希表的扩容

1、为ht[1]分配空间,rehashidx置为0,表示正在rehash,并以此记录索引;

2、采用渐进式rehash,不是rehash开始就一次性的把ht[0]都移动到ht[1];

而是:保持rehash的状态,之后每次对此hash表的元素进行添加、删除、查找、更新时,除了执行相应的操作外,将rehashidx索引处的key-value,移动到ht[1]中;

3、随着对哈希表的操作,终会在某一次操作的时候,rehash完成;把h[1]整个放回ht[0],清空ht[1];

4、渐进式rehash:避免了集中rehash的计算量;

缺点:查找的时候,可能要找的键已经rehash到ht[1]中去了,所以,每次查找,两个ht[0]、ht[1]都要进行查找;

posted @ 2020-07-05 18:15  mussessein  阅读(275)  评论(0编辑  收藏  举报