redis的数据结构

                   redis的数据结构

目录

简单动态字符串(SDS)

链表(list)

字典(dict)

跳跃表(zskiplist)

整数集合(intset)

压缩列表(ziplist)

对象



简单动态字符串(SDS)


redis的默认字符串:简单动态字符串(simple dynamic string, SDS)的抽象类型
每个sds.h/sdshdr结构表示一个SDS值:

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

SDS示例:


SDS对比c字符串的优势:
1、常数复杂度获取字符串长度,时间复杂度从O(n) 降到O(1)
2、杜绝缓冲区溢出,因为SDS的空间分配策略,API在对SDS修改前会检查其空间是否满足修改所需的要求
3、减少修改字符串时带来的内存重分配次数。SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联,数组里面可以包含未使用的字节。
SDS实现了空间预分配和惰性空间释放两种优化策略。
1)空间预分配
如果空间足够,API就会直接使用未使用空间,而无需执行内存重分配。SDS的长度(len)小于1MB时,程序会分配和len属性同样大小的未使用空间(free);大于1MB时,程序会分配1MB的未使用空间。
2)惰性空间释放
当SDS的API需要缩短SDS保存的字符串时,不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来来使用。当需要真正释放SDS的未使用空间时,也提供了相应的API,不会造成内存浪费的现象。
4、SDS可以保存文本或者二进制(图片、音频、视频)数据,C语言只能保存文本数据,因其字符串除末尾之外,里面不能包含空字符串。
5、SDS遵循C字符串以空字符结尾的惯例,是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。


链表(list)

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
每一个链表节点使用一个adlist.h/listNode结构来表示:

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

多个listNode可以通过prev和next指针组成双端链表,但使用adlist.h/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;

链表实现的特性:
1、双端:获取某个节点的前置或后置节点的复杂度都是O(1)
2、无环:对链表的访问以NULL为终点
3、带表头指针和表尾指针:获取的时间复杂度为O(1)
4、带链表长度计数器:获取链表中节点数量的复杂度为O(1)
5、多态:链表节点使用void* 指针来保存节点值,可以用于保存各种不同类型的值


字典(dict)

是一种用户保存键值对(key-value pair)的抽象数据结构。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点就保存了字典中的一个键值对。
1、哈希表由dict.h/dictht结构定义:

typedef struct dictht {
        // 哈希表数组,每个dictEntry结构保存着一个键值对
        dictEntry **table;
        // 哈希表大小
        unsigned long size;
        // 哈希表大小掩码,用于计算索引值
        // 总是等于size - 1,这个属性和哈希值一起决定一个键应该被放到table数组的哪一个索引上面
        unsigned long sizemask;
        // 该哈希表已有节点的数量
        unsigned long used;
} dictht;

2、哈希表节点,用dictEntry表示,如下:

typedef struct dictEntry {
        // 键
        void *key;
        // 值,可以是一个指针,或者一个uint64_t整数,又或者是一个int64_t 整数
        union {
            void *val;
            uint64_tu64;
            int64_ts64;
        }v;
        // 指向下个哈希表节点,形成链表,将多个哈希值相同的键值对连接在一次,以此解决键冲突问题
        struct dictEntry *next;
    } dictEntry;

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

typedef struct dict {
        // 类型特定函数
        dictType *type;
        // 私有数据
        void *privdata;
        // 哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用
        dictht ht[2];
        // rehash 索引,记录了rehash目前的进度
        // 当rehash不在进行时,值为-1
        int rehashidx;
    } dict;

type和privdata属性是针对不同类型的键值对,为创建多态字典而设置的,redis会为用途不同的字典设置不同的类型特定函数。
而privdata属性则保存了需要传给那些类型特定函数的可选参数。

typedef struct dictType {
        // 计算哈希值的函数
        unsigned int (*hashFunction)(const void *key);
        // 复制键的函数
        void *(*keyDup)(void *privdata, const void *key);
        // 复制值的函数
        void *(*valDup)(void *privdata, const void *obj);
        // 对比键的函数
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
        // 销毁键的函数
        void (*keyDestructor)(void *privdata, void *key);
        // 销毁值的函数
        void (*valDestructor)(void *privdata, void *obj);
    } dictType;

4、哈希算法
新的减值对添加到字典里面时,需计算哈希值,索引值,然后放到哈希表数组的指定索引上面
# 计算键key的哈希值,Redis使用MurmurHash2算法来计算键的哈希值
hash = dict->type->hashFunction(key);
# 使用哈希表的sizemask属性和哈希值,计算出索引值
index = hash & dict->ht[x].sizemask;
5、解决键冲突
使用链地址法解决键冲突,因dictEntry节点组成的链表没有指向链表表尾的指针,为了效率,总将新节点添加到链表的表头位置(复杂度为O(1))。
6、rehash
为了将负载因子(load factor)维持在一个合理的范围之内,当保存的键值对数量太多或者太少时,需要对哈希表的大小进行相应的扩展或者收缩。
公式:负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
扩展条件(满足下面任一条件即可):
1) 服务器目前没有执行BGSAVE或者BGREWRITEAOF命令,并且load_factro >= 1
2) 服务器目前正在执行BGSAVE或者BGREWRITEAOF命令,并且load_factro >= 5(执行该命令会创建当前服务器的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,当子进程存在时,需要提高执行扩展操作所需的负载因子,避免不必要的内存写入操作,最大限度地节约内存)
收缩条件:
当load_factor < 0.1时,程序自动开始对哈希表执行收缩操作。

7、渐进式rehash
采取分而治之的方式,将rehash键值对所需的计算工作均分摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。


跳跃表(zskiplist)

一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
Redis的跳跃表由redis.h/zskiplistNode 和 redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,zskiplist结构则用于保存跳跃表节点的相关信息。
zskiplistNode结构的定义如下:

typedef struct zskiplistNode {
        // 层
        struct zskiplistLevel {
            // 前进指针
            struct zskiplistNode *forward;
            // 跨度,用于记录两个节点之间的距离
            unsigned int span;
        } level[];
        // 后退指针
        struct zskiplistNode *backward;
        // 分值,按分值从小到大来排序所有节点
        double score;
        // 成员对象
        robj *obj;
    } zskiplistNode;

 zskiplist结构的定义如下:

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

 

1) 定位表头节点和表尾节点的复杂度为O(1)
2) 通过length记录节点的数量,返回跳跃表的长度的复杂度为O(1)
3) level属性在O(1) 复杂度内获取跳跃表中层高最大的那个节点的层数量,特别注意,表头节点的层高并不计算在内。

 

整数集合(intset)

是Redis用于保存整数值的集合抽象数据解耦股,可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
每一个intset.h/intset结构表示一个整数集合:

typedef struct intset {
       // 编码方式
        uint32_t encoding;
       // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组,大小从小到大有序地排列,不包含任何重复项,类型取决于encoding属性的值
        int8_t contents[];
    }

如果每个集合元素都是int64_t类型的整数值,contents数组的大小为
sizeof(int64_t) * 4 = 64 * 4 = 256 位

如果添加新的元素的类型比所有元素的类型都要长时,需要先进行升级。(时间复杂度为O(n))
1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
2)将旧元素的类型转化为新元素的类型,放置到正确的位上,需要继续维持底层数组的有序性质不变;
3)将新元素添加到底层数组里面。

另外,此整数集合不支持降级操作。


压缩列表(ziplist)

是列表键和哈希键的底层实现之一,它为节约内存而开发的顺序型数据结构。
1)压缩列表的构成
  是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

 

2)压缩列表节点(entry)的构成
previous_entry_length: 记录了压缩列表中前一个节点的长度,单位为字节(目的,通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址)
encoding:记录了节点的content属性所保存数据的类型以及长度。
                  字节数组编码:一字节、两字节、五字节长;整数编码:一字节长。
content: 节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

3)连锁更新
添加新节点、删除节点都会引发连锁更新(cascade update),最坏的情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连续更新的最坏复杂度为O(N*N)。
首先,压缩列表里要恰好有好个连续的、长度介于250至253字节之间的节点,连锁更新才有可能被引发;
然后,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响。
在实际中,可以放心使用想ziplistPush等命令,因其命名的平均复杂度仅为O(N), 不必担心连锁更新会影响压缩列表的性能。


对象

Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序对象这五种类型的对象。
当创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
1)类型

 


2)编码
encoding属性记录了对象所使用的编码,即使用了什么数据结构作为对象的底层实现。


每种类型的对象都至少使用了两种不同的编码。
 

使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码。
 3)字符串对象
int: 保存的是整数值,可以用long类型来表示
raw: 字符串值的长度大于32字节(创建对象需要调用两次内存分配)
embstr: 字符串值的长度小于等于32字节(保存短字符串,创建对象只需调用一次内存分配)
long double类型表示的浮点数在redis中也是作为字符串值来保存的。
embstr编码的字符串对象实际上是只读的,如果进行了修改,会转化为raw编码。

4)列表对象
编码转换的条件:
1、满足列表对象保存的所有字符串元素的长度都小于64字节;
2、保存的元素数量小于512个。
ziplist:同时满足两个条件
linkedlist: 不满足条件时
上面的条件的上限值可以修改, 配置文件对应属性:list-max-ziplist-value 和 list-max-ziplist-entries选项

5)哈希对象
编码转换的条件:
1、哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
2、哈希对象保存的键值对数量小于512个。
ziplist:同时满足两个条件
hashtable:不满足条件时
上面的条件的上限值可以修改, 配置文件对应属性:hash-max-ziplist-value 和 hash-max-ziplist-entries选项

6)集合对象
编码转换的条件:
1、集合对象保存的所有元素都是整数值;
2、集合对象保存的元素数量不超过512个。
intset:同时满足两个条件
hashtable:不满足条件时
上面的条件的上限值可以修改, 配置文件对应属性:set-max-intset-entries选项

7)有序集合对象
编码转换的条件:
1、有序集合保存的元素数量小于128个;
2、有序集合保存的所有元素成员的长度都小于64字节。
ziplist: 同时满足两个条件
skiplist:  不满足条件时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值