Redis数据库里面的每个键值对都是由对象组成的。其中数据库键只能是字符串对象,数据库键的值可以是以下五种:字符串对象、列表对象哈、希对象、集合对象和有序集合对象。这些对象使用的底层数据结构包括:简单动态字符串(simple dynamic string)、链表、字典、跳跃表(skiplist)、整数集合(intset)、压缩列表。
简单动态字符串
Redis没有使用C语言的字符串表示,而是自己构建了一种新的抽象类型简单动态字符串(simple dynamic string ,SDS)。SDS的结构如下图所示:
struct sdshdr{
int len; //记录buf数组中已使用字节的数量
int free;//记录buf数组中未使用字节的数量
char buf[];//字节数组,用于保存字符串
}
下图展示了SDS的实例:
可以看出SDS遵循C字符串已空字符结尾的的方式,好处是SDS可以直接重用C字符串函数库中的部分函数。和C字符串不同的是,因为SDS在len属性中记录了SDS的本身长度,获取SDS长度的复杂度为O(1)。SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能:当SDS的API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后执行实际的修改操作。在SDS中,buf的数组长度不一定是字符数量加1,数组中可以包含未使用的字节,这些字节的长度由上图SDS的free属性记录。通过未使用的空间,SDS实现了空间预分配和惰性空间释放两种优化策略。
空间预分配方式1:
如果对SDS进行修改之后,SDS的长度小于1MB,程序会分配一个和len属性同样大小未使用的空间,len = free。总长度=len+free+1('\0')
空间预分配方式2:
如果对SDS进行修改之后,SDS的长度大于1MB,程序会再分配1MB的未使用空间。总长度=len+1MB+1B。
惰性空间释放:当SDS的API需要缩短SDS保存的字符串时,不是立即回收缩短后多出来的字节,而是使用free属性将缩短的字节数记录下来,等待将来使用。
链表
链表的结构:
typedef stuct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr)
//节点值释放函数
void (*free)(void *ptr)
//节点值对比函数
int (*match)(void *ptr,void *key)
}
链表的结构图:
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 dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;//sizemask = size - 1
//该哈希表已有节点的数量
}
下图表示了一个为4的空哈希表:
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint_64t u64;
int_64t s64;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}
其中key属性保存着键值对中的键,v属性保存着键值对中的值,next属性指向另一个哈希表节点的指针,将多个哈希值相同的键值对连接在一起,解决键冲突的问题。
字典的结构:
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
int trehashidx;
}
ht属性是包含两个项的数组,数组中的每个想都是一个dictht的哈希表,一般情况,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
哈希算法:根据键值对的键计算处哈希值和索引值,根据索引值,在数组中找到指定的位置。
哈希值计算:hash = dict->type->hashFunction(key);
索引值计算:index = hash & dist->ht[x].sizemask;
解决键冲突:当有两个或多个数量的键都对应数组上的同一个索引,就产生了冲突。Redis的哈希表使用链地址法来解决键冲突。
rehash:随着操作增多,哈希表保存的键值对会逐渐的增多或减少,为了让哈希表的负载因子维持在一个合理地范围,对哈希表的大小进行相应的扩展或收缩。
rehash的步骤:
1.执行扩展操作:ht[1].size = ht[0].used*2
执行收缩操作:ht[1].size = ht[0].used/2
2.将保存在ht[0]的所有键值对rehash到ht[[1]上面
3.当ht[0]的所有键都迁移到了ht[1]之后,释放ht[0],ht[1]=ht[0],在ht[1]创建新的空白哈希表,为下一次rehash做准备。
渐进式rehash:为了避免rehash对服务器的性能造成影响,服务器不是一次性将ht[0]里面所有的键值对全部rehash到ht[1],而是分多次,渐进式地将ht[0]里面的键值对rehash到ht[1]。当对字典执行添加、删除、查找和更新操作时进行rehash。
跳跃表
跳跃表如图;
header:指向跳跃表的表头节点
tail:指向跳跃表的表尾节点
level:记录目前跳跃表内,层数最大的那个节点的层数
length:记录跳跃表的长度,即跳跃表目前包含的节点的数量
层:节点中用L1、L2、L3表示的,L1表示第一层,L2表示第二层。每层有两个属性:前进指针和跨度,带方向的箭头是前进指针,前进指针上面的数字就是跨度
后退指针:用BW字样标记后退指针,指向当前节点的前一个节点。在程序从表尾向表头遍历时使用
分值:各个节点中的数字就是节点保存的分值,在跳跃表中按各自保存的分值从小到大排列
成员对象:各个节点中o1、o2和o3是节点保存的成员对象。
跳跃表结构定义:
typedef struct skiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double scores;
//成员对象
robj *obj;
//层
struct zskiplistNode{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}
}
zskiplistNode的结构:
typedef struct zskiplist{
//表头节点和表尾节点
struct zskiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}
header和tail指针分别指向跳跃表的表头节点和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的时间复杂度为O(1),通过使用length属性来记录节点的数量,程序可以在O(1)的时间复杂度返回跳跃表的长度。level属性用来在O(1)时间复杂度内获取跳跃表中层数最高的那个节点的层数量。
整数集合
整数集合是集合键的底层实现。整数集合是Redis用于保存整数值的集合抽象结构,可以保存的类型是:int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复的元素。
整数集合的结构表示:
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
如下图:一个包含5个int16_t类型整数值的整数集合
contents数组是整数集合的底层实现,length属性记录了整数集合包含的元素数量,即是contents数组的长度。
encoding属性的值为INTSET_ENC_INT16,contents是一个int16_t类型的数组,最小值-32768,最大值32767
encoding属性的值为INTSET_ENC_INT32,contents是一个int32_t类型的数组,最小值-2147483648,最大值2147483647
encoding属性的值为INTSET_ENC_INT64,contents是一个int64_t类型的数组,最小值-9223372036854775808,最大值9223372036854775807
contents数组按从小到大的顺序保存着集合中的五个元素。
每个集合元素都是int16_t类型的整数值,所以contents数组的大小等于sizeof(int16_t) *5=16*5=80位。
升级:当要将一个新元素加入整数集合时,新元素的类型比整数集合中的任意一个元素的类型要长时进行升级操作。升级的步骤:先将整数集合的底层的数组空间大小扩大,为新元素分配内存空间,将之前的元素转换成和新元素相同的类型,将新元素添加到底层数组中。
升级的好处:1、提升灵活性,添加新元素的时候,不用担心新增的元素造成类型错误 2、节约内存,升级只会在有需要的时候进行,尽量节省内存。
压缩列表
压缩列表是列表键和哈希键的底层实现之一。
Redis使用压缩列表来做列表键的底层实现的情况:1.当一个列表键只包含少量列表项,并且每个列表项是小整数或者短字符串 2.当一个哈希键只包含少量键值对,并且每个键值对的键和值是小整数或者短字符串。
压缩列表的构成:
属性zlbytes记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或则计算zlend的位置时使用
属性zltail记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无序遍历整个压缩列表就可以确定表尾节点的位置
属性zllen记录压缩列表包含的节点数量:当这个属性的值小于65535时,这个属性的值就是压缩列表包含的节点数量,当这个属性的值等于65535时,节点的真实数量需要遍历整个压缩列表才能计算出来
属性entryX压缩列表包含的各个节点,节点的长度由节点保存的内容决定
属性zlend特殊值0xFF,用于标记压缩列表的末端
连锁更新:每次空间重分配的最坏时间复杂度为O(N),所以连锁更新的最坏复杂度为O()