深入解析Redis——数据结构

简单动态字符串 SDS

 

Redis 没有使用C语言传统的字符串表示,而是自己构建了一种简单动态字符串的抽象类型,用为Redis的默认字符串表示

除了用了保存数据库的字符串值之外,SDS还被用作缓冲区:AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。

struct shshdr{
	int len; // 记录buf数据中已使用字节的数量
	int free; // 记录buf数组中未使用字节的数量
	char buf[]; // 字节数组,用于保存字符串
}

SDS与C字符的区别在于,C语言使用的这种简单字符串表示方式,并不能满足Redis对字符串安全性、效率的要求。

  • 由于C字符串并没有保存字符串的长度信息,当想要获取长度的时候,必须要遍历字符串,时间复杂度为O(N);而SDS 的len变量保存了字符串的长度信息,时间复杂度为O(1)。
  • C中若想要增加或者减少字符串的长度,需要妥善地处理分配或者删除内存,否则会导致缓冲区溢出;SDS则采用了一种空间预分配和惰性空间两种方式来优化这个问题。

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

当修改后SDS的长度小于 1MB的时候,为程序分配和len一样大小的空间,即 free=len;若修改后的长度大于1MB,则分配 1MB的未使用空间。

**惰性空间释放:**惰性空间释放用于优化SDS的字符串缩短操作。当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。当然,SDS也提供了相应的API,在需要的时候真正地释放未使用的内存空间!

**二进制安全:**SDS的API都是二进制安全的(binary-safe),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。SDS使用len属性的值而不是空字符来判断字符串是否结束,同时SDS可以兼容部分C字符串函数。

链表 Linked List

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

链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)。

 // 每个链表节点用一个listNode结构来表示
 struct listNode{
		struct listNode *prev; // 前置节点
		struct listNode *next; // 后置节点
		void *value; //节点的值
}

多个listNode结构就可以组成链表,但是我们通常使用list来持有链表

struct list {
		listNode * head;//表头节点
		listNode * tail;//表尾节点
		unsigned long len;//链表所包含的节点数量
		void *(*dup)(void *ptr);//节点值复制函数,用于复制链表节点所保存的值
		void (*free)(void *ptr);//节点值释放函数,用于释放链表节点所保存的值
		int (*match)(void *ptr,void *key);//节点值对比函数,用于对比链表节点所保存的值和另一个输出值是否相等
} 

深入解析Redis——数据结构_第1张图片

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

  • 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
  • 每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
  • 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。

字典 Dick

在字典中,一个键(key)可以和一个值(value)进行关联,这些关联的键和值就称为键值对,字典中的每一个键都是独一无二的。

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希节点,而每个哈希节点就保存了字典中的一个键值对。

// 哈希表
struct dicthashTabale{
		dictEntry **table;// 哈希表数组 table 其实是一个HashEntry的数组,数组中每个元素都是一个指向HashEntry结构的指针
		unsigned long size;//哈希表大小
		unsigned long sizemask;//哈希表大小掩码,用于计算索引值,总是等于size-1
		unsigned long used;//该哈希表已有节点的数量
}
// 哈希表节点
struct dictEntry {
		void *key;// 键
		union{
				void *val;
				uint64_tu64;
				int64_ts64;
		} v;//值, v属性保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。
		struct dictEntry *next; //指向下个哈希表节点,形成链表
}
//Redis中的字典由 dict结构来表示
struct dict {
		dictType *type;//类型特定函数,指向一个dictType结构的指针,每个dictType保存了一组用于操作特定类型键值对的函数
		void *privdata;//私有数据 保存了需要传给那些类型特定函数的可选参数 
		dictht ht[2];//哈希表,通常只使用 ht[0]作为哈希表,ht[1]只会在对ht[0]rehash的时候使用
		// rehash索引,rehash不在进行时,值为-1
	  int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} 

redis的哈希算法不太复杂,先计算key的hash值,再将hash值与sizemask进行与运算,算出在hashtable中的索引值;面对哈希冲突的情况,redis采用的是拉链法,为了速度来考虑,采用头插法,将新节点插入到链表的头位置。

**Rehash:**为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。Redis对字典的哈希表rehash的步骤如下:

  • 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量。

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

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

渐进式Rehash:渐进式Rehash是将Rehash的过程分多次完成,为了避免大量的数据在Rehash的时候造成服务器因庞大的数据量而停止服务,将每一次rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新的操作上,步骤如下:

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
  • 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  • 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在 rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,rehashindex+1。
  • 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

跳跃表 Skip List

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。Redis使用跳跃表作为有序集合键的底层实现之一。

Redis的跳跃表由zskiplistNode和zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息。


struct zskiplistNode {
		struct zskiplistLevel { //层
			struct zskiplistNode *forward;//前进指针,用于访问位于表尾方向的其他节点
			unsigned int span;//跨度,前进指针所指向节点和当前节点的距离
		} level[];
	struct zskiplistNode *backward; //后退指针,指向当前节点的前一个节点,用于从表尾到表头的遍历。每个节点只有一个后退指针且只能后退一个节点。
	double score;//分值 节点保存的分值,跳跃表中节点按照分值从小到大排序
	robj *obj;//成员对象 节点保存的成员对象
};

每次创建一个新跳跃表节点的时候,程序都根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。跳跃表的遍历操作使用前进指针就可以了,跨度实际上是用来进行排位的。在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。

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

Redis通过一个zskiplist来持有这些zskiplistNode,方便对整个跳跃表进行处理。

struct zskiplist {
		struct zskiplistNode *header, *tail;//表头节点和表尾节点,通过这两个指针,定位头和尾节点
		unsigned long length;//表中节点的数量
		int level;//表中层数最大的节点的层数。表头节点的层高不在计算之内。
} zskiplist;

整数集合 intSet

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

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

contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于 encoding属性的值。

升级:每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。升级机制可以保证集合类型的灵活性,同时节约内存

  • 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  • 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
  • 将新元素添加到底层数组里面。

**降级:**整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态

压缩列表 zipList

你可能感兴趣的:(深入解析Redis,缓存,redis,数据结构)