从根儿上理解 Redis(一)

简单动态字符串

Redis 底层使用 C 语言实现的,但是 Redis 没有直接使用 C 语言传统的字符串表示,而是构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型。
当 Redis 需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会使用 SDS 来表示字符串值。
下面来看一下,为什么 Redis 会使用 SDS 而不是 C 字符串。

SDS 的定义

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

上图是 SDS 结构实例,free 表示生于多少空间,len 表示占用了多少空间,末尾 ‘\0’ 不统计。buf 用于存储。

SDS 对比 C 字符串优势:
1.常数复杂度获取字符串长度,C 字符串获取长度需要遍历整个字符串。
2.杜绝缓冲区溢出,SDS 完全杜绝了缓冲区溢出,在新分配和拼接的时候,都会判断空间够不够。
3.减少修改字符串时带来的内存重分配次数。应用空间预分配和惰性kongjianshifang机制。
4.二进制安全,可以保存任意格式的二进制数据。
5.兼容部分 C 字符串函数。

链表

Redis 构建了自己的链表实现,当一个列表键包含了数量比较多的元素,又或者列表中包含的与那素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表。
链表节点结构:

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

多个 listNode 组成的双端链表


image.png

Redis 应用一个 list 结构来持有链表,如下:
typedef struct list{
//表头
listNode *head;
//表尾
listNode * tail;
//节点个数
unsigned long len;
//节点值复制函数
void (dup)(void ptr);
//节点释放函数
void (
free)(void ptr);
//节点值对比函数
int (
match)(void *ptr,void *key);
} list;
list 结构为链表提供了表头指针 head、表尾指针 tail、以及链表长度技术器 len,dup 函数用于复制节点所保存的值,free 函数用于释放链表节点所保存的值,match 函数用于对比链表节点所保存的值和另一个输入值是否相等。
list 示意图:

image.png

字典

字典,又称为符号表、关联数组或映射,是一种用于保存键值对的抽象数据结构。
Redis 的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表结点,每个哈希表结点就保存了字典中的一个键值对。
哈希表结构:

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

table 属性是一个数组,数组中每个元素都是一个指向 dictEntry 结构的指针,每个 dictEntry 结构保存着一个键值对。size 记录哈希表的大小,used 标识哈希表已有节点的数量。sizemark 总是等于 size - 1。

哈希表节点:

typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        unit64_tu64;
        int64_ts64;
    }v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
}dictEntry;

next 属性指向另一个哈希表节点指针,这个指针可以将多个哈希值相同的键值对连接在一起,来解决哈希冲突。


image.png

字典结构:

typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dict ht[2];
    //rehash 索引
    //当 rehash 不在进行时,值为 -1
    int rehashidx;
}dict;

type 属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一簇用于特定类型键值对的函数。
privdata 属性保存了需要传给那些类型特定函数的可选参数。

image.png

跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
大部分情况下,跳跃表可以和平衡树相匹配。
Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
跳表示例:


image.png

header:指向跳跃表的表头节点。
tail:指向跳跃表的表尾节点。
level:记录目前跳跃表内,层数最大的那个节点的层数。
length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量。
跳表节点每一层都有前进指针和跨度,跨度标识前进指针指向的节点和当前节点的距离。
每个节点都有一个后退指针,指向前一个节点。
1.0 2.0 3.0 是每个节点的分值,按顺序排列。
o1,o2,o3 是节点所保存的成员对象。

跳跃表节点

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

跳跃表

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

整数集合

整数集合是 Redis 用于保存整数值的集合抽象数据结构,可以保存的类型为 int16_t、int32_t 或者 int64_t 的整数,并且保证集合不会出现重复元素。

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

contents 是整数集合的底层实现,整数集合的每个元素都是 contents 数字的一个数组项。
length 属性记录了整数集合包含的元素数量。
encoding 决定 contents 数组的类型。


image.png

整数集合的升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有元素的类型长,整数集合就要做升级。
升级分为三步:
1.根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
例如下图,原本集合中的元素是 int16_t 类型的 1,2,3。然后将 65535 添加进去,因为 65535 是 int32_t 类型,所以就要做升级。


image.png

2.将底层数组现有元素转换成新元素相同类型,所有元素如下图,从后往前,一次重新放到新分配的空间的 3,2,1 的位置。

image.png
image.png
image.png

整数集合升级好处就是灵活性和尽可能的节约内存。
整数集合不支持降级操作。

压缩列表

压缩列表是列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项,并且每个列表要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现。
压缩列表的构成:


image.png

image.png

压缩列表节点构成

每个压缩列表节点可以保存一个字节数组或者一个整数值。


image.png

previous_entry_length,记录了压缩列表中前一个节点的长度。
encoding,记录了节点的 content 属性所保存的数据类型及长度。
content,保存节点的值。

压缩列表有连锁更新机制,跟上面那个整数集合升级有点像。
previous_entry_length 记录了压缩列表中前一个节点的长度,previous_entry_length 的大小是一个字节,两个字节或者五个字节,根据前一个元素大小有关。但是假如在多小于 254 字节数之前添加一个大于等于 254 字节的数,后续的 previous_entry_length 都要挨个更新长度。

最后

至此 Redis 的底层数据结构就介绍完了,来看下下面这个表,Redis 使用的不同类型队形的编码方式。是不是就很清晰了。

image.png

参考:Redis 设计与实现

推荐阅读更多精彩内容

  • 转载:可能是目前最详细的Redis内存模型及应用解读 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据...
    jwnba24阅读 442评论 0 4
  • Redis 是一个键值对数据库(key-value DB),数据库的值可以是字符串、集合、列表等多种类型的对象,而...
    吴昂_ff2d阅读 1,553评论 0 5
  • 雨过天晴,阳光明媚,换上喜欢的衣服,快快乐乐出门上班。 恰逢世界微笑日,让我们嘴角上翘,我们用微...
    嫣嫣洋阅读 136评论 0 3
  • 昨晚放学的时候,在楼梯上遇到高一带过的学生。互相打招呼,我调侃:最近瘦了,读书用功了吧?他则告诉我一个惊人的消息:...
    紫色小路阅读 58评论 0 1
  • 所有的问题都会被解决。 你有能力解决你遇到的所有问题。 因为老天爷不会夸大一个人的能力,只要是你遇到的问题,你都有...
    cc2974阅读 91评论 0 1