Redis数据结构与对象

Redis笔记


数据结构与对象

Redis是key—value数据库, 同时支持列表,哈希,集合和有序集。

简单动态字符串

SDS(Simple Dynamic String,简单动态字符串)是Redis默认的字符串表示。

SDS定义

struct sdshdr{

//记录buf数组中已使用字节的数量

//等于SDS所保存字符串的长度

    int len;
//记录buf数组中未使用自己的数量

    int free;
//字节数组,用于保存字符串

    char buf[];
}

SDS与C字符串的区别

常数复杂度获取字符串长度

C字符串的长度需要一个对字符串进行遍历,时间复杂度是O(N)。

而对于SDS而言,数据结构中有len可直接获取字符串长度。设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无须进行任何手动修改长度的工作。

获取字符串的复杂度就降为O(1)

杜绝缓冲区溢出

c语言中,

减少修改字符串时带来的内存重分配次数

SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加1,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。

通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

  1. 空间预分配
    空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改并且需要对SDS进行空间扩展的时候 ,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额外的未使用空间。

  2. 惰性空间释放
    惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

二进制安全

Redis是二进制安全的,意味着Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。

兼容部分C字符串函数

SDS的API是二进制安全,一样遵循C字符串以空字符结尾的惯例:API会在SDS保存的数据末尾设置为空字符,并在buf数组中多分配一个字节来容纳这个空字符,这样SDS可以重用

总结
  • Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS作为字符串表示。

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区。

链表和链表节点的实现
typedef struct listNode{

    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;
}listNode;

list结构:

list 结构
head 表头指针
tail 表尾指针
len 链表长度计数器
dup 复制链表节点保存的值
free 释放链表节点所保存的值
match 对比链表节点所保存的值和另一个输入值是否相等
重点回顾
  • 链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。

字典

字典,又称为符号表,关联数组或映射,是一种用于保存键值对的抽象数据结构。
Redis的数据库是使用字典来作为底层实现的,对数据库的增,删,改,查操作时构建在对字典的操作之上的。

字典的实现

哈希表
typedef struct dictht{

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

}dictht;
dictht 哈希表
table 数组中的每个元素都是指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
size 哈希表的大小,也即是table数组的大小
sizemask sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。
used 而used属性则记录了哈希表目前已有节点的数量
字典

Redis中的字典由dict.h/dict结构表示:

typedef struct dict{

    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引
    //当rehash不在进行时,值为-1
    in trehashidx;

}dict;

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

  • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的紫爱娜设置不同的类型特定函数。

  • privdata属性则保存了需要传给哪些类型特定函数的可选参数。

typedef struct dictType{
    //计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);

    //复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    //复制值的函数
    void *(*valDup)(void *privdata, const void *key);
    //对比键的函数
    int (*keyCompare)(void *privdata,const void *key1,const void *key2);
    //销毁键的额函数
    void (*keyDestructor)(void *privdata, void *key);
    //销毁值的函数
    void (*valDestructor)(void *privdata, void *key);
}dictType;
哈希算法
  1. hash = dict->type->hashFunction(key);
  2. index = hash & dict->ht[x].sizemask;

Redis使用MurmurHash2算法来计算键的哈希值。

解决键冲突

当有两个或以上数量的键被分配到了哈希数组的同一个索引上面是,我们称这些键发生了冲突。

Redis使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表链接起来,这就解决了键冲突问题。

rehash

哈希表保存的键值对会逐渐的增多或者减少,维持哈希表的负载因子。当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。

扩展或者收缩哈希表的工作可以通过执行rehash操作来完成,渐进式的rehash。

哈希表的扩展与收缩

哈希表的负载因子:
load_factor = ht[].used/ht[].size;

跳跃表

跳跃表是一宗有序数据结构,通过在每个节点维持多个指向其他节点的指针,从而达到快速访问节点的目的。

Redis使用跳跃表作为有序集合键的底层实现之一。

Redis在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构,除此之外,跳跃表在Redis里没有其他用途。

跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义。

跳跃表节点
typedef struct zskiplistNode{

    //层
    struct zskiplistLevel{
    //前进指针
    struct zskiplistNode *forward;
    //跨度
    unsigned int span;

    }level[];
    //后退指针
    struct zskiplistNode *backward;
    //分值
    double score;
    //成员变量
    robj *obj;
}zskiplistNode;

跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。

每次创建一个新跳跃表节点的时候,程序都根据幂次定理(power law,越大的数出现的概率越小)可参考长尾分布理解

Redis数据结构与对象_第1张图片

An example power-law graph, being used to demonstrate ranking of popularity. To the right is the long tail, and to the left are the few that dominate (also known as the 80–20 rule).

随机生成介于1-32之间的值作为level数组的大小,这个大小就是层的”高度”。

前进指针

每个层有一个指向表尾方向的前进指针(level[i].forward),用于表头向表尾方向访问节点。

跨度

层的跨度(level[i].span属性)用于记录两个节点之间的距离

遍历只需要前进指针,跨度是用来计算排位的:在查找节点中,将沿途访问过的所有层的跨度累计起来,得到的结构就是目标节点在跳跃表中的排位。

后退指针

节点的后退指针用于从表尾向表头方向访问节点:可以一次跳过多个节点的前进指针不同,每次只能后退至前一个节点。

分值和成员

节点的分值(score属性)是一个double的浮点数

节点的成员对象(obj属性)是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以相同。分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点排在靠近表头的方向,成员对象较大的节点排在靠近表尾的方向。

跳跃表

zskpilist结构的定义:

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

}zskiplist;

header和tail指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度为O(1);

整数集合

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

每个intset.h/intset结构表示一个整数集合:

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

}intset;

contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项item,各个项在数组中按值的大小从小到大有序的排列,并且数组中不包含任何重复项。

升级

加一个新元素进入整数集合里面,新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能添加。

升级整数集合并添加新元素:

  1. 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组的元素转换成现有元素相同的类型,维持数组中元素有序性质不变
  3. 将新元素添加到底层数组里面

升级好处

  • 提升灵活性
  • 节约内存

压缩列表

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

连锁更新

每个节点的previous_entry_length属性都记录了前一个节点的长度:

  • 前一个节点的长度小于254字节,那么previous_entry_length属性需要用1字节长的空间来保存这个长度值。
  • 前一个节点的长度大于等于254字节,那么previous_entry_length属性需要用5字节长的空间来保存这个长度值。

Redis在特殊情况下产生的连续多次空间扩展操作称之为连锁更新。

连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的

  • 首先,压缩列表里恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能被引发,实际情况下不多见;
  • 其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响。
重点回顾
  • 压缩列表是一种为节约内存而开发的顺序型数据结构
  • 压缩列表被用作列表键和哈希键的底层实现之一
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
  • 添加新节点到压缩列表,从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现几率并不高。

对象

Redis没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,系统包括字符串对象,列表对象,哈希对象,集合对象和有序结合对象。

对象类型与编码

Redis使用对象来标识数据库中的键和值,每次当我们在Redis的数据库中仙剑一个键值对时,至少会创建两个对象,一个对象用作键值对的键,另一个对象用作键值对的值。

Redis中的每个对象都是由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性

typedef struct redisObject{

    //类型
    unsigned type:4;

    //编码
    unsigned encoding:4//指向底层实现数据结构的指针
    void *ptr;
}robj;
类型

对象的type属性记录了对象的类型,这个属性的值可以是 string,list,hash,set,zset

编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,这些数据结构由对象的encoding属性决定。

重点回顾
  • Redis系统带有引用计数实现的内存回收机制。

  • Redis会共享0-9999的字符串对象

  • 对象会记录自己的最后一次被访问的额时间,这个时间可以用于计算对象的空转时间

你可能感兴趣的:(Database)