Redis:数据结构

在这里插入图片描述

简单动态字符串SDS

在Redis中,使用简单动态字符串(simple dynamic string)作为默认字符串表示。

SDS 结构如下:

struct sdshdr {
    
    
	int len;	// 字符串长度 单位:字节
	int free;	// 未使用长度
	char buf[];	// 字符串内容
}

SDS保留C字符串以空字符结尾的惯例,结尾所需空间不计算在len长度里。
同时,也因为保留C字符串空字符结尾的惯例,能够兼容部分C字符串函数,避免了不必要的代码重复。

与C字符串的区别

  1. C字符串没有记录len,获取长度的时间复杂度是O(n)。SDS中使用len记录了字符串长度,因此获取长度的时间复杂度是O(1)
  2. C字符串不记录长度,在进行一些如strcat操作的时候,默认认为已经分配了足够的空间,因此在空间不足的时候,会产生缓冲区溢出,造成数据混乱。SDS在修改前会先检查当前空间是否满足修改需求,若不足会进行自动扩展,再执行操作,杜绝了缓冲区溢出
  3. C字符串在存储的时候,总是N+1个字符长的数组,因此每增加/缩短字符串的时候,都要对字符串数组进行重分配操作。SDS会在需要空间扩展的时候,进行空间预分配,若修改后SDS长度<1MB,则会分配当前字符串长度的free空间,即实际长度为len+len+1byte;若修改后SDS长度≥1MB,则会分配1MB的free空间,即实际长度为1MB+len+1byte。此外,SDS还采用惰性空间释放,在字符串缩短的时候,不进行内存重分配。因此SDS减少了字符串长度改变带来的内存重分配次数
  4. 由于C字符串没有记录长度,只以 \0 作为结尾标志,若存储了空字符,会出现读取字符串不完整的情况,这导致C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。SDS是二进制安全的,所有SDS的API会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,被读取就是怎么样。
    在这里插入图片描述

链表

Redis中的链表是双端、无环( head 节点的 prev 指针和 tail 节点的 next 指针都指向 null )、具有多态性(链表节点使用 void* 来保存值)的链表。

// 定义链表节点
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字典使用哈希表作为底层实现,一个哈希表里面可以有多个节点,而每个节点就保存了字典中的一个键值对。

// 哈希表节点
typedef struct dictEntry {
    
    
	void *key;				// 键
	union {
    
    					// 值 可以是指针/uint64_t整数/int64_t整数
		void *val;
		uint64_t u64;
		int64_t s64;
	} v;
	struct dictEntry *next;	// 下个节点
} dictEntry;

// 哈希表
typedef struct dictht {
    
    
	dictEntry **table;		// 哈希表数组
	unsigned long size;		// 哈希表大小
	unsigned long sizemask;	// 哈希表大小掩码 和哈希值一起决定一个键应该放在table的哪个索引上
	unsigned long used;		// 已有节点数量
} dictht;

// 字典
typedef struct dict {
    
    
	dictType *type;		// 类型特定函数
	void *privdata;		// 私有数据
	dictht ht[2];		// 哈希表
	int rehashidx;		// rehash进度 若当前不在rehash,则为-1
} dict;

从上面的定义我们可以看到字典的哈希表 ht 有2个项,正常情况下只会使用 ht[0] , rehashidx 的值也默认为-1。只有在 rehash 的时候会使用 ht[1] ,并用 rehashidx 记录当前 rehash 的进度。

渐进式 rehash

在进行 rehash 的时候,不是马上将 ht[0] 的哈希节点重分配到 ht[1] 的,而是渐进完成的,详细步骤如下:

  1. 为 ht[1] 分配空间。
  2. 将字典中的 rehashidx 值设置为0,表示rehash开始工作。
  3. 在 rehash 进行期间,每次对字典进行操作,会顺带将 ht[0] 哈希表在 rehashidx 上的所有键值对重新分配到 ht[1] 中,完成后将 rehashidx+1。
  4. 重复步骤3直至 ht[0] 所有键值对被 rehash 到 ht[1]。
  5. 释放 ht[0] ,并将 ht[1] 设置为 ht[0] , 同时为 ht[1] 分配一个空白的哈希表。并将 rehashidx 设置为-1,表示 rehash 工作完成。

如下图,为 ht[1] 分配完空间后,将 rehashidx改为0,然后在第一次操作字典的时候,会将 ht[0] 里哈希表索引为0的所有哈希节点(k2)进行 rehash 到 ht[1],然后修改rehashidx=rehashidx+1=1;在第二次操作字典的时候,会将 ht[0] 里哈希表索引为1的所有哈希节点(k0)进行 rehash 到 ht[1],然后修改rehashidx=rehashidx+1=2;一直重复直到遍历完哈希表。
图源《Redis设计与实现》

跳跃表

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

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

// 跳跃表
typedef struct zskiplist {
    
    
	structz skiplistNode *header, *tail;	// 头、尾指针
	unsigned long length;	// 节点数量(除头结点)
	int level;				// 节点最大层次(除头结点)
}

示例如下:
图源《Redis设计与实现》

整数集合

当一个集合只包含整数值元素,且元素数量不多的时候,Redis 会使用整数集合作为集合键的底层实现。

// 整数集合
typedef struct intset {
    
    
	uint32_t encoding;	// 编码方式 int16_t/int32_t/int64_t
	uint32_t length;	// 元素数量
	int8_t contents[];	// 保存元素数组 从小到大排序
} intset;

升级

若向整数集合新增一个比原本元素的类型都要长的元素的时候,需要先进行升级。具体步骤如下:

  1. 按新元素类型的大小,扩展数组空间,同时为新元素分配空间。
  2. 将原本的元素进行类型转换,并放置在正确的位置。(原本就是有序的,因此后移即可)
  3. 添加新元素。

好处:提高灵活性(无需重新创建、复制集合),尽可能节约内存(不在一开始就创建最大的内存)

整数集合不支持降级操作,一旦进行了升级,编码就会一直保持升级后的状态,即使原本“大类型”的元素被删除。

压缩列表

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

压缩列表具体组成如下:
在这里插入图片描述
压缩节点具体组成如下:
在这里插入图片描述
通过 previous_entry_length 和当前节点的起始地址,能够计算出前一个节点的起始地址。
通过 encoding 的前两位来区分是字节数组编码,还是整数编码。整数编码(1字节)前2位是 11,此外都是字节编码。在字节编码中,又根据编码长度进行细分,00表示1字节的字节数组编码;01表示2字节的字节数组编码;10表示5字节的字节数组编码。

连锁更新

阅读文章:https://blog.csdn.net/weixin_45729809/article/details/123789656

参考

参考《Redis设计与实现》第一部分:数据结构与对象


猜你喜欢

转载自blog.csdn.net/lena7/article/details/125151845