Redis 底层数据结构

基本数据类型介绍

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

struct sdshdr {        
	// 记录buf数组中已使用字节的数量
    int len;// 等于SDS已存字符串的长   可以直接得到而非遍历得到字符串的长度
    int free;// 记录buf数组中未使用字节的数量
    char buf[];// 字节数组,用于保存字符串
}; 

虽然SDS是Redis自己定义的一个字符串类型,但是由于它一样满足C字符串以’\0’字符为结尾的规则,所以它可以兼容C的部分<string.h>函数库。

和C字符串的不同之处
1)、常数复杂度获取字符串长度
2)、杜绝缓冲区溢出
3)、减少修改字符串时带来的内存重分配次数
4)、二进制安全:它并不像C语言那样,使用’\0’作为判定一个字符串的结尾,而是使用了独立的len,这样可以保证即使存储的数据中有’\0’这样的字符,它也是可以支持读取的,只关心二进制化的字符串,不关心具体格式.只会严格的按照二进制的数据存取。不会妄图已某种特殊格式解析数据。我们看到SDS中有一个len,代表字符串的长度,SDS他不判断空字符而是根据len来判断字符串是否结束的。

二、链表

typedef struct listNode {
	struct listNode *prev;// 前置节点
	struct listNode *next;// 后置节点
	void *value;// 节点的值
}listNode;
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;

Redis的链表实现的特性可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值

三、字典

字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现

//字典
typedef struct dict {
    dictType *type;// 类型特定函数
    void *privedata; // 私有数据
    dictht  ht[2];// 哈希表
    
    // rehash 索引
    // 在rehash不在进行时,值为-1
    int trehashidx;
}dict;
//哈希表
typedef struct dictht {
	dictEntry **table;// 哈希表数组
	unsigned long size;// 哈希表大小
	unsigned long sizemask;// 哈希表大小掩码,用于计算索引值  总是等于size-1
	unsigned long used; // 该哈希表已有节点的数量
}dictht;
//哈希表结点
typeof struct dictEntry{
	void *key;// 键
	// 值
	union{
		void *val;
		uint64_t u64;
		int64_t s64;
	}
	struct dictEntry *next;// 指向下个哈希表节点,形成链表
}dictEntry;

在这里插入图片描述

Redis使用Murmurhash2算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突

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

因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面
在这里插入图片描述
4、rehash
为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩

扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis对字典的哈希表执行rehash的步骤如下:

1)、为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也就是ht[0].used属性的值)

如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used * 2的2 n 2^n2
n

如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2 n 2^n2
n

2)、将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新就按键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上

3)、当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备)

哈希表的扩展与收缩:

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:

服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
哈希表的负载因子计算公式:

负载因子=哈希表已保存节点数量/哈希表大小

load_factor = ht[0].used / ht[0].size
1
2
在执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免必要的内存写入操作,最大限度地节约内存

当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

5、渐进式rehash
为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]

哈希表渐进式rehash的详细步骤:

1)、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

2)、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始

3)、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序出了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一

4)、随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成

因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。而新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作

跳跃表

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

跳跃表支持平均O ( l o g N ) O(logN)O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现

Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构
在这里插入图片描述
五、整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值