【Redis 02】Redis底层基本数据结构

Redis有6种基本数据结构:

  • 简单动态字符串(SDS)
  • 双向链表(linkedlist/quicklist)
  • 哈希表 (hash)
  • 跳表(skiplist)(重点关注,面试常问)
  • 整数列表(intset)
  • 压缩列表(ziplist)
  • radix-tree (Stream会用到)

一、简单动态字符串(SDS)

1、数据结构

Redis中所有键的类型都是SDS,值可能是不同类型,当然常见的值类型是SDS。
在这里插入图片描述

(图片来源:极客时间Redis专栏)

如:RPUSH zoo "dog" "panda" "cat"

Key: zoo 是 字符串对象类型,value :dog、panda、cat都是字符串对象类型。

SDS结构定义和数据结构如下:
在这里插入图片描述

  • free:分配未使用的空间;
  • len:分配已使用的空间大小
  • buf:char类型的数组

注:新的版本中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[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; // 已使用的空间大小
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[]; // 字节数组 保存字符串
};

2、与C字符串区别(为什么一个字符串要定义成这样的数据结构?)

  • 获取字符串长度由O(n)变为O(1),直接读取len字段即可知道(Redis关心性能);

  • 空间分配时杜绝缓存溢出:Redis在存储数据之前会检查SDS空间是否足够,不够会先扩展空间然后再进行拼接;

    Redis在分配内存时,会额外多分配一些内存。如:当SDS长度小于1M时,会再多分配len大小的未使用空间,buf数组长度就是为 2*len +1;当SDS长度大于1M时,会多分配1M。通过这种方法,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

二、双向链表 linkedlist

Redis中的链表结构是由 list结构 + 双向链表结点 组成的。
在这里插入图片描述

Redis中链表特点如下:

  • 双端:每个链表结点都有prev和next指针,获取某个节点的前、后置节点时间复杂度都是O(1);
  • 表头指针和表尾指针:通过head和tail获取链表头、尾节点复杂度都是O(1);
  • len:获取链表节点长度

注:3.2版本之后使用quicklist代替了linkedlist,原因是linkedlist的前后指针太占用空间。

quicklist 将list按段切分,每一段是ziplist,通过指针连接。如果ziplist过大,会进一步压缩。

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;	// 保存的数据 指向ziplist
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* ziplist数量 */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* 所有ziplists中元素数量 */
    unsigned long len;          /*quicklistNodes 个数 */
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

三、哈希表 - hash table

1、hash table的数据结构

Redis中字典dict 使用哈希表dictht(Table + Entry数组)来实现。(字典可以理解为管理哈希表rehash时使用,重点理解哈希表就可以)

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; // 哈希表,只会用ht[0], ht[1]用于rehash
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

/* 每个字典会有2个hash表的结构,为rehashing的时候使用。 */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

// 哈希表中的entry节点
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

Redis的字典dictionary定义了两个哈希表,一般只会使用ht[0]哈希表,ht[1]哈希表在rehash时使用。
在这里插入图片描述

(1)哈希算法 - 添加元素过程

  • 首先根据key算出哈希值: hash
  • 再算出在哈希桶中的位置:hash & ht[0].sizemark。(sizemark=size - 1)

(2)解决键冲突

Redis哈希表使用链地址法来解决键冲突,新节点会被放在链表的头部。

(3)rehash (渐进式hash)

当哈希表负载因子过大、键值对数量过多或过少时,Redis都会调整哈希表,建立一个2倍已使用内存的大小:Redis会为ht[1]分配 >= ht[0].used * 2 的2^n空间、

但是rehash且复制元素的过程比较耗时,会造成Redis线程阻塞,无法处理其他请求。

Redis采用渐进式的思想避免rehash对服务器性能造成影响,即:

  • 查找、更新、删除都是先在ht[0]哈希表上操作,ht[0]上没有再到ht[1]上操作;
  • 新增元素只会在ht[1]上操作;
  • 在这期间会逐渐将ht[0]的键值rehash,再复制到ht[1]上。

注:哈希表rehash过程源码在文章末尾。

四、跳表(skiplist)

跳跃表是一种有序数据结构,通过空间换时间的思想在链表的基础上增加了多级索引,通过索引可以快速定位到别的节点的数据,时间复杂度为O(logN) ~ O(N).

跳跃表主要是 有序集合(Sorted Set - Zset)中使用。

// 跳表
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

// 跳表节点
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

// Zset数据结构使用hash表和跳表两种数据结构
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

1、跳跃表数据结构

跳跃表由zskiplist (跳跃表)和 zskiplistNode (跳跃表节点)两个组成。

zskiplist用于保存跳跃表信息(表头节点、表尾节点、长度),zskiplistNode用于表示跳跃表节点。
在这里插入图片描述
在这里插入图片描述

zskiplist : 可以快速访问跳跃表头节点和尾节点,获取跳跃表节点数量。

  • header:指向跳跃表的表头节点;
  • tail:指向跳跃表的表尾节点;
  • level:记录目前跳跃表节点中层数最大的节点(1-32之间,表头节点不算);
  • length:跳跃表节点个数(表头节点不算)

zskipListNode:

  • 层(level):节点中用L1、L2…表示。每个层有两个属性:前进指针和跨度。前进指针是指向下一个层数相同的节点,数字代表跨度;

  • 后退指针(backward):指向当前节点前一个节点;

  • 分值(score):节点中按分值从小到大排序;

  • 成员对象(ele):各个节点中o1、o2是节点保存的成员对象。成员对象是一个指针,最终指向的是一个SDS字符串对象。

    分值可以相同,但是成员对象必须是唯一的。分值相同按成员对象在字典中的大小来排序。

Redis为什么用跳表而不用红黑树?

  • 跳表查询效率Ologn
  • 跳表可以在Ologn时间范围内进行区间查询,效率比红黑树高
  • 跳表代码实现起来没有红黑树简单点

Redis跳表介绍:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGWrUnJF-1616053628026)(../../../../Library/Application%20Support/typora-user-images/image-20201214200352898.png)]

https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

五、整数集合(intset)

整数集合是集合键(set)底层实现之一,保证集合中元素不重复、数组元素按从小到大排列、可保存16(short)、32(int)、64(long)大小的数字。
在这里插入图片描述
根据encoding决定存储的是16还是32还是64字节的整数。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

1、升级规则

当添加新元素时,如果新元素类型比现有元素类型都要长,则进行升级:先分配空间对底层元素进行空间重分配,然后将原有元素包括新添加的元素放入到新的空间中,且需要保持顺序不变。

如:假设原集合保存了3个int_16的元素,则占用空间为3*16=48位。当新添加一个int-32的元素时,就需要升级,先重新分配空间:4 * 32 = 128位,然后再逐渐将元素移动到新位置上,最后将整数集合encoding改成int_32.

在这里插入图片描述

升级可以提升灵活性,不用担心3个元素类型不一样带来的出错,也可以节约内存(一开始可以是int_16,需要的时候才会升级)。

六、压缩列表( ziplist)

压缩列表时列表键和集合键底层实现之一,可以理解成是数组,只是首尾存储了列表长度、个数等。存放较小元素或整数值时使用。查找首尾节点时间复杂度为O(1),查找其他元素复杂度为O(N)

数据结构

1、压缩列表可包含任意多个节点,每个节点保存一个字节数组或整数值。
在这里插入图片描述

  • zlbytes:压缩列表总长
  • zltail:到表尾节点entry5的距离;
  • zllen:列表节点个数;
  • entry:压缩列表节点;
  • zlend:末尾标志位;

2、压缩列表节点

压缩列表节点entry可以保存字节数组或者整数值。
在这里插入图片描述

  • previous_entry_length:前一个节点长度;
  • encoding:记录content数据类型,如11000000代表 int16类型整数。
  • content:可以是字节数组’hello world’ 或者是整数。

哈希表rehash的源码

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    // 确定 哈希表ht[0]有数据:used != 0
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        // rehashidx: 找到哈希桶中第1个元素不为null的位置
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        // de:需要拷贝的哈希桶位置的首节点
        de = d->ht[0].table[d->rehashidx];
        
        
        while(de) {
            uint64_t h;
            // 先保存下个节点
            nextde = de->next;
            // 计算待拷贝的该节点de在新哈希表ht[1]在的hash值
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 放到哈希桶的头位置,指向已经存在的节点
            de->next = d->ht[1].table[h];
            // 赋值
            d->ht[1].table[h] = de;
            
            // 更新ht新、老表已使用内存值
            d->ht[0].used--;
            d->ht[1].used++;

            // 循环复制哈希桶中的下个结点
            de = nextde;
        }
        // 复制完的哈希桶置手动释放内存,rehashidx+1 指向下个哈希桶
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* 老表的used=0时表示完成扩容,将ht[1]的地址赋值给ht[0] */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQzFelQ7-1616053628028)(../../../../Library/Application%20Support/typora-user-images/image-20201214174035987.png)]

猜你喜欢

转载自blog.csdn.net/noaman_wgs/article/details/114981844