redis底层数据结构
一、简单动态字符串(SDS)
定义:
struct sdshdr{
int len; //SDS所保存的字符串长度
int free //记录buf数组中为使用的字节数量,预留内存长度
char buf[] //字节数组,用于保存字符串
}
SDS与C字符串的区别及特点:
1)获取字符串长度:
C字符串:遍历整个字符串,直至遇到结束符为止,复杂度为O(n)。
SDS:在len中记录了本身的长度,所以获取一个SDS长度的复杂度为O(1)。
2)杜绝缓存区溢出
C字符串:不记录本身的长度,当将一个字符串拼接到另一个字符串的末尾时,如果内存不够多的话,就会产生缓存区的溢出。
SDS:在进行拼接之前,会先检查给定的SDS空间是否足够,如果不够,会先扩展SDS的空间,然后才执行拼接操作。
3)减少内存重新分配次数
SDS:通过空间预分配额外空间作为保留使用,从而减少内存的重分配为题。
额外分配空间数量的公式:
1、当SDS的长度小于1MB的时候,分配与len属性的值相同长度的内存作为预存。(实际上所有的长度将多一字节保存空字符)
2、当SDS的长度大于等于1MB的时候,那么程序会分配1MB的未使用空间。
4)惰性空间释放
SDS的API需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收缩短后多出来的字节,
而是使用free属性将这些字节的数量记录起来,并等待将来使用。
5)二进制安全
C字符串:必须符合某种编码。并且除了字符串末尾之外,字符串里面并不能包含空字符,所以导致其职能保存文本数据,
而不能保存像图片、音频、视频、压缩文件这样的二进制文件。
SDS:所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设。
这主要得益于其使用len属性值而不是使用空字符来判断字符串是否结束。所以SDS中的buf属性成为字节数组而不是字符数组。
6)兼容部分C字符串函数
二、链表
链表节点定义:
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;
链表实现特性总结:
1、双端:链表节点带有prev和next指针。
2、无环:表头节点的prev和表尾节点的next都指向null。
3、带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
4、带链表长度计数器:获取长度直接通过len获得,复杂度为O(1)。
5、多态:使用void* 保存节点值,所以可以使用来保存各种不同类型的值。
三、字典
1)哈希表定义
typedef struct dictht{
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsihned long sizemask; //哈希表大小掩码
unsigned long used; //哈希表已有节点的数量。
}dictht;
2)哈希表节点定义(table锁指向)
typedef struct dictEntry{
void *key; //键
union{ //值
void *val;
uint64 _tu64;
int64 _ts64;
}v;
stuct dictEntry *next; //指向下一个哈希表节点
}dictEntry;
3)字典定义
typedef struct dict{
dictType *type; //类型特定函数
void *privdata; //私有数据
dictht ht[2]; //哈希表
int trehashidx; //rehash索引,当rehash不在进行时,值为-1
}dict;
type:指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数。
privdata:保存需要传给那些类型特定函数的可选参数
ht[2]:是一个包含两个项的数组,每个项都是一个哈希表,一般只用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
rehashidx:记录rehash目前的进度,如果目前没有进行rehash,那么它的值为-1。
4)dictType定义(type所指向)
typedef struct dictType{
unsigned int (*hashFunction)(const void *key); //计算哈希值的函数
void *(*keyDup)(void *privdata,const void *key) //复制键的函数
void *(*avlDp)(void *privdata,const void *obj); //复制值的函数
int (*ketCompare)(void privdata, const void *key1,const void *key2); //对比键的函数
void (*keyDestructor)(void *privdata,void *key); //销毁键的函数
void (*valDestrctor)(void *privdata,void *obj) ; //销毁值的函数
}dictType;
5)哈希算法
将新的键值对添加到字典里面。程序先根据键值对的键,计算出哈希值和索引值,然后在根据索引值,将包含
新键值对的哈希表节点放到哈希表数组指定索引上面。
1、使用字典设置的哈希函数。计算出键key的哈希值
2、使用哈希表的sizemask属性和哈希值,计算出索引值
3、根据情况存储在ht[0]或ht[1]上。
6)解决键冲突
当有两个或者以上数量的键呗分配到了哈希表数组的同意个索引上面时,则会发生冲突,redis的哈希表使用链地址的方法解决冲突,
每个哈希表节点有一个next指针,多个哈希表节点可以使用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个
单向链表连接起来,从而解决键冲突的问题。
7)rehash
当哈希表保存的键值对增长或减少到一定的数量时,为了让哈希表的负载因子维持在一个合理的范围之内,程序需对哈希表进行相应的扩展或者收缩。
rehash可以完成扩展和收缩功能,其步骤为
1、为ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当期那包含的键值对数量。
如果是扩展,ht[1]的大小为第一个大于ht[0].used*2的2^n(2的n次方幂)
2、将ht[0]的所有键值对rehash到ht[1]上面,rehash指的是重新计算键的哈希值和索引值,然后放到对应的ht[1]哈希表的指定位置上。
3、ht[0]所有数据都迁移后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建一个空白哈希表,为下一次rehash做准备。
8)渐进式rehash
考虑到大量数据迁移导致CPU繁忙,采用渐进式rehash方案
1、为ht[1]分配空间,同时持有两个哈希表(一个空表,一个有数据)
2、维持一个技术器rehashidx,初始值为0
3、每次对字典增删查改,会顺带将ht[0]中的数据迁移到ht[1],rehashidx++。(ht[0]中的数据只减不增)
4、知道rehash操作完成,rehashidx值设为-1
采用分而治之的思想,将庞大的迁移工作量划分为每一次SURD中,避免服务繁忙。
四、跳跃表
跳跃表定义
typedef struct zskiplist
{
struct zskiplistNode *header,*tail; //表头节点和表尾节点
unsigned long length; //表中节点的数量
int level; //表中层数最大的节点的层数
}zskiplist
zskiplist结构:
header:指向跳跃表的表头节点
tail:指向跳跃表的表尾节点
level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
length:记录跳跃表的长度,也是跳跃表目前包含节点的数量
跳跃表节点定义
typedef struct zskiplistNode{
struct zskiplistLevel{//层
struct zskiplistNode*forward; //前进指针
unsigned int span; //跨度
}level[]
struct zskiplistNode*backward; //后退指针
double score; //分值
robj *obj; //成员对象
;}zskiplistNode;
解析:
层:跳跃表节点的level数组可以包含多个元素,每个元素包含一个指向其他节点的指针。一般来说,层数越多,访问其他节点的速度就越快。
五、整数集合
整数集合的定义
typedef struct intset{
uint32_t encoding; //编码方式
uint32_t length; //结合包含的元素数量
int8_t contents[]; //保存元素的数组
}intset;
encoding的编码决定contents数组的值类型。
升级
何时升级:新添加元素的类型比目前整数集合里所有的元素类型都要长时需要升级。
怎么升:1、扩展整数集合底层数组的空间大小
2、将现有元素都转换与新元素相同的类型,并将转换后的元素按顺序放到新的位置上
3、将新元素添加到底层数组里
升级的好处:1、提升整数集合的灵活性
2、尽可能的节约内存
能否降级? 不能!
六、压缩列表
压缩列表是列表键和哈希键的底层实现之一。
压缩列表的构成
引用redis设计与实现