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

剖析源代码使用的Redis源代码为4.0.10版本

可以直接从redis的官网redis.io获取

本次讲解Redis底层数据结构,对于比较基础的内容不会做过多的讲解。

字符串

字符串是Redis中的基础数据结构

所有的key都是字符串

Redis中因为众所周知的原因放弃了C语言原生字符串进行了封装

redis中的字符串被称为Simple dynamic string

举个应用场景的例子:

redis> SET msg "Hello World"
OK

在这个场景中Key是msg,其底层就是一个保存着msg的SDS,value的”Hello World"的底层也是一个SDS

在sds.h中可以看到SDS的结构

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

其中len用于记录字符串长度,alloc用于记录分配的空间。

对于常用API均在sds.h文件中有封装

相比较于C的字符串,SDS可以常数时间获取长度,同时防止了缓冲区溢出。在此不展开。有兴趣可以查看源码和相关书籍

链表

在adlist.c中定义了链表节点和链表结构

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    listNode *head;
    listNode *tail;
    //复制函数
    void *(*dup)(void *ptr);
    //释放函数
    void (*free)(void *ptr);
    //匹配函数
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;


可以看到Redis中的链表为双向链表,同时会记录链表长度。

节点中的值为(void *)类型,目的是保存各种值。

三个函数指针是出于多态的目的设计的,用于保存不同的函数实现不同数据类型的各种功能。

listIter是一个用于遍历链表的迭代器,direction表示方向,用于正向或者反向遍历链表。

链表API请看adlist.c文件,都是链表的常规操作。

字典

直接上代码

在dict.h中定义了字典的结构

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 
    //rehashidx=-1表示不在rehash过程中
    unsigned long iterators; /* number of iterators currently running */
} dict;

dichht中定义了一个table(哈希表)用于存放Entry,Entry都是一个KV对。Entry用next指针指向下一个Entry,dict中用拉链法解决Hash冲突。sizemask用于计算索引值。

放一张比较清楚的图,图片来自于《Redis的设计与实现》

hash

一个dict中有2个dictht,这个主要作用在于rehash扩容。

Redis为了防止一次性扩容使得客户端响应时间增加,采用了一种渐进式rehash的方法,每次对数据进行操作时,将一部分的数据rehash到另一个dictht中。

rehash的时机:负载因子(used/size)大于1。

跳跃表

skiplist

非常神奇的数据结构,数学上可以证明这个东西搜索的平均时间复杂度时O(lgN),和搜索树差不多。从上往下看其实就是一颗二叉搜索树。

在redis.h中定义了skiplist相关的结构(很奇怪为什么在这里定义。。)

typedef struct zskiplistNode {
    sds ele;
    double score;//权重
    struct zskiplistNode *backward;//后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;//前进指针
        unsigned int span;//跨度,用于表示该位置到下一个节点,需要跳过几个最底层节点
    } level[];//层级数组
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;//头尾节点
    unsigned long length;//跳表长度
    int level;//层数
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;


可以看出有序集合使用的结构是一个dict和一个跳跃表。

跳跃表中,每一个节点都有一个层数,层数在创建节点随机决定。层数越高,节点数越少。

每一层形成一个链表,搜索时从最高层开始,搜到则返回结果搜到不存在的节点则向下走一层,直到搜到最底层仍没有则表中不存在该值。

附一个跳跃表搜索过程。

SkipListSearch

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//ZSKIPLIST_P=0.25
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    //ZSKIPLIST_MAXLEVEL=32
}


该函数用于确定每个节点的层数,最高层数为32,每个节点每次有0.25的概率层数+1,一个节点层数的期望就是1+0.25^1+0.25^2+···+0.25^n。求和就是每个节点约1.33层。

插入一个节点时,先从最高层开始搜索,找到插入位置插入,之后下降一层,继续寻找插入位置插入,依次类推直到每一层都插入。代码细节比较多,这里不贴。有兴趣可以查看t_zset.c中相关部分代码。

skiplist的优势在于可以O(lgN)时间复杂度进行查找,有序集合中常用的统计范围中的数据就利用了这个优势。使用dict的目的在于O(1)复杂度进行查找成员。

下周更新剩下的数据结构,quicklist,ziplist,intset

参考资料:

1.《Redis设计与实现》 黄健宏
2. https://blog.csdn.net/acceptedxukai/article/details/17333673
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值