Redis的几种底层数据结构
简单字符串
-
Redis的字符串是自己构建的一种名为简单动态字符串(SDS)的抽象类型。 和常规c语言字符串不同
-
SDS的定义如下:
struct sdshdr{
int len; //记录buf数组中已使用字节的数量 既SDS所保存字符串长度
int free; // 记录buf数组中未使用字节的数量
char buf[]; //字节数组,用于保存字符串
};
如图所示 存储 “Redis ”这个字符串
- SDS 可以常数获取字符串长度,里面的len记录的字符串的长度,所以可以直接获取。而c语言需要O(n)的复杂度
- SDS可以杜绝缓冲区溢出。当API对SDS进行修改时,API会先检查SDS的空间是否满足修改需求如果不满足,会先扩展空间
- 可以减少修改字符串时带来的内存分配次数。
- 空间预分配。当SDS进行修改的时候,程序不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用空间(free)
- 惰性空间释放。当SDS的API需要缩短SDS保存的字符串时,使用free属性记录下来,并不会去回收这些内存
- 二进制安全。所有SDS API 都是二进制安全,API都会以处理二进制的方式来处理SDS存放在buf数组里的数据。
链表
-
链表提供了高效的节点重拍能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表长度
-
链表的结构:
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;
字典
字典,又称为符号表,关联数组或映射,是一种用于保存键值对的抽象数据结构 ,在字典中,一个key和一个value进行关联,字典中的key是独一无二的
-
字典的实现:
1.哈希表: typedef struct dicht{ dictEntry **table; // 哈希表数组 unsigned long size; // 哈希表大小 unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于size-1 unsigned long used; // 该哈希表已有的节点数量 }dicht; 2.哈希表节点 typedef struct dicEntry{ void *key; //键 union{ // 值 void *val; uint64_t u64; int64_t s64; }v; struct dicEntry *next; // 指向下个哈希表节点,形成链表 }dicEntry; 3. 字典 typedef struct dict{ dictType *type; //类型特定函数 void *privdata; //私有数据 dictht ht[2]; // 哈希表 int trehashidx;//rehash 索引,当rehash不再进行时 值为-1 }dict; 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,const void *key); //销毁值的函数 void (*valDestructor)(void *privdata,const void *obj); }dictType;
图示:
-
解决键冲突问题。当有两个或以上数量的键被分配到哈希表数组的同一个所以上面时,我们采用链地址法来解决键冲突
图示:
-
rehash。随着操作的执行,哈希表保存的键值对会逐渐曾多或者减少,为了维护哈希表在一个合理范围内,进行rehash操作
- 为字典的ht[1] 哈希表分配空间
- 将保存在ht[0]的所有键值对重新计算哈希值和索引值,然后将键迁移到ht[1]
- ht[0]包含的所有键值对都迁移到ht[1]之后(ht[0]变为空)释放ht[0],将ht[1]设置为ht[0],并且在ht[1]新创建一个空白哈希表为下一次rehash 做准备
-
渐进式rehash。rehash的动作不是一次性集中式的完成 ,而是分多次,渐进式的完成。
跳跃表
跳跃表是一种有序数据结构,通过每个节点中维持多个指向其他节点的指针,从而到达快速到达访问节点的目的
整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多的时候,Redis会使整数集作为集合键的底层实现
-
集合的实现:
typedef struct intset{ // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; }intset;
图示:
-
升级。 每当我们将一个新元素添加到整数集合,并且新元素的类型比整数集合现有的元素类型要长时,我们要进行升级
-
升级的好处:提升灵活性,节约内存
-
整数集合不支持降级操作,一旦升级,编码就会一直保持升级后的状态
压缩列表
压缩列表是列表键和哈希键的底层实现之一
-
压缩列表的构成:
压缩列表是为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构
如图所示:
-
previous_entry_length。节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一点的长度,从表位向前遍历压缩列表时只用拥有一个只想某个节点的起始地址指针,通过这个指针以及这个节点的 previous_entry_length程序就可以向前一个节点回溯
-
encoding。节点的encoding属性记录了节点的content属性所保存数据的类型以及长度
-
content。节点的content负责保存节点的值
-
连锁更新。如果当前列表的节点e1到eN的节点长度都小于254字节,如果我们将一个长度大于等于254字节的新节点设置为压缩列表的表头时程序会不断地对压缩列表执行空间重分配操作直到eN位置,操作成为“连锁更新”